  /* ---- Theme tokens (light default; dark overrides) ----
     Palette tuned to feel like gaitherresearch.org (academic, sober)
     rather than gaitherstephens.com (warmer entertainment). Surfaces
     are off-white with a hint of warm gray in light; deep midnight
     blue-gray in dark. Cyan stays as the brand accent on the
     wordmark, but navy is the structural accent (links, hovers,
     active states for non-brand affordances). */
  :root {
    --bg:        #fafaf7;
    --bg-alt:    #f3f1ec;
    --fg:        #1a1a1a;
    --muted:     #4a4a4a;
    --subtle:    rgba(26,26,26,0.05);
    --line:      #e5e2db;
    --line-strong: #c9c5bc;
    --card-bg:   #ffffff;
    --card-shadow: 0 1px 2px rgba(26,26,26,0.04), 0 4px 12px rgba(26,26,26,0.04);
    /* Brand cyan stays (logo / "News" wordmark) */
    --cyan:      #0099cc;
    --cyan-soft: rgba(0,153,204,0.08);
    /* Academic accent — deep navy, used for non-brand interactive states */
    --accent:    #1e3a5e;
    --accent-hover: #2c5587;
    --accent-faint: #eef2f7;
    /* Left/right framing colors. Dem-blue / Rep-red convention so the
       site reads partisan-coded at a glance. Both desaturated a touch
       so they don't shout against the warm-paper background. */
    --left:      #2f6fbb;
    --left-soft: rgba(47,111,187,0.07);
    --right:     #c23b5e;
    --right-soft: rgba(194,59,94,0.07);
    --magenta:   #c11577;
    --purple:    #6b00c2;
    --serif: "Source Serif 4", Georgia, "Times New Roman", serif;
    --sans:  Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
  }
  /* Colorblind palette: deuteranopia/protanopia-friendly. Avoid
     red/green pairings, lean on blue + amber (Okabe-Ito style).
     Surfaces stay close to light to maximize contrast. */
  [data-theme="colorblind"] {
    --bg:        #ffffff;
    --bg-alt:    #f0f0f0;
    --fg:        #111111;
    --muted:     #555555;
    --subtle:    rgba(0,0,0,0.05);
    --line:      #cccccc;
    --line-strong: #999999;
    --card-bg:   #ffffff;
    --card-shadow: 0 1px 2px rgba(0,0,0,0.06), 0 4px 12px rgba(0,0,0,0.06);
    --cyan:      #0072B2;  /* Okabe-Ito blue */
    --cyan-soft: rgba(0,114,178,0.12);
    --accent:    #D55E00;  /* Okabe-Ito vermillion (deuter-safe) */
    --accent-hover: #E69F00;
    --accent-faint: rgba(213,94,0,0.10);
    --left:      #0072B2;
    --left-soft: rgba(0,114,178,0.08);
    --right:     #D55E00;
    --right-soft: rgba(213,94,0,0.10);
    --magenta:   #CC79A7;  /* Okabe-Ito reddish purple */
    --purple:    #5E3C99;
  }

  /* Retro palette: warm beige magazine vibe, deep brown text,
     mustard/rust accents. Inspired by 70s editorial / GaitherDyn retro. */
  [data-theme="retro"] {
    --bg:        #f4ecd8;
    --bg-alt:    #e8dcc0;
    --fg:        #2a1a0a;
    --muted:     #6b4f2a;
    --subtle:    rgba(42,26,10,0.06);
    --line:      #c7b78f;
    --line-strong: #a08850;
    --card-bg:   #faf3df;
    --card-shadow: 0 1px 2px rgba(80,50,10,0.08), 0 4px 12px rgba(80,50,10,0.10);
    --cyan:      #b8651e;  /* rust replaces cyan for accent */
    --cyan-soft: rgba(184,101,30,0.12);
    --accent:    #7c4a1e;
    --accent-hover: #a36430;
    --accent-faint: rgba(124,74,30,0.10);
    --left:      #2b5d8a;
    --left-soft: rgba(43,93,138,0.10);
    --right:     #a83820;
    --right-soft: rgba(168,56,32,0.10);
    --magenta:   #8b3a62;
    --purple:    #5a3e8f;
  }

  /* 80s Retro / Neon palette: VHS-grade hot magenta + cyan on near-black,
     yellow accents. Reads as an intentional aesthetic, not just "dark
     with extra saturation." Per Gaither 2026-05-14. */
  [data-theme="retro80s"] {
    --bg:        #0a0418;
    --bg-alt:    #140a2c;
    --fg:        #f1e8ff;
    --muted:     #a896c8;
    --subtle:    rgba(241,232,255,0.06);
    --line:      #2a1b48;
    --line-strong: #4a2f7a;
    --card-bg:   #160930;
    --card-shadow: 0 1px 2px rgba(0,0,0,0.35),
                   0 0 14px rgba(255,42,180,0.10),
                   0 4px 18px rgba(0,0,0,0.40);
    --cyan:      #00f0ff;  /* hot neon cyan */
    --cyan-soft: rgba(0,240,255,0.14);
    --accent:    #ff2bd6;  /* hot pink */
    --accent-hover: #ff6be5;
    --accent-faint: rgba(255,43,214,0.14);
    --left:      #5eb9ff;
    --left-soft: rgba(94,185,255,0.14);
    --right:     #ffb84d;  /* sodium-vapor amber */
    --right-soft: rgba(255,184,77,0.14);
    --magenta:   #ff2bd6;
    --purple:    #b347ff;
  }

  [data-theme="dark"] {
    /* Deep midnight rather than flat gray. Pulled directly from
       gaitherresearch.org's dark palette so the two sites feel like
       siblings instead of strangers. */
    --bg:        #0f1419;
    --bg-alt:    #161c23;
    --fg:        #e8e6df;
    --muted:     #a8a59c;
    --subtle:    rgba(232,230,223,0.05);
    --line:      #2a3138;
    --line-strong: #3d454e;
    --card-bg:   #1a2129;
    --card-shadow: 0 1px 2px rgba(0,0,0,0.3), 0 4px 12px rgba(0,0,0,0.2);
    --cyan:      #01ccff;
    --cyan-soft: rgba(1,204,255,0.10);
    --accent:    #7eaad5;
    --accent-hover: #a3c4e3;
    --accent-faint: #1d2937;
    --left:      #6da4d8;
    --left-soft: rgba(109,164,216,0.10);
    --right:     #e26b87;
    --right-soft: rgba(226,107,135,0.10);
    --magenta:   #ee2b8b;
    --purple:    #a560ee;
  }

  * { box-sizing: border-box; }
  html { color-scheme: light dark; background-color: var(--bg); }
  html, body { margin: 0; background-color: var(--bg); color: var(--fg);
    font-family: var(--sans); font-size: 16px; line-height: 1.55;
    -webkit-font-smoothing: antialiased;
    transition: background-color .2s ease, color .2s ease; }
  /* overflow-x: clip on <body>, not <html>. Setting it on <html>
     creates a scroll container at the html level, which kills
     position:sticky on the header — Gaither flagged that the mobile
     header was disappearing entirely on scroll. Putting it on body
     prevents horizontal scrollbars (the original intent) without
     touching the html element's scroll context.

     overscroll-behavior-x: contain + width:100% on html stops the
     "page drifts around after pinch-zoom" problem on mobile per
     Gaither 2026-05-14. The pinch-zoom itself is still allowed (a11y),
     but the browser no longer treats horizontal pans past the
     viewport edge as scroll-chain that drags the whole layout. */
  html {
    width: 100%; max-width: 100%;
    /* overflow-x: clip (NOT hidden) stops the whole page sliding/drifting
       horizontally like a misbehaving web page (Gaither 2026-06-23: make it
       more like an app, no weird horizontal slide). Unlike hidden, clip does
       NOT create a scroll container, so position:sticky on the header still
       works. */
    overflow-x: clip;
    overscroll-behavior: none;
    /* Prevent double-tap zoom on tap targets — best current practice for
       "app-like" mobile UX. touch-action: manipulation removes the 300ms
       delay AND blocks the double-tap-zoom gesture. */
    touch-action: manipulation;
    -webkit-tap-highlight-color: transparent;
    -webkit-text-size-adjust: 100%;
  }
  body {
    overflow-x: clip;
    max-width: 100vw;
    overscroll-behavior: none;
    touch-action: manipulation;
  }
  a { color: var(--cyan); text-decoration: none; }
  a:hover { text-decoration: underline; }

  /* ---- Header ----
     Sticky at top. Scroll-collapses aggressively when scrolled past
     40px: padding shrinks dramatically, brand wordmark shrinks, and
     the background goes semi-transparent with a backdrop blur so the
     page content underneath shows through. JS toggles header.scrolled. */
  header { padding: 24px 32px 16px; border-bottom: 1px solid var(--line);
    display: flex; align-items: center; gap: 20px; flex-wrap: wrap;
    position: sticky; top: 0; z-index: 50;
    background: var(--bg);
    transition: padding .2s ease, background .2s ease,
                backdrop-filter .2s ease, box-shadow .2s ease,
                border-color .2s ease; }
  /* Defensive: re-assert sticky on mobile. Some past CSS regressions
     have broken it; explicit re-declaration here keeps it locked in.
     Also pin a minimum height so the header never collapses to a
     sliver that visually reads as missing — Gaither flagged the
     mobile header as "cut off" repeatedly. */
  @media (max-width: 720px) {
    header {
      position: -webkit-sticky !important;
      position: sticky !important;
      top: 0 !important; z-index: 50;
      min-height: 56px;
      background: var(--bg);
    }
    /* Defensive opaque bg when scrolled too — translucent + blur can
       fail to repaint above iOS URL bar transitions on some devices. */
    header.scrolled { background: var(--bg) !important;
      backdrop-filter: none !important;
      -webkit-backdrop-filter: none !important; }
  }

  /* Inline weather widget — sits inside the header strip. Compact
     pill: location + current temp + 5-day forecast. Forecast hides
     on scroll-collapse and on narrow viewports. */
  .weather-bar {
    display: inline-flex; align-items: center; gap: 8px;
    padding: 0;
    background: transparent;
    font-family: var(--sans); font-size: 12px;
    color: var(--fg);
    flex-wrap: nowrap;
    /* Mobile/tablet baseline: no auto margins, natural flex flow.
       Desktop adds flex:1 below so weather-bar absorbs all leftover
       flex space and centers its content while icon-row pins to the
       far-right edge. Per Gaither 2026-05-17 (eighth iteration on
       icon alignment): icons must be HARD right at all desktop
       widths. */
    margin-left: 0;
    margin-right: 0;
    transition: max-width .2s ease;
  }
  /* Desktop ≥1180: switch header from flex to CSS Grid with 4
     explicit columns:
       col 1 = brand (auto width)
       col 2 = News/Research tabs (auto width)
       col 3 = weather-bar (1fr — takes leftover, content centered)
       col 4 = icon-row (auto width, justify-self end)
     This gives true 3-region layout (brand-left | weather-center |
     icons-right) which auto-margins on a flat flexbox can't deliver
     cleanly. Grid keeps icons hard-right at every width AND avoids
     the "huge empty void in middle" problem flex:1 + justify-content:
     center had on the scrolled state. Per Gaither 2026-05-17 (10th
     iteration on this header): "header is even worse... look at old
     code and test better". */
  /* Macbook-size desktop (1180-1919): TWO-ROW grid layout per Gaither
     2026-05-17. Row 1 holds brand+tabs+weather. Row 2 holds icon-row
     pinned to the right. This frees vertical space at expanded state
     so weather can spread without colliding with icons. */
  @media (min-width: 1180px) and (max-width: 1919px) {
    header {
      display: grid;
      grid-template-columns: auto auto 1fr;
      grid-template-rows: auto auto;
      column-gap: 20px;
      row-gap: 6px;
      align-items: center;
      padding-top: 10px;
      padding-bottom: 14px;
    }
    header > .brand            { grid-column: 1; grid-row: 1; }
    header > .kind-tabs--header { grid-column: 2; grid-row: 1; }
    header > .weather-bar      { grid-column: 3; grid-row: 1; justify-self: center; margin: 0; }
    header > .header-icon-row  { grid-column: 1 / -1; grid-row: 2; justify-self: center; align-self: center; }
    header > .hamburger,
    header > .mobile-icon-drawer-toggle { display: none; }
    header > .header-controls  { display: contents; }
    /* Updated inline on row 2 alongside icons, pinned right. No more
       absolute top-right. Per Gaither 2026-05-31. */
    header .updated {
      position: static;
      top: auto; right: auto;
      grid-column: 1 / -1;
      grid-row: 2;
      justify-self: end;
      align-self: center;
      max-width: 240px;
    }
  }
  /* Icon-label span sits next to the SVG inside each header icon
     button. Hidden by default; only shown at wide desktop (>=1920)
     where there's room. Real <span> instead of ::after because the
     ::after pseudo is reserved by the global [data-tooltip]::after
     tooltip rule. Per Gaither 2026-05-17 "on big expanded version
     of desktop's that are wide, use more text for lower row icons". */
  .icon-label { display: none; }
  /* Macbook retina + wide desktop (>=1180): show text labels next to
     icon-row icons. Buttons go pill-shaped. Per Gaither 2026-05-17
     "on macbook retina screen view expanded header add more text to
     icons". Labels hidden again at scrolled state so the compact
     strip stays thin. */
  @media (min-width: 1180px) {
    .header-icon-row #viewModeBtn,
    .header-icon-row #sortModeBtn,
    .header-icon-row #themeToggle,
    .header-icon-row #fontSizeToggle,
    .header-icon-row #helpBtn {
      width: auto;
      min-width: 0;
      padding: 0 10px 0 8px;
      gap: 5px;
      border-radius: 999px;
    }
    .header-icon-row .icon-label {
      display: inline-block;
      font-family: var(--sans);
      font-size: 11px;
      font-weight: 500;
      color: inherit;
      letter-spacing: 0.01em;
      white-space: nowrap;
      flex-shrink: 0;
    }
    .header-icon-row .bmc-header-label[hidden] { display: inline-block !important; }
    .header-icon-row .bmc-header-label::before { content: "Support"; }
    .header-icon-row .bmc-header { padding: 5px 12px 5px 9px; gap: 5px; }
    /* Scrolled state: drop the labels so the compact strip stays thin. */
    header.scrolled .header-icon-row .icon-label,
    header.scrolled .header-icon-row .bmc-header-label { display: none !important; }
    header.scrolled .header-icon-row #viewModeBtn,
    header.scrolled .header-icon-row #sortModeBtn,
    header.scrolled .header-icon-row #themeToggle,
    header.scrolled .header-icon-row #fontSizeToggle,
    header.scrolled .header-icon-row #helpBtn {
      width: 36px; padding: 0; border-radius: 50%;
    }
    header.scrolled .header-icon-row .bmc-header { padding: 6px 10px; }
  }
  /* Wide desktop (>=1920): slightly smaller labels per Gaither
     2026-05-17 "On full desktop mode make icon text a little smaller
     on expanded view". Was 11.5px before this consolidation; now
     10.5px so the labels are clearly secondary to the icons. */
  @media (min-width: 1920px) {
    .header-icon-row .icon-label {
      font-size: 10.5px;
    }
    .header-icon-row .bmc-header-label,
    .header-icon-row .bmc-header-label::before {
      font-size: 10.5px;
    }
  }
  /* Wide desktop (>=1920): single-row 4-column grid (room for everything). */
  @media (min-width: 1920px) {
    header {
      display: grid;
      grid-template-columns: auto auto 1fr auto auto;
      column-gap: 20px;
      align-items: center;
    }
    header > .brand            { grid-column: 1; }
    header > .kind-tabs--header { grid-column: 2; }
    header > .weather-bar      { grid-column: 3; justify-self: center; margin: 0; }
    header > .header-icon-row  { grid-column: 4; justify-self: end; }
    header > .hamburger,
    header > .mobile-icon-drawer-toggle { display: none; }
    header > .header-controls  { display: contents; }
    /* Wide desktop fits everything on one row: Updated gets its own col 5
       right of the icon strip. Per Gaither 2026-05-31. */
    header .updated {
      position: static;
      top: auto; right: auto;
      grid-column: 5;
      justify-self: end;
      align-self: center;
      max-width: 240px;
      margin: 0;
    }
  }
  /* At SCROLLED state, collapse back to single row even at macbook
     widths so the header strip stays thin. Icon-row jumps to col 4
     of a 4-column grid alongside brand+tabs+weather. */
  @media (min-width: 1180px) {
    header.scrolled {
      grid-template-columns: auto auto 1fr auto auto;
      grid-template-rows: auto;
      row-gap: 0;
    }
    header.scrolled > .header-icon-row {
      grid-column: 4;
      grid-row: 1;
    }
    header.scrolled .updated {
      grid-column: 5;
      grid-row: 1;
      justify-self: end;
      align-self: center;
      margin-left: 12px;
    }
  }
  /* When scrolled, the header tightens to 4px padding and the weather
     strip needs to keep all 5 day tiles visible. Shrink the day-tile
     padding so the row stays inside the available header width without
     clipping. */
  body.scrolled-page .weather-day { padding: 0 3px; min-width: 36px; }
  body.scrolled-page .weather-day .wd-icon svg { width: 15px; height: 15px; }
  .weather-loc {
    /* Compact pill. Per Gaither 2026-05-17 "make the weather location
       picker smaller": 4px gap, 2/7 padding, 11px font (was 5/9/12). */
    display: inline-flex; align-items: center; gap: 4px;
    padding: 2px 7px; border-radius: 999px;
    background: transparent; color: var(--fg);
    border: 1px solid var(--line);
    cursor: pointer; font-family: inherit; font-size: 11px; font-weight: 500;
    white-space: nowrap;
    transition: border-color .15s, color .15s, background .15s;
    flex-shrink: 0;
  }
  .weather-loc:hover { border-color: var(--cyan); color: var(--cyan); }
  .weather-loc svg { width: 11px; height: 11px; }
  /* Default (no zip saved) state — cyan to call attention to the click
     target. Cleared by JS once the user picks a location and the value
     is persisted to gn:weatherLoc. */
  .weather-loc[data-default="true"] {
    border-color: var(--cyan); color: var(--cyan);
    background: var(--cyan-soft);
  }
  .weather-loc[data-default="true"]:hover {
    background: var(--cyan); color: #fff;
  }
  .weather-now {
    display: inline-flex; flex-direction: column; align-items: center;
    gap: 0;
    flex-shrink: 0; line-height: 1.05;
    padding: 0 4px;
  }
  .weather-now-label {
    font-size: 10px; font-weight: 600; letter-spacing: 0.05em;
    text-transform: uppercase; color: var(--muted);
  }
  .weather-now-row {
    display: inline-flex; align-items: center; gap: 4px;
  }
  .weather-icon { display: inline-flex; align-items: center;
    color: var(--cyan); line-height: 1; }
  .weather-icon svg { width: 20px; height: 20px; }
  .weather-temp { font-weight: 700; font-size: 15px; color: var(--fg); }
  /* Today's high / low under the current temp. Smaller font so it
     reads as supplemental info to the headline number. */
  .weather-now-hilo {
    font-size: 10px; color: var(--fg); font-weight: 600;
    line-height: 1.1; margin-top: 1px;
  }
  .weather-now-hilo small { color: var(--muted); font-weight: 500; font-size: 0.9em; }
  .weather-now-hilo:empty { display: none; }
  /* When scrolled: KEEP hi/lo under the current temp (was hidden in
     an earlier pass). Just tighten the line-height so the compact
     header doesn't grow vertically. Gaither 2026-05-13. */
  body.scrolled-page .weather-now-hilo {
    font-size: 9px; margin-top: 0; line-height: 1;
  }
  /* Sunset chip lives under the Today + temp pillar instead of buried
     in the metrics column. Smaller text so the temp stays the focal
     point. Empty by default; refresh() fills it. */
  .weather-sunset {
    display: inline-flex; align-items: center; gap: 3px;
    font-size: 10px; color: var(--muted); line-height: 1.1;
    margin-top: 1px;
  }
  .weather-sunset svg { width: 10px; height: 10px; }
  .weather-sunset:empty { display: none; }
  body.scrolled-page .weather-sunset { display: none; }
  .weather-forecast {
    display: inline-flex; align-items: center; gap: 6px;
    /* overflow: visible so the per-day tooltip ::after popovers escape
       the forecast container instead of getting clipped. The previous
       overflow: hidden was suppressing every weather-day tooltip
       silently. Per Gaither 2026-05-16: "no tooltips over weather days".
       If the forecast row ever needs horizontal-scroll behavior, switch
       to overflow-x: auto + overflow-y: visible so the tooltip stays
       free vertically. */
    overflow: visible;
  }
  /* Metrics — 2-column grid so the 5 items pack into a tight 2x3 block
     instead of a tall vertical column that pushed the day tiles off
     screen. Each cell is an icon + tiny number, lined up cleanly. */
  .weather-meta {
    display: grid;
    grid-template-columns: auto auto;
    gap: 0 8px; column-gap: 8px; row-gap: 1px;
    align-items: center;
    font-size: 10px; color: var(--muted);
    padding-right: 8px;
    border-right: 1px solid var(--line);
    flex-shrink: 0; line-height: 1.25;
  }
  .weather-meta > span { white-space: nowrap; display: inline-flex; align-items: center; gap: 3px; }
  /* Wrapper slot for the metrics. Lives between .weather-now and
     .weather-forecast inside .weather-bar so the six metric chips
     visually separate Today's chip from the multi-day forecast.
     The actual .weather-meta grid is injected by JS; empty slot is
     display:none so the bar collapses cleanly while loading. */
  .weather-meta-slot { display: inline-flex; align-items: center; flex-shrink: 0; }
  .weather-meta-slot:empty { display: none; }
  /* Cyan wire icons in the header weather meta strip per Gaither
     2026-05-17: "use cyan wire icons instead of grey ones for those
     six categories". Matches the per-day tile icon color (which is
     also cyan) so the row reads as a unified weather block. */
  .weather-meta svg { width: 11px; height: 11px; color: var(--cyan); flex-shrink: 0; }
  /* Day tiles — vertical stack: weekday on top, icon, then hi/lo. Much
     cleaner read than the horizontal pill we had before. */
  .weather-day {
    display: inline-flex; flex-direction: column; align-items: center;
    gap: 1px; padding: 0 6px;
    border-left: 1px solid var(--line);
    min-width: 42px; flex-shrink: 0;
    font-size: 11px; line-height: 1.15;
    color: var(--muted);
    white-space: nowrap;
  }
  .weather-day .wd-name { font-weight: 600; color: var(--fg); font-size: 10px;
    letter-spacing: 0.04em; text-transform: uppercase; }
  .weather-day .wd-icon { color: var(--cyan); display: inline-flex; }
  .weather-day .wd-icon svg { width: 18px; height: 18px; }
  .weather-day .wd-hilo { color: var(--fg); font-weight: 600; font-size: 10.5px; }
  .weather-day .wd-hilo small { color: var(--muted); font-weight: 500;
    font-size: 0.9em; }
  .weather-bar[data-loading="true"] .weather-forecast { opacity: 0.3; }
  /* Scrolled-desktop: drop the 5-day forecast, the metrics chips, AND
     the city-name label so the scrolled strip stays compact. Today's
     temp + a city pin icon are what readers need in the persistent
     header strip; full forecast is one scroll-to-top away. Per
     Gaither 2026-05-17: "header on my macbook is overloaded on
     shrink". Earlier "keep 5 days even when scrolled" request
     superseded by this one — overload trumps completeness. */
  body.scrolled-page .weather-meta { display: none; }
  body.scrolled-page .weather-meta-slot { display: none; }
  body.scrolled-page .weather-now-label { display: none; }
  @media (min-width: 1180px) {
    /* Show ALL 5 forecast days at scrolled state on every desktop
       width (macbook + wide). Per Gaither 2026-05-17 follow-up: "make
       5 day weather (today plus 5 days) on macbook screen size view".
       Earlier rule hid the 4th/5th tiles on macbook widths; removed
       so today + 5 days are always visible at desktop scrolled. */
    /* Collapse the city picker pill to just the pin icon at scrolled
       state — the label text is repetitive once the user is reading
       and saves ~120px on the strip. */
    body.scrolled-page header.scrolled .weather-loc .weather-loc-label {
      display: none;
    }
    body.scrolled-page header.scrolled .weather-loc {
      padding: 3px 6px;
    }
  }
  /* (Macbook 3-day rule and the wide-desktop 5-day override removed
     2026-05-17 — all desktop widths now show today + 5 days at
     scrolled.) */
  /* Mobile: drop forecast and label to icon-only by default. Tap the
     degree-circle toggle (top right of the weather bar) to expand to
     the full 5-day forecast + location label. State persisted in
     localStorage gn:weatherExpanded so it sticks across reloads. */
  @media (max-width: 720px) {
    .weather-bar { position: relative; padding-right: 30px; }
    .weather-bar:not([data-expanded="true"]) .weather-forecast { display: none; }
    .weather-bar:not([data-expanded="true"]) .weather-loc-label { display: none; }
    .weather-bar:not([data-expanded="true"]) .weather-loc { padding: 3px 6px; }
    /* Per Gaither 2026-05-18: tighter side padding on mobile expanded
       weather. Was padding 6px 30px 6px 8px which left a lot of empty
       space on the right; now symmetrical 6px 4px so the inner content
       has room to breathe edge-to-edge. */
    .weather-bar[data-expanded="true"] { flex-wrap: wrap; gap: 6px;
      width: 100%; padding: 6px 4px; border-radius: 10px;
      background: rgba(255,255,255,0.03);
      border: 1px solid rgba(255,255,255,0.06);
      /* Per Gaither 2026-06-01: center all rows of the wrapped bar.
         Need BOTH justify-content (centers each row's flex items) AND
         margin-left/right auto on the .weather-now block (otherwise the
         current-temp + location share a row that pushes left because
         weather-loc claims the gap on the left edge). */
      justify-content: center;
      align-items: center;
      text-align: center; }
    .weather-bar[data-expanded="true"] .weather-now {
      margin-left: auto;
      margin-right: auto;
    }
    /* Location pill on expanded view: a centered, content-width pill, NOT a
       full-width bar (Gaither 2026-06-23: "weather location is full width when
       weather is expanded"). flex-basis:auto + margin auto centers it; the
       forecast/metrics rows below still take their own 100% rows. */
    .weather-bar[data-expanded="true"] .weather-loc {
      flex: 0 1 auto;
      width: auto;
      max-width: 100%;
      margin: 0 auto;
      justify-content: center;
    }
    /* Per Gaither 2026-05-18: center the 5-day forecast strip on
       mobile expanded view. Without this it flows left-aligned
       with the rest of the weather-bar flex content. */
    .weather-bar[data-expanded="true"] .weather-forecast {
      display: flex !important;
      justify-content: center;
      width: 100%;
      flex-wrap: wrap;
    }
    .weather-bar[data-expanded="true"] .weather-loc-label { display: inline !important; }
    .weather-bar[data-expanded="true"] .weather-day { padding: 4px 6px; min-width: 42px; }
    /* CENTER the 6 metric chips (humidity, feels, wind, rain%,
       sunrise, sunset) in a 2x3 grid on the mobile expanded view.
       Per Gaither 2026-05-17 "on mobile expanded, weather stack
       humidity and those other 5 weather things like it in 2 rows,
       3 columns, centered". The .weather-meta-slot wraps a single
       .weather-meta child; both need the grid + centering or the
       outer flex-bar would left-align the inner grid block. */
    /* Metrics (humidity/wind/feels/rain%/sunrise/sunset) show ONLY on the
       EXPANDED mobile view; hidden when COLLAPSED so the collapsed weather is
       just "Today" + today's temp and fits the header top row next to the logo
       (Gaither 2026-06-23, corrected: I had this backwards). */
    .weather-bar:not([data-expanded="true"]) .weather-meta-slot { display: none !important; }
    .weather-bar[data-expanded="true"] .weather-meta-slot {
      display: flex !important;
      justify-content: center;
      width: 100%;
      padding: 2px 0;
      margin: 2px auto 4px;
      border-right: 0;
    }
    .weather-bar[data-expanded="true"] .weather-meta {
      display: grid !important;
      grid-template-columns: repeat(3, max-content);
      grid-auto-rows: auto;
      gap: 4px 14px;
      justify-content: center;
      justify-items: center;
      align-items: center;
      width: auto;
      padding: 0;
      margin: 0;
      border-right: 0;
    }
    .weather-bar[data-expanded="true"] .weather-meta > span {
      font-size: 11px;
      white-space: nowrap;
    }
    .weather-bar[data-expanded="true"] .weather-meta-slot:empty {
      display: none !important;
    }
  }
  /* Smaller wordmark on mobile + weather pinned top-right next to it
     (Gaither 2026-06-23: "weather should go up to top right and GaitherNews
     logo could be smaller"). margin-left:auto pushes the COLLAPSED weather to
     the right of the brand on the first header row; expanded weather is
     width:100% so it still drops to a full-width panel below. */
  @media (max-width: 720px) {
    .brand-wordmark { font-size: 18px; }
    .brand-tagline { font-size: 8.5px; letter-spacing: 0.02em; }
    header > .weather-bar { margin-left: auto; }
    header > .weather-bar:not([data-expanded="true"]) { align-self: flex-start; }
  }
  /* Degree-circle expand toggle. Mobile-only. Lives top-right of the
     weather bar; visually echoes a degree marker (small circle with
     ° inside). Tap to toggle. */
  .weather-expand-toggle { display: none; position: absolute;
    top: 2px; right: 4px;
    width: 22px; height: 22px; padding: 0;
    background: transparent; border: 0;
    color: var(--fg); cursor: pointer;
    line-height: 1; font-family: var(--sans);
    font-size: 22px; font-weight: 900;
    transition: color .15s, transform .15s; }
  .weather-expand-toggle:hover,
  .weather-expand-toggle:focus-visible {
    color: var(--cyan); outline: none; }
  .weather-expand-toggle .weather-expand-deg { display: inline-block;
    transform: translateY(-2px); }
  .weather-bar[data-expanded="true"] .weather-expand-toggle {
    color: var(--cyan);
    transform: rotate(180deg); }
  @media (max-width: 720px) {
    .weather-expand-toggle { display: inline-flex;
      align-items: center; justify-content: center; }
  }
  /* Tablet + narrow-laptop (721-1499px): show Today + next 2 forecast
     days (3 total) + drop the metrics row. Range extended from
     ≤1180 to ≤1499 on 2026-05-15 — Gaither's MBP 16" (2019) viewport
     fell in the tablet/desktop in-between zone where the full 5-day
     weather was crowding the header. True desktop (≥1500px) keeps
     the full 5-day strip + metrics row. Other 1180-breakpoint rules
     (cats button, etc.) untouched per "leave desktop like it is". */
  /* Tablet (721-1024): hide weather-meta chips and days 4-5. Above
     1024 (macbook + wide desktop) shows the full 5-day forecast. Was
     721-1499 which clipped MacBook 16 retina at default scaling
     (1496px logical) to 3 days. Per Gaither 2026-05-17 "still only
     3 days of weather on my macbook screen". */
  @media (min-width: 721px) and (max-width: 1024px) {
    .weather-meta { display: none; }
    .weather-day:nth-of-type(n+4) { display: none; }
  }
  /* Aggressive collapse: 24/16 -> 4/4 padding, no border, translucent
     bg + 14px blur so the cards behind show through faintly. */
  /* Header-scrolled copy/version moved to the sticky footer per
     Gaither 2026-05-14: belongs at the bottom of the page, not the
     top. See .sticky-stats below for the new home. */
  /* Footer corner row: copyright bottom-left, build-version bottom-right.
     Visible on every screen; tablet+ shows both, narrow mobile drops
     the version (keeps just the copyright). Sits below the main
     footer content. */
  .footer-corner { display: flex; justify-content: space-between;
    align-items: center; gap: 12px;
    margin-top: 18px; padding-top: 14px;
    border-top: 1px solid var(--line);
    font-family: var(--sans); font-size: 11px;
    color: var(--muted); opacity: 0.85;
    letter-spacing: 0.04em; }
  .footer-corner-copy { white-space: nowrap; }
  .footer-corner-version { white-space: nowrap;
    font-variant-numeric: tabular-nums; }
  @media (max-width: 480px) {
    .footer-corner-version { display: none; }
  }
  header.scrolled { padding: 4px 32px;
    background: var(--header-translucent-light, rgba(250,250,247,0.72));
    backdrop-filter: saturate(180%) blur(14px);
    -webkit-backdrop-filter: saturate(180%) blur(14px);
    border-bottom-color: transparent;
    box-shadow: 0 2px 16px rgba(0,0,0,0.06); }
  [data-theme="dark"] header.scrolled {
    background: rgba(15,20,25,0.62);
    box-shadow: 0 2px 16px rgba(0,0,0,0.50); }
  /* Theme-aware scrolled header. The default rule above uses a light
     cream translucent bg which leaves retro and retro80s text
     invisible (retro80s renders white text on near-black, so a cream
     overlay swallows it). Per Gaither 2026-05-15. */
  [data-theme="retro"] header.scrolled {
    background: rgba(244,236,216,0.78);
    box-shadow: 0 2px 16px rgba(80,50,10,0.18); }
  [data-theme="retro80s"] header.scrolled {
    background: rgba(10,4,24,0.72);
    box-shadow: 0 2px 16px rgba(0,0,0,0.55); }
  /* Brand stacks the wordmark and tagline vertically. .brand-tagline
     ("Escape the Algorithm") sits in muted serif italic under the
     wordmark at rest, and collapses out when the header scrolls so
     the compact header stays single-line. */
  .brand { font-family: var(--serif); font-weight: 700;
    letter-spacing: -0.015em; color: var(--fg); text-decoration: none;
    display: inline-flex; flex-direction: column; align-items: flex-start;
    line-height: 1.05;
    transition: font-size .2s ease; }
  .brand-wordmark { font-size: 26px; transition: font-size .2s ease; }
  /* line-height 1.35 + padding-bottom 2px ensures descenders (the 'g'
     in 'Algorithm') don't clip when the tagline is visible. max-height
     bumped to 24px to match the actual rendered box. overflow:hidden
     was tight against the descender ink in the 12px serif italic. */
  .brand-tagline { font-family: var(--serif); font-style: italic;
    font-weight: 500; font-size: 12px;
    line-height: 1.35;
    letter-spacing: 0.02em;
    color: var(--muted);
    margin-top: 2px;
    padding-bottom: 2px;
    transition: opacity .2s ease, max-height .2s ease, margin-top .2s ease, padding-bottom .2s ease;
    opacity: 1; max-height: 24px; overflow: hidden; }
  header.scrolled .brand-wordmark { font-size: 16px; }
  header.scrolled .brand-tagline { opacity: 0; max-height: 0;
    margin-top: 0; padding-bottom: 0; }
  /* Three-line Updated stack per the screenshots Gaither attached
     2026-05-15:
       Line 1: 'Updated'              (small, muted)
       Line 2: 'May 14, 12:03 PM'     (slightly larger, fg color)
       Line 3: 'Next refresh in 3h 5m' (small, muted)  ← countdown
     Scrolled state hides line 3 and tightens the font, keeping
     'Updated' + date visible. */
  .brand:hover { text-decoration: none; }
  .brand .cyan { color: var(--cyan); font-style: italic; }
  /* Restored to the format that lived longest (commit 4fbadcb~1 era)
     per Gaither 2026-05-15: all 3 rows use the .updated 13px base,
     with next-refresh dropping to 11px+muted. gap 2px, line-height
     1.2 — old proportions. */
  .updated { font-size: 11px; color: var(--muted); margin-left: auto;
    display: inline-flex; flex-direction: column; align-items: flex-end;
    gap: 1px; line-height: 1.15;
    /* Pin to the top of the header. Without this it default-aligns to
       "stretch" and could drop to the second wrap row at certain
       widths. Higher flex order + explicit alignment keeps it pinned
       at the top-right at every desktop width including MBP 16 2019
       retina (~1792px). Per Gaither 2026-05-16.
       Font sizes dropped 13->11 / 11->10 per Gaither 2026-05-17
       "Make Updated area text smaller". */
    align-self: flex-start;
    flex-shrink: 0;
    order: 100; }
  .updated .updated-label { font-size: 11px; color: var(--muted); }
  .updated .updated-date { font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
  .next-refresh { font-size: 10px; color: var(--muted); opacity: 0.75; }
  .next-refresh:empty { display: none; }
  .stale-note { max-width: 720px; margin: 10px auto 0; padding: 8px 14px;
    border: 1px solid var(--line); border-radius: 8px; color: var(--muted);
    font-size: 13px; text-align: center; }
  header.scrolled .updated { font-size: 10px; }
  header.scrolled .updated .updated-label,
  header.scrolled .updated .updated-date { font-size: 10px; }
  header.scrolled .updated .next-refresh { display: none; }
  .hamburger { display: none; background: transparent; border: 1px solid var(--line);
    color: var(--fg); width: 36px; height: 36px; border-radius: 8px;
    cursor: pointer; align-items: center; justify-content: center;
    padding: 0; margin-left: auto; }
  .hamburger:hover { border-color: var(--cyan); background: var(--cyan-soft); }
  .hamburger svg { width: 18px; height: 18px; }
  .header-controls { display: contents; }
  /* Keep font-size + help + theme bound together so they always wrap
     as a unit (or stay on the same row when there's room). On tablet
     before this rule, each icon was a flex child of header and could
     wrap independently — leaving font-size dangling on its own row
     while help + theme sat further down. */
  .header-icon-row { display: inline-flex; align-items: center; gap: 8px; }
  /* ======================================================================
     CANONICAL HEADER ICON ORDER (Gaither 2026-05-16, re-confirmed 2026-05-17
     "Ensure consistency of icon order across different views"):
       1. Categories filter (mobile/tablet entry; hidden on desktop)
       2. Hide read (newspaper pill — collapses to icon-only at scroll)
       3. Sort mode (category / sources / random / saved)
       4. View mode (default / horizontal / grid + short variants)
       5. Theme (palette)
       6. Font size (AA)
       7. Keyboard shortcuts (hidden on mobile)
       8. Support (Buy Me a Coffee pill — collapses to icon at scroll)

     Implementation: flex-order on each child. DOM stays in render order
     so screen readers + Tab key follow visual order. The order numbers
     use a gap of 10 (rather than 1) so we can insert a new icon at any
     point without renumbering everything else.

     IMPORTANT: never re-order these inside an @media block. Hide/show
     individual icons is fine; reordering breaks user muscle memory. ====== */
  .header-icon-row { flex-direction: row; }
  .header-icon-row > #catsHeaderBtn      { order: 10; }
  .header-icon-row > #hideViewedToggle   { order: 20; }
  .header-icon-row > #sortModeBtn        { order: 30; }
  .header-icon-row > #viewModeBtn        { order: 40; }
  .header-icon-row > #themeToggle        { order: 50; }
  .header-icon-row > #fontSizeToggle     { order: 60; }
  .header-icon-row > #helpBtn            { order: 70; }
  .header-icon-row > .bmc-header         { order: 80; }
  /* Any unenumerated icon falls back to order:0 which puts it FIRST.
     This catches accidental insertions during development — they'll
     visually pop to the leftmost position so the mistake is obvious. */
  /* viewModeBtn must be present on every breakpoint (mobile + tablet +
     desktop). Per Gaither 2026-05-16: "missing change view button on
     mobile and tablet". The flex-order rule above only handles order,
     not visibility; this guard ensures no @media-scoped display:none
     can hide it. */
  #viewModeBtn { display: inline-flex !important; }
  /* Tablet (721-1179px): icon row sits naturally to the right of the
     weather widget. Earlier version centered it with margin-left/right
     auto, but the weather-bar above ALSO uses auto margins to center
     itself — two elements both demanding the visual center fought for
     space and produced the "weird" header layout the user flagged
     2026-05-15. With this rule removed, the layout matches desktop:
     brand left, weather centered, icons natural-flow to the right. */
  @media (min-width: 721px) and (max-width: 1179px) {
    header > .header-icon-row {
      margin-left: 0;
      margin-right: 0;
      justify-content: flex-end;
    }
  }
  /* Narrow desktop (1180px+): icon row anchored to right, Updated
     follows AFTER icons at the far right edge. Per Gaither
     2026-05-15: push Updated info to far right of icon group,
     similar to tablet. We do this with CSS order so the DOM stays
     unchanged: icons get a low order and absorb left space via
     margin-left:auto; updated block gets a higher order so it
     lands after icons in the flex sequence. */
  /* (Removed 2026-05-31: the desktop-wide "header padding-right: 190px
     reserve + Updated absolute top-right" rules. Updated now lives inline
     on row 2 next to icons via the @media (min-width: 1180px) and
     (max-width: 1919px) and @media (min-width: 1920px) grid placements.
     Keeping a no-op block for the scrolled top:8 override is unnecessary —
     position: static via the grid placement applies in both states.) */
  /* Mobile icon-drawer chevron toggle: ONLY ever visible at mobile
     viewport + scrolled state (see @media (max-width: 720px) below for
     the override). Default to display:none everywhere else so the
     chevron button never leaks into desktop/tablet header layout. Per
     Gaither 2026-05-17 ("header is ALL jacked up" — the toggle was
     showing on desktop because every rule scoping it was inside the
     mobile media block). */
  .mobile-icon-drawer-toggle { display: none; }
  /* Header categories button — primary entry point on mobile/tablet.
     Hidden on desktop where the left rail is always visible. Selector
     specificity raised to .header-icon-row .cats-header-btn (0,2,0) so
     it beats .icon-btn { display: inline-flex } which sits later in
     the stylesheet. Without this the apps-grid icon leaked onto the
     desktop header — Gaither flagged 2026-05-13. */
  .header-icon-row .cats-header-btn { display: inline-flex; }
  /* "Show categories" / "Hide categories" inline label. Matches the
     visual weight of the adjacent "Hide read" + "Support" inline
     labels (same font-size 11.5px, normal weight, no uppercase) so
     the trio reads as a consistent row instead of "Show categories"
     dominating. */
  .cats-header-label { display: none; }
  @media (min-width: 721px) and (max-width: 1180px) {
    .header-icon-row .cats-header-btn { width: auto; padding: 0 10px;
      gap: 6px; border-radius: 999px; }
    .cats-header-label { display: inline-block;
      font-family: var(--sans); font-size: 11.5px; font-weight: 500;
      letter-spacing: 0; text-transform: none;
      color: inherit; }
  }
  /* On scroll, the header tightens — drop the label so the icon
     stays compact and other elements have room. */
  header.scrolled .cats-header-label { display: none; }
  /* (Reverted 2026-05-17: the tablet-only absolute-positioned Updated
     block + padding-right:200 was causing visible overlap with the
     icon row at narrow tablet widths and breaking the expanded
     layout. Tablet falls back to natural flex-flow Updated like the
     pre-fix state. If the top-right pin is desired again, revisit
     after sorting out width math at 721/900/1024.) */
  @media (min-width: 1180px) {
    .header-icon-row .cats-header-btn { display: none; }
  }
  .kind-tabs--header { margin: 0; }
  /* Dark theme: lift the icon-button border off the dark card surface.
     Default --line at #2a3138 was barely visible on tablet against the
     midnight background; --line-strong sits a bit brighter (#3d454e). */
  [data-theme="dark"] .icon-btn,
  [data-theme="dark"] .theme-toggle { border-color: var(--line-strong); }
  [data-theme="dark"] .icon-btn:hover,
  [data-theme="dark"] .theme-toggle:hover { border-color: var(--cyan); }
  /* Tablet (721-1024) EXPANDED: TWO ROWS ONLY.
       Row 1: brand | tabs | weather-bar (centered)
       Row 2: icon-row (centered) + Updated (right edge), SAME ROW
     Per Gaither 2026-05-31 "move Updated to the exact same row as
     the icons on the lower row. only two rows". Updated leaves its
     old absolute top-right slot and sits inline alongside the icons. */
  @media (min-width: 721px) and (max-width: 1024px) {
    header {
      display: grid;
      grid-template-columns: auto auto 1fr;
      grid-template-rows: auto auto;
      column-gap: 16px;
      row-gap: 6px;
      align-items: center;
      padding-top: 10px;
      padding-bottom: 14px;
    }
    header > .brand             { grid-column: 1; grid-row: 1; }
    header > .kind-tabs--header { grid-column: 2; grid-row: 1; }
    header > .weather-bar       { grid-column: 3; grid-row: 1; justify-self: center; margin: 0; }
    /* header-controls wraps Updated; display:contents lets its child
       become a direct grid item so we can place Updated in row 2. */
    header > .header-controls   { display: contents; }
    /* Both icon-row AND updated occupy row 2, spanning the full width.
       Icon-row centers itself; Updated pins to the right edge. They
       share the same row-track so the header is exactly two rows. */
    header > .header-icon-row {
      grid-column: 1 / -1;
      grid-row: 2;
      justify-self: center;
      align-self: center;
      margin: 0;
    }
    header .updated {
      position: static;     /* override the legacy absolute */
      top: auto; right: auto;
      grid-column: 1 / -1;
      grid-row: 2;
      justify-self: end;
      align-self: center;
      max-width: 150px;
      margin: 0;
      font-size: 10px;
      line-height: 1.15;
      text-align: right;
      z-index: auto;
      pointer-events: auto;
    }
    header .updated .updated-label,
    header .updated .updated-date { font-size: 10px; }
    header .updated .next-refresh { display: block; font-size: 9.5px; }
    header > .hamburger,
    header > .mobile-icon-drawer-toggle { display: none; }
  }
  /* Tablet scroll-collapsed header: tighter vertical padding so the
     compressed row reads as a thin strip rather than a chunky band. */
  @media (min-width: 721px) and (max-width: 1024px) {
    /* Move scrolled-state left items DOWN a hair via increased
       top padding. Updated stays at top:4 (absolute) so it doesn't
       move. Per Gaither 2026-05-17 "on shrunk tablet header, move
       top left stuff down a little, except 'Updated' area, it's
       fine". Was 2px top. */
    header.scrolled { padding: 10px 24px 4px; gap: 12px; }
    header.scrolled .icon-btn,
    header.scrolled .theme-toggle { width: 32px; height: 32px; }
    header.scrolled .icon-btn svg,
    header.scrolled .theme-toggle svg { width: 16px; height: 16px; }
    header.scrolled .kind-tab { padding: 4px 12px; }
    /* Center the icons in scrolled state. Per Gaither 2026-05-17
       "center lower icons on tablet in shrunk mode". */
    header.scrolled > .header-icon-row {
      margin-left: auto;
      margin-right: auto;
      justify-content: center;
    }
    /* Tablet scrolled: weather sits CENTER-RIGHT (toward the right end
       of the header). Grid col 3 is 1fr, so justify-self: end pushes
       it to the right edge of that cell, which visually reads as
       "center-right" with brand+tabs on the left. Per Gaither
       2026-05-31 "center more towards right". */
    header.scrolled > .weather-bar {
      margin-left: auto;
      margin-right: 12px;
      justify-self: end;
    }
    /* Scrolled-state tablet: keep the SAME two-row grid (Updated inline
       on row 2 with icons), just tightened. Per Gaither 2026-05-31
       "only two rows". */
    header.scrolled .updated {
      position: static;
      top: auto; right: auto;
      grid-column: 1 / -1;
      grid-row: 2;
      justify-self: end;
      align-self: center;
      max-width: 140px;
      margin: 0;
      font-size: 9.5px;
      line-height: 1.15;
      text-align: right;
    }
    header.scrolled .updated .updated-label,
    header.scrolled .updated .updated-date { font-size: 9.5px; }
    header.scrolled .updated .next-refresh { display: none; }
  }
  @media (max-width: 720px) {
    /* 2026-05-17 mobile header layout:
        Expanded (top of page):
          Row 1: brand (left)  +  weather-bar (top-right, pinned)
          Row 2: News/Research kind tabs (centered) + icon-row (centered)
        Scrolled:
          Row 1: brand (left) + weather-bar (top-right) + chevron drawer
                 toggle (replaces icon row). Icon row hides; tap chevron
                 to reveal it as an overlay drawer below the header.
       Subtle shadow under the header so it feels lifted above the
       content when sticky. */
    header { padding: 16px 18px; gap: 8px; flex-wrap: wrap;
      box-shadow: 0 2px 6px rgba(0,0,0,0.10);
      /* Force a solid bg layer so the iOS URL bar collapse/expand
         doesn't briefly paint a dark gap behind the header. Was visible
         when tapping between News and Research. */
      background: var(--bg) !important; }
    .hamburger { display: none; }
    .header-controls { display: none; }
    #helpBtn { display: none; }
    /* Weather-bar pinned to top-right of the header at all mobile widths
       and scroll states. margin-left:auto pushes it to the right of the
       brand on the first wrap-row. Per Gaither 2026-05-17. */
    header > .weather-bar {
      margin-left: auto;
      margin-right: 0;
      order: 2;
      align-self: flex-start;
    }
    header.scrolled > .weather-bar {
      margin-left: auto;
      margin-right: 0;
    }
    /* Row 2 at expanded state: News/Research tabs centered, icon row
       centered (matches prior layout). */
    .kind-tabs--header,
    header > .header-icon-row { order: 3; margin-top: 4px; }
    .kind-tabs--header { align-self: center; margin: 0 auto;
      justify-content: center; }
    header > .header-icon-row { display: inline-flex; gap: 6px;
      margin: 0 auto;
      /* Wrap so the labeled pills in the expanded header flow onto extra
         centered rows instead of overflowing off-screen (Gaither 2026-06-23). */
      flex-wrap: wrap;
      align-self: center; justify-content: center; }
    /* Slightly larger icons on mobile so they're comfortable tap
       targets. Per Gaither 2026-05-17 "make lower icon row icons
       slightly larger on mobile". Was 32 (forced down to fit 3 in
       one row); bumped to 36 since we're already wrapping the row
       onto row 2 anyway. */
    header > .header-icon-row .icon-btn,
    header > .header-icon-row .theme-toggle { width: 36px; height: 36px; }
    header > .header-icon-row .icon-btn svg,
    header > .header-icon-row .theme-toggle svg { width: 18px; height: 18px; }
    header > .header-icon-row .bmc-header { padding: 6px 12px; font-size: 12.5px; }

    /* Expanded header (and the open scrolled-state drawer): label every
       icon-row button so the controls are self-explanatory (Gaither
       2026-06-23: "on mobile expanded header view, put text on the buttons
       so people know what they do"). Buttons become labeled pills and wrap
       across the centered row. Collapsed (scrolled, drawer closed) stays
       icon-only behind the chevron. */
    header:not(.scrolled) > .header-icon-row .icon-btn,
    header:not(.scrolled) > .header-icon-row .theme-toggle,
    body[data-icons-open="true"] header.scrolled > .header-icon-row .icon-btn,
    body[data-icons-open="true"] header.scrolled > .header-icon-row .theme-toggle {
      width: auto; min-width: 36px; border-radius: 999px;
      padding: 0 13px; gap: 6px;
    }
    header:not(.scrolled) > .header-icon-row .icon-label,
    header:not(.scrolled) > .header-icon-row .cats-header-label,
    header:not(.scrolled) > .header-icon-row .hide-viewed-label,
    body[data-icons-open="true"] header.scrolled > .header-icon-row .icon-label,
    body[data-icons-open="true"] header.scrolled > .header-icon-row .cats-header-label,
    body[data-icons-open="true"] header.scrolled > .header-icon-row .hide-viewed-label {
      display: inline-block !important;
      font-size: 11.5px; font-weight: 500; white-space: nowrap;
    }
    /* Hide-read button: widen from the forced 32px circle back to a labeled
       pill in the expanded / open-drawer state. */
    header:not(.scrolled) > .header-icon-row .hide-viewed-pill,
    body[data-icons-open="true"] header.scrolled > .header-icon-row .hide-viewed-pill {
      width: auto !important; max-width: none;
      border-radius: 999px !important; padding: 0 13px 0 9px !important;
    }

    /* --- Mobile icon-drawer (Task A, Gaither 2026-05-17) ---
       When the header collapses (scrolled state) on mobile, the icon
       row tucks behind a chevron drawer toggle. Tap the chevron to
       reveal the icons as an overlay below the header; tap again or
       scroll to top to collapse. */
    .mobile-icon-drawer-toggle { display: none; }
    header.scrolled > .header-icon-row {
      display: none;
    }
    header.scrolled > .mobile-icon-drawer-toggle {
      display: inline-flex;
      align-items: center; justify-content: center;
      width: 36px; height: 36px;
      padding: 0;
      background: transparent;
      border: 1px solid var(--line);
      border-radius: 50%;
      color: var(--fg);
      cursor: pointer;
      order: 4;
      margin-left: auto;
      margin-right: 0;
      transition: border-color .15s, transform .2s ease;
    }
    [data-theme="dark"] header.scrolled > .mobile-icon-drawer-toggle {
      border-color: var(--line-strong);
    }
    header.scrolled > .mobile-icon-drawer-toggle:hover {
      border-color: var(--cyan); color: var(--cyan);
    }
    header.scrolled > .mobile-icon-drawer-toggle svg {
      width: 18px; height: 18px;
      transition: transform .2s ease;
    }
    /* When the drawer is open: rotate the chevron 180deg and re-render
       the icon row as an overlay drawer below the header. position:
       absolute relative to the (position: sticky) header so the drawer
       hangs underneath without forcing a header reflow. */
    body[data-icons-open="true"] header.scrolled > .mobile-icon-drawer-toggle svg {
      transform: rotate(180deg);
    }
    body[data-icons-open="true"] header.scrolled > .mobile-icon-drawer-toggle {
      border-color: var(--cyan); color: var(--cyan);
    }
    body[data-icons-open="true"] header.scrolled > .header-icon-row {
      display: inline-flex;
      position: absolute;
      top: 100%; left: 0; right: 0;
      margin: 0;
      padding: 10px 16px;
      gap: 8px;
      background: var(--bg);
      border-top: 1px solid var(--line);
      box-shadow: 0 4px 10px rgba(0,0,0,0.12);
      justify-content: center;
      flex-wrap: wrap;
      z-index: 49;
    }
  }
  /* Tablet (721-1024): same subtle shadow under sticky header. */
  @media (min-width: 721px) and (max-width: 1024px) {
    header { box-shadow: 0 2px 6px rgba(0,0,0,0.08); }
    /* Location chooser collapses to just the pin icon on tablet (Gaither
       2026-06-23: "remove location name ... make it just the location
       icon"). The icon still opens the zip-code prompt. */
    .weather-loc .weather-loc-label { display: none; }
    .weather-loc { padding: 3px 6px; }
  }

  /* Header Hide-viewed toggle pill. Lives where the search input used
     to be. Compact pill button with the same iOS-style switch graphic
     as the left-rail toggles, but inline-friendly. */
  .header-toggle { display: inline-flex; align-items: center; gap: 10px;
    background: var(--subtle); border: 1px solid var(--line);
    border-radius: 999px; padding: 4px 14px 4px 8px; cursor: pointer;
    font-family: inherit; font-size: 13px; font-weight: 500; color: var(--fg);
    transition: background .15s, border-color .15s, color .15s; }
  .header-toggle:hover { border-color: var(--cyan); }
  .header-toggle-switch { width: 28px; height: 16px; border-radius: 999px;
    background: rgba(0,0,0,0.22);
    box-shadow: inset 0 1px 2px rgba(0,0,0,0.12);
    position: relative; flex-shrink: 0;
    transition: background .22s cubic-bezier(.32,.72,.45,1.16); }
  .header-toggle-switch::after { content: ""; position: absolute;
    left: 2px; top: 50%; width: 12px; height: 12px; border-radius: 50%;
    background: #ffffff;
    box-shadow: 0 1px 2px rgba(0,0,0,0.18);
    transform: translateY(-50%);
    transition: left .22s cubic-bezier(.32,.72,.45,1.16); }
  .header-toggle[data-active="true"] .header-toggle-switch { background: var(--cyan); }
  .header-toggle[data-active="true"] .header-toggle-switch::after { left: 14px; }
  .header-toggle[data-active="true"] { color: var(--cyan); border-color: var(--cyan); }
  [data-theme="dark"] .header-toggle-switch { background: rgba(255,255,255,0.16); }
  [data-theme="dark"] .header-toggle[data-active="true"] .header-toggle-switch { background: var(--cyan); }
  .icon-btn { background: transparent; border: 1px solid var(--line);
    color: var(--fg); width: 36px; height: 36px; border-radius: 50%;
    cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
    padding: 0; transition: border-color .15s, background .15s; }
  .icon-btn:hover { border-color: var(--cyan); background: var(--cyan-soft); }
  .icon-btn svg { width: 18px; height: 18px; }
  /* Hide-read newspaper icon: open newspaper when OFF (showing all),
     newspaper with strikethrough when ON (hiding read articles). */
  .hide-viewed-icon .icon-news-closed { display: none; }
  .hide-viewed-icon[data-active="true"] .icon-news-open { display: none; }
  .hide-viewed-icon[data-active="true"] .icon-news-closed { display: inline-block; }
  .hide-viewed-icon[data-active="true"] {
    color: var(--cyan); border-color: var(--cyan); background: var(--cyan-soft);
  }
  /* Pill mode: shows newspaper icon + 'Hide read' label initially.
     On scroll the label collapses to icon-only via body.scrolled-page. */
  .hide-viewed-pill {
    width: auto !important;
    padding: 0 12px 0 9px;
    gap: 8px;
    border-radius: 999px !important;
    font-size: 12.5px;
    font-weight: 500;
    letter-spacing: 0.01em;
    white-space: nowrap;
    /* Bumped from 110px so 'Hide read articles' / 'Show all articles'
       fit alongside the icon without clipping. Per Gaither 2026-05-14. */
    max-width: 180px;
  }
  .hide-viewed-pill .hide-viewed-label { display: inline; white-space: nowrap; }
  body.scrolled-page .hide-viewed-pill {
    width: 36px !important; padding: 0; border-radius: 50% !important;
    max-width: 36px;
  }
  body.scrolled-page .hide-viewed-pill .hide-viewed-label { display: none; }
  /* On mobile the icon row gets narrower; drop the label proactively. */
  @media (max-width: 720px) {
    .hide-viewed-pill { width: 32px !important; padding: 0; border-radius: 50% !important; }
    .hide-viewed-pill .hide-viewed-label { display: none; }
  }
  /* Bottom-fixed categories drawer toggle (mobile/tablet only). Pill
     with apps-grid icon + label + chevron. Larger + cyan-tinted so
     it's clearly visible against any card image behind it. */
  .cats-drawer-toggle {
    position: fixed; bottom: 22px; left: 50%;
    transform: translateX(-50%);
    display: inline-flex; align-items: center; gap: 10px;
    padding: 12px 20px;
    background: var(--cyan); color: #fff;
    border: 0; border-radius: 999px;
    font-family: inherit; font-size: 14px; font-weight: 600;
    letter-spacing: 0.01em;
    box-shadow: 0 4px 16px rgba(0,0,0,0.32);
    cursor: pointer;
    z-index: 80;
    transition: padding .2s ease, background .2s ease, gap .2s ease;
  }
  .cats-drawer-toggle:hover { background: var(--cyan); filter: brightness(1.05); }
  .cats-drawer-toggle svg { width: 18px; height: 18px; flex-shrink: 0; }
  .cats-drawer-toggle .cats-drawer-chev { width: 16px; height: 16px;
    transition: transform .15s ease; }
  .cats-drawer-toggle[data-active="true"] .cats-drawer-chev {
    transform: rotate(180deg);
  }
  /* Bottom-fixed drawer chevron is now redundant — the apps-grid
     icon in the header opens/closes the drawer, and an in-drawer
     close chevron lives at the bottom of the drawer itself. Gaither
     2026-05-13 — kept the markup so old handlers still bind, but
     suppressed visually everywhere. */
  .cats-drawer-toggle { display: none !important; }

  /* ---- Back to top ----
     button.back-to-top (specificity 0,1,1) beats [data-tooltip]
     { position: relative } — without this tag the button collapses
     into the document flow and disappears from view. */
  button.back-to-top { position: fixed; bottom: 24px; right: 24px;
    width: 44px; height: 44px; border-radius: 50%;
    background: var(--card-bg); color: var(--cyan);
    border: 2px solid var(--cyan); cursor: pointer;
    display: flex; align-items: center; justify-content: center;
    box-shadow: 0 2px 8px rgba(0,0,0,0.12);
    opacity: 0; pointer-events: none;
    transition: opacity .2s ease, transform .15s ease;
    z-index: 60; padding: 0; }
  button.back-to-top.is-visible { opacity: 1; pointer-events: auto; }
  button.back-to-top:hover { transform: translateY(-2px); }
  button.back-to-top svg { width: 20px; height: 20px; }

  /* ---- Load more ---- */
  .load-more-row { display: flex; justify-content: center; margin-top: 32px; }
  .load-more { background: transparent; border: 1px solid var(--line);
    color: var(--fg); padding: 12px 28px; border-radius: 999px;
    font-family: inherit; font-size: 14px; font-weight: 500;
    cursor: pointer; transition: border-color .15s, background .15s, color .15s; }
  .load-more:hover { border-color: var(--cyan); color: var(--cyan); background: var(--cyan-soft); }
  .load-more[disabled] { opacity: 0.5; cursor: default; }
  /* Kind-tabs pill height-matched to .icon-btn (36px). Same outer
     dimension means the tabs row visually aligns with the icon row
     instead of one sitting slightly higher or lower. */
  .kind-tabs { display: inline-flex; gap: 2px; padding: 3px;
    background: var(--subtle); border-radius: 999px;
    height: 36px; align-items: center; box-sizing: border-box; }
  .kind-tab { padding: 5px 12px; border-radius: 999px;
    font-size: 13px; font-weight: 500; color: var(--muted);
    text-decoration: none; transition: background .15s, color .15s;
    display: inline-flex; align-items: center; gap: 6px; line-height: 1; }
  .kind-tab:hover { color: var(--fg); text-decoration: none; }
  .kind-tab[data-active="true"] { background: var(--cyan-soft); color: var(--cyan);
    box-shadow: 0 1px 2px rgba(26,26,26,0.06); font-weight: 600; }
  [data-theme="dark"] .kind-tab[data-active="true"] {
    box-shadow: 0 1px 2px rgba(0,0,0,0.4); }
  /* Icons always visible alongside the text label on every width.
     Per Gaither 2026-05-12 — icons next to "News" / "Research" on
     desktop AND tablet at all times; mobile and tablet then drop the
     label on scroll-collapse so the icon-only pill reclaims room while
     reading. */
  .kind-tab-icon { display: inline-flex; align-items: center; }
  .kind-tab-icon svg { width: 16px; height: 16px; display: block; }
  .kind-tab-label { display: inline; }
  /* Scroll-collapse: drop the kind-tab text label at all widths when
     the header is scrolled. Per Gaither 2026-05-16: "macbook 16 retina
     2019 screen hide the text from the News / Research button and make
     sure icons move up to header on scroll / shrink." Icons stay
     visible at every breakpoint so the active tab indicator (cyan-soft
     pill) keeps reading correctly. */
  /* Tablet (721-1024): SLIGHTLY smaller kind-tabs to claim less
     header width per Gaither 2026-05-17 "News Research toggle a
     little smaller on tablet". Affects expanded + scrolled. */
  @media (min-width: 721px) and (max-width: 1024px) {
    .kind-tabs { height: 30px; padding: 2px; gap: 2px; }
    .kind-tab  { padding: 4px 10px; font-size: 12px; gap: 4px; }
    .kind-tab .kind-tab-icon svg { width: 14px; height: 14px; }
    /* KEEP the News/Research labels even when scrolled — per Gaither
       2026-05-31 "expand News/Research text" on collapsed header. */
    header.scrolled .kind-tab { padding: 4px 10px; }
    header.scrolled .kind-tab-label { display: inline; font-size: 12px; }
  }
  /* Mobile (<=720): KEEP the News/Research text label at scrolled.
     Per Gaither 2026-05-17: "on mobile header shrunk add News /
     Research text to toggle". Small viewport readers want clear
     labels for the section switcher, not just bare icons. */
  @media (max-width: 720px) {
    header.scrolled .kind-tab { padding: 4px 10px; }
    header.scrolled .kind-tab-label {
      display: inline;
      font-size: 12px;
    }
  }
  /* Desktop / MBP 16 retina (>1024px): drop the label on scroll
     so the compressed header strip has room for the icon row + Updated
     + weather without wrapping. */
  @media (min-width: 1025px) {
    /* Keep News/Research labels even on scroll, per Gaither 2026-05-31. */
    header.scrolled .kind-tab { padding: 5px 12px; }
    header.scrolled .kind-tab-label { display: inline; font-size: 12px; }
  }
  /* Earlier rule hid .kind-tab-label permanently on widths ≤1920px.
     Per Gaither 2026-05-16 follow-up: "News / Research can have text
     on Expanded header, before scroll and shrink." Labels return at
     the expanded (pre-scroll) state on every desktop width; the
     scrolled-state hide above still kicks in once the header shrinks. */
  .theme-toggle { background: transparent; border: 1px solid var(--line);
    color: var(--fg); width: 36px; height: 36px; border-radius: 50%;
    cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
    padding: 0; transition: border-color .15s, background .15s; }
  .theme-toggle:hover { border-color: var(--cyan); background: var(--cyan-soft); }
  .theme-toggle svg { width: 18px; height: 18px; }
  .theme-toggle .icon-pref { display: none; }
  html[data-theme-pref="light"] .theme-toggle .icon-pref-light { display: inline; }
  html[data-theme-pref="dark"] .theme-toggle .icon-pref-dark { display: inline; }
  html[data-theme-pref="auto"] .theme-toggle .icon-pref-auto { display: inline; }
  html[data-theme-pref="colorblind"] .theme-toggle .icon-pref-colorblind { display: inline; }
  html[data-theme-pref="retro"] .theme-toggle .icon-pref-retro { display: inline; }
  html[data-theme-pref="retro80s"] .theme-toggle .icon-pref-retro80s { display: inline; }
  /* Sort-mode cycle button. Only the icon matching data-mode shows. */
  .sort-btn .sort-icon { display: none; }
  .sort-btn[data-mode="category"] .sort-icon-category { display: inline; }
  .sort-btn[data-mode="sources"]  .sort-icon-sources  { display: inline; }
  .sort-btn[data-mode="random"]   .sort-icon-random   { display: inline; }
  .sort-btn[data-mode="saved"]    .sort-icon-saved    { display: inline; color: #e94e7b; }
  .sort-btn[data-mode="saved"] { border-color: #e94e7b; }
  /* Cards filtered out by the sort-btn (saved mode) hide entirely. */
  .grid > .card[data-sort-hidden="true"] { display: none; }
  .rail-list > .rail-item[data-sort-hidden="true"] { display: none; }
  /* Font-size cycle. data-font-size on <html> drives explicit overrides
     on the reader-visible text (headlines, summaries, L/R panels, source
     pills). Setting html { font-size } alone wouldn't do anything
     because the cards use px values throughout — Gaither correctly
     flagged that the button "did nothing" before. Three steps to stay
     simple: normal / large / xlarge. */
  html[data-font-size="large"]  .card .headline { font-size: 27px; }
  html[data-font-size="large"]  .card .expanded { font-size: 16.5px; }
  html[data-font-size="large"]  .card .meta     { font-size: 13px; }
  html[data-font-size="large"]  .lr-col p,
  html[data-font-size="large"]  .lr-col .lr-body,
  html[data-font-size="large"]  .prior-coverage-inline,
  html[data-font-size="large"]  .read-more-box { font-size: 16px; }
  html[data-font-size="large"]  .rail-headline { font-size: 14.5px; }
  html[data-font-size="xlarge"] .card .headline { font-size: 30px; }
  html[data-font-size="xlarge"] .card .expanded { font-size: 18px; }
  html[data-font-size="xlarge"] .card .meta     { font-size: 14px; }
  html[data-font-size="xlarge"] .lr-col p,
  html[data-font-size="xlarge"] .lr-col .lr-body,
  html[data-font-size="xlarge"] .prior-coverage-inline,
  html[data-font-size="xlarge"] .read-more-box { font-size: 17.5px; }
  html[data-font-size="xlarge"] .rail-headline { font-size: 15.5px; }
  /* Font-size icon constrained to the 36px button so it doesn't
     visually balloon past the circle border the way other header
     icons (theme, random, keyboard) stay inside. Per Gaither feedback
     2026-05-12 — icons should look aligned in size + frame. */
  .font-size-toggle svg { width: 22px; height: 22px; overflow: hidden; }
  /* Three A glyphs at different sizes; CSS picks which one shows based
     on the current html[data-font-size] state. Default (no attribute)
     shows the smallest A. */
  .font-size-toggle .fs-a-normal { display: inline; }
  .font-size-toggle .fs-a-large,
  .font-size-toggle .fs-a-xlarge { display: none; }
  html[data-font-size="large"]  .font-size-toggle .fs-a-normal { display: none; }
  html[data-font-size="large"]  .font-size-toggle .fs-a-large  { display: inline; }
  html[data-font-size="xlarge"] .font-size-toggle .fs-a-normal { display: none; }
  html[data-font-size="xlarge"] .font-size-toggle .fs-a-xlarge { display: inline; }
  /* No active-state glow on the font sizer — it looked like a button
     that was stuck in a "pressed" state next to the plain help/theme
     icons. The font itself growing on the page is the only feedback
     the user needs that the cycle worked. */

  /* ---- Desktop side rails ----
     [filters rail | main | opposite-kind rail] grid on wide viewports.
     - Left rail (.rail-filters) holds the category chips, sticky so
       they're always reachable while scrolling the main feed.
     - Right rail (.rail-opposite) shows the OPPOSITE-kind recent items
       (news on research page, research on news page), also sticky.
     - Mobile (<1180px) gets the original horizontal filter strip at the
       top of the page and no side rails. */
  .layout { display: block; }
  .rail { display: none; }
  /* Floating chevron rail-toggle is desktop-only. The mobile layout has
     no left rail to toggle. Without this base rule the button defaulted
     to <button> inline-block on mobile and painted as a small white
     square below the filter strip. */
  .rail-toggle-tab { display: none; }
  .right-rail-toggle-tab { display: none; }
  /* Higher-specificity selectors (.filters.filters--*) so we override
     the base .filters { display: flex } rule defined later in the file
     regardless of source order. */
  .filters.filters--desktop { display: none; }
  @media (min-width: 1180px) {
    /* padding-top: 18px so the first card has breathing room below
       the header / filter strip instead of sitting flush against it. */
    .layout { display: grid; grid-template-columns: 220px minmax(0, 1fr) 260px;
      gap: 28px; padding: 18px 28px 0; align-items: start; }
    /* Main max-width tuned for the desktop grid layout: 1100px gives
       breathing room around the column while still being prominent.
       width:100% forces main to fill its grid column (capped by
       max-width). */
    .layout > main { width: 100%; min-width: 0; padding: 0; max-width: 1100px; margin: 0 auto; }
    .filters.filters--mobile { display: none; }
    /* Floating chevron toggle for the categories rail.
       Selector raised to .layout > .rail-toggle-tab (specificity 0,1,1)
       so it BEATS the later [data-tooltip] { position: relative } rule
       which otherwise overrode position:fixed, collapsing the button
       back into grid auto-placement and stealing column 2 from main.
       That bug presented as: middle column empty, main rendering inside
       the 260px right-rail column. */
    .layout > .rail-toggle-tab { display: inline-flex;
      position: fixed; top: 50%; transform: translateY(-50%);
      left: 248px; z-index: 60;
      width: 32px; height: 32px;
      background: transparent; color: var(--fg);
      border: 0; border-radius: 0;
      cursor: pointer; padding: 0;
      align-items: center; justify-content: center;
      box-shadow: none;
      opacity: 0.55;
      transition: opacity .15s, color .15s, left .22s ease; }
    .layout > .rail-toggle-tab:hover { opacity: 1; color: var(--cyan); }
    .rail-toggle-chevron { width: 28px; height: 28px;
      transition: transform .25s ease; }
    /* When the rail is collapsed, dock the chevron at the LEFT edge
       of the screen instead of leaving it floating where the rail
       used to be. */
    .layout > .rail-toggle-tab[data-state="hidden"] { left: 8px; }
    .layout > .rail-toggle-tab[data-state="hidden"] .rail-toggle-chevron {
      transform: rotate(180deg); }
    /* When collapsed:
       - Collapse the rail's grid column to 0 width so the middle
         column absorbs the freed space.
       - Keep the .rail-filters element in the DOM with width: 0 +
         visibility: hidden + overflow: hidden so the JS that queries
         filter chips still works, but they take no space visually.
       - Bump main's max-width so it actually uses the reclaimed
         space (without ballooning past readable width).
       - The right-rail (.rail-opposite) column stays untouched at
         260px.
       - The chevron is position:fixed (already left:8px when hidden
         via line ~1319) so it floats independently of the grid.
       Per Gaither 2026-05-14, retry. */
    .layout.rail-hidden { grid-template-columns: 0 minmax(0, 1fr) 260px; }
    .layout.rail-hidden > .rail-filters {
      visibility: hidden;
      width: 0;
      min-width: 0;
      overflow: hidden;
      padding: 0;
      margin: 0;
    }
    .layout.rail-hidden > main { max-width: 1200px; }

    /* Right-rail (.rail-opposite) collapse chevron. Mirrors the left
       rail-toggle-tab pattern but anchored at the rail's LEFT edge so
       the arrow points outward (away from the rail) when shown. When
       the right rail is hidden, the chevron docks at the viewport's
       right edge and points left to invite re-open. State persists in
       localStorage gn:rightRailHidden. Per Gaither 2026-05-17. */
    /* Position the chevron in the gap BETWEEN main and right-rail so
       it doesn't overlap either column's content. Right-rail is 260px
       wide; layout has 28px right padding; gap between columns is 28px.
       Anchoring the chevron at right:296px (rail + padding + small
       breathing) puts it just outside the rail's left edge, sitting
       in the gap. Per Gaither 2026-05-17: "better right column chevron
       placement. Currently overlaps content." */
    /* Per Gaither 2026-05-17: right-rail toggle must MIRROR the left
       rail-toggle-tab style EXACTLY — same size, transparent bg, no
       border, no shadow, opacity 0.55. Also vertically centered on
       the viewport at all times via position:fixed + top:50% +
       translateY(-50%). */
    .layout > .right-rail-toggle-tab { display: inline-flex;
      position: fixed; top: 50%; transform: translateY(-50%);
      /* 290px = rail width (260) + 30px clear of rail's left edge.
         Bumped from 280 back to 290 per Gaither 2026-05-17
         ("nudge it just a hair back towards the left"). 6px left
         of previous 280 setting. */
      right: 290px; z-index: 60;
      width: 32px; height: 32px;
      background: transparent; color: var(--fg);
      border: 0; border-radius: 0;
      cursor: pointer; padding: 0;
      align-items: center; justify-content: center;
      box-shadow: none;
      opacity: 0.55;
      transition: opacity .15s, color .15s, right .22s ease; }
    .layout > .right-rail-toggle-tab:hover { opacity: 1; color: var(--cyan); }
    .right-rail-toggle-chevron { width: 28px; height: 28px;
      transition: transform .25s ease; }
    /* When the right rail is collapsed, dock the chevron at the RIGHT
       edge of the viewport and flip its arrow to point left so the
       user knows clicking re-opens the rail. */
    .layout > .right-rail-toggle-tab[data-state="hidden"] { right: 8px; }
    .layout > .right-rail-toggle-tab[data-state="hidden"] .right-rail-toggle-chevron {
      transform: rotate(180deg); }
    /* When the right rail is collapsed:
       - Collapse the right-rail grid column to 0 width so main
         absorbs the freed space.
       - Hide .rail-opposite with visibility:hidden + width:0 so its
         contents stay in DOM but take no space.
       - Bump main's max-width so it actually uses the reclaimed
         horizontal room. */
    .layout.right-rail-hidden { grid-template-columns: 220px minmax(0, 1fr) 0; }
    .layout.right-rail-hidden > .rail-opposite {
      visibility: hidden;
      width: 0;
      min-width: 0;
      overflow: hidden;
      padding: 0;
      margin: 0;
    }
    .layout.right-rail-hidden > main { max-width: 1200px; }
    /* When BOTH rails are hidden, give main even more room. */
    .layout.rail-hidden.right-rail-hidden { grid-template-columns: 0 minmax(0, 1fr) 0; }
    .layout.rail-hidden.right-rail-hidden > main { max-width: 1280px; }
    /* Almanac has no right rail (it shows no opposite-kind panel), so it
       gets right-rail-hidden server-side to collapse that empty column and
       the feed uses the reclaimed width. Per Gaither 2026-06-23: "Almanac
       can use the right side." Wider main than the 1200 right-rail-hidden
       default so the textbook cards actually fill the space; the body text
       inside stays at a readable measure (see .card--lesson rules) while the
       hero image spans the full card width. */
    .layout.layout--almanac > main { max-width: 1280px; }
    .layout.layout--almanac.rail-hidden > main { max-width: 1380px; }

    .filters.filters--desktop,
    .rail-extras { display: flex; flex-direction: column;
      gap: 2px; padding: 0; align-items: stretch; }
    .rail-extras { margin: 0 0 14px; padding-bottom: 12px;
      border-bottom: 1px solid var(--line); }
    /* Compact category toggles — smaller track (30x16), smaller knob
       (12px), tighter row padding. Header Hide-viewed pill (.header-toggle)
       stays at its own scale. */
    /* NOTE: no overflow:hidden on the chip itself - that would clip
       the [data-tooltip]::after tooltips on the chip-eye, count, and
       num-toggle children (they paint above the chip at top:-29.5px).
       Label truncation lives on .filter-label below, which DOES have
       overflow:hidden + text-overflow:ellipsis. */
    .filters--desktop .filter,
    .rail-extras .filter { text-align: left; padding: 5px 4px;
      font-size: 12.5px; width: 100%; max-width: 100%; background: transparent;
      border: 0; border-radius: 6px; color: var(--fg); font-weight: 500;
      display: flex; align-items: center; gap: 4px;
      position: relative; cursor: pointer;
      box-sizing: border-box;
      transition: background .15s ease; }
    .filters--desktop .filter .filter-count { padding: 1px 5px; min-width: 16px; }
    .filters--desktop .filter .filter-label,
    .rail-extras .filter .filter-label {
      flex: 1 1 auto; min-width: 0;
      white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    }
    /* iOS-style toggle pseudo-elements REMOVED per Gaither 2026-05-13.
       The per-chip eye icon is now the single visual source of truth
       for active vs hidden state. Chip body stays a clean label row. */
    .filters--desktop .filter,
    .rail-extras .filter { border-color: transparent; }
    /* Subtle inactive dim per Gaither 2026-05-15. Eye-slash is still
       the primary indicator but the chip label + glyph also gets a
       muted color so inactive chips read as "disabled" at a glance. */
    .filter[data-active="false"] .filter-label,
    .filter[data-active="false"] .filter-count { opacity: 0.45; }
    .filter[data-active="false"] .filter-glyph { opacity: 0.5; filter: grayscale(0.4); }
    /* Category glyph next to the toggle label. The .filter-glyph span
       contains an inline SVG (single-color via currentColor) inserted by
       renderHTML; the color comes from --cat-color, set per category by
       the broadened [data-cat="X"] rules. Active toggle = colored glyph;
       off toggle = grayscale + muted. */
    .filter .filter-glyph { display: inline-flex; align-items: center;
      justify-content: center;
      width: 18px; min-width: 18px; height: 18px;
      color: var(--cat-color, var(--muted));
      transition: color .15s, filter .15s ease, opacity .15s ease;
      flex-shrink: 0;
    }
    .filter .filter-glyph svg { width: 16px; height: 16px; display: block; }
    /* Inactive-glyph dim removed 2026-05-14 — last leftover from the
       opacity-based deactivated visual. Eye-slash icon carries all
       the deactivated-state signal now. */
    .filter .filter-label { flex: 1; }
    /* Unread-count badge to the right of each category. Counts cards
       in that category on the current page that haven't been viewed
       yet (per the gn:viewed localStorage set). When the count is
       zero the badge dims slightly but the rest of the chip stays at
       full opacity — per Gaither 2026-05-14, dimming the whole chip
       row caused a "dark mask over the categories" after JS had a
       chance to compute remaining counts on load. */
    .filter .filter-count { font-size: 11px; font-weight: 600;
      color: var(--muted); margin-left: 6px; padding: 1px 6px;
      border-radius: 999px; background: var(--subtle);
      min-width: 18px; text-align: center; flex-shrink: 0;
      cursor: pointer;
      transition: color .15s, background .15s, opacity .15s; }
    .filter .filter-count:hover { background: var(--card-bg); color: var(--fg); }
    /* Per-mode visual styling. The cycle is actual → all → none → actual.
       Make each mode unmistakably different so the user can SEE which
       state they're in even when the number is the same in two of them
       (e.g. 0/0 when nothing read OR everything read). Per Gaither
       2026-05-16: "clicking on numbers does not cycle article state...
       definitely broken" — partly a cycle bug (fixed in writePerCatMode)
       and partly a visual-feedback bug (fixed here). */
    .filter .filter-count[data-mode="actual"] { background: var(--subtle); color: var(--muted); }
    .filter .filter-count[data-mode="all"]    {
      background: var(--cyan); color: #fff;
      box-shadow: 0 0 0 1px var(--cyan);
      text-decoration: line-through; text-decoration-color: rgba(255,255,255,0.6);
    }
    .filter .filter-count[data-mode="none"]   {
      background: transparent; color: var(--fg);
      outline: 1px dashed var(--cyan); outline-offset: -1px;
      font-weight: 700;
    }
    .filter[data-count-empty="true"] .filter-count { opacity: 0.55; }
    /* Desktop-specific layout for mark-all-read sits in the rail. The
       base button styling has been moved OUT of this media query so
       the mobile drawer copy doesn't render as a browser-default white
       box (Gaither flagged 2026-05-13). */
    /* No row background on hover or active — the toggle visual itself
       (cyan track + halo + label) carries the state; a full-row pill
       on top of that read as cluttered. Keep just the cursor and a
       small color shift on the label for affordance. */
    .filters--desktop .filter:hover,
    .rail-extras .filter:hover { background: transparent; }
    .filters--desktop .filter:hover { color: var(--fg); }
    .filters--desktop .filter:focus-visible,
    .rail-extras .filter:focus-visible { outline: 2px solid var(--cyan);
      outline-offset: 2px; }
    /* DELETED: dark-mode styling for the old iOS-style toggle ::before
       (track) and ::after (knob) pseudo-elements. The toggles were
       replaced by the chip-eye icon, but these rules were never cleaned
       up — and they hijacked .filter::after which is now the [data-tooltip]
       tooltip element. That caused the All chip to render its tooltip
       as a giant white block (white bg + white text). The edit / reset
       buttons inherited the same broken styling and showed nothing.
       Gaither flagged 2026-05-13. */
    /* "All" master row stays as an uppercase label with its own toggle.
       Sized to its own content (width: fit-content) so the tooltip
       hover area only covers the visible 'ALL 4141' text — Gaither
       2026-05-13 flagged that the parent rail's full-width hit area
       was triggering the All tooltip when hovering empty space.
       No border / pill chrome — Gaither 2026-05-13 flagged that the
       cyan pill made the row look highlighted vs. the bare category
       chips below. */
    .filters--desktop .filter.all-toggle {
      width: fit-content; max-width: 100%;
      font-weight: 700;
      text-transform: uppercase; letter-spacing: 0.08em; font-size: 10.5px;
      color: var(--muted); margin-bottom: 4px; padding: 5px 8px;
    }
    .filters--desktop .filter.all-toggle[data-active="true"] { color: var(--cyan); }
    /* LEFT rail is STICKY so the filters stay reachable while scrolling.
       RIGHT rail scrolls WITH the page (no sticky) so the three columns
       move together as one document. */
    .rail { display: block; padding: 0 0 32px; }
    .rail-filters { position: sticky; top: 88px;
      max-height: calc(100vh - 110px); overflow-y: auto;
      overflow-x: hidden;
      /* 8px left padding so the chip pills don't sit flush against the
         rail's left edge (was visually clipping the first character on
         narrow desktop viewports per Gaither 2026-05-13). */
      padding: 4px 0 20px 8px;
      transition: top .2s ease, max-height .2s ease; }
    /* When the header collapses on scroll, the rail can climb closer
       to the top and gain back vertical room — reducing the chance of
       a chip overflow needing the scrollbar. Per Gaither 2026-05-17:
       "Categories rail could move up a bit more so it has more vertical
       space and less chance of a vertical scrollbar." */
    body.scrolled-page .rail-filters {
      top: 56px;
      max-height: calc(100vh - 70px);
    }
    /* Modern uppercase sans for rail titles — Inter at tight letter-
       spacing reads cleaner than Source Serif at this size, and pairs
       with the toggle row beneath. */
    /* Skip-to-articles link for keyboard users. Hidden offscreen until
     focused, then floats over the top-left of the page so the first
     Tab key press jumps past 30+ category chips straight to the grid. */
  /* Truly hidden until focused. clip-path is the modern safe pattern;
     left:-9999 alone leaked into the header flex layout on some
     viewports (visible "Skip to articles" text top-right on tablet). */
  .skip-link { position: fixed; top: 0; left: 0;
    width: 1px; height: 1px;
    overflow: hidden; clip-path: inset(50%);
    white-space: nowrap;
    background: var(--cyan); color: #fff;
    padding: 8px 16px; border-radius: 4px;
    font-family: var(--sans); font-weight: 600;
    z-index: 9999; text-decoration: none;
    pointer-events: none; }
  .skip-link:focus, .skip-link:focus-visible {
    width: auto; height: auto;
    overflow: visible; clip-path: none;
    top: 12px; left: 12px;
    pointer-events: auto; outline: 2px solid var(--cyan);
    outline-offset: 2px; }
  .rail-title { font-family: var(--sans); font-size: 11.5px;
      text-transform: uppercase; letter-spacing: 0.22em; color: var(--muted);
      font-weight: 700; margin: 0 0 12px; padding-bottom: 8px;
      border-bottom: 1px solid var(--line); }
    /* Opposite-kind rail title — clickable to switch view. On news pages
       the "Research" label switches to research view; on research pages
       the "News" label switches to news view. The chevron + cyan hover
       sells the affordance without adding noise. */
    .rail-title--switch { padding: 0; padding-bottom: 8px;
      border-bottom: 1px solid var(--line); }
    .rail-title-link { display: inline-flex; align-items: center; gap: 6px;
      color: var(--muted); text-decoration: none; font: inherit;
      letter-spacing: inherit; text-transform: inherit; padding: 4px 0;
      border-radius: 4px; transition: color 0.12s ease; cursor: pointer; }
    .rail-title-link:hover, .rail-title-link:focus-visible {
      color: var(--cyan); text-decoration: none; }
    .rail-title-arrow { width: 13px; height: 13px; opacity: 0.7;
      transition: transform 0.12s ease, opacity 0.12s ease; }
    .rail-title-link:hover .rail-title-arrow,
    .rail-title-link:focus-visible .rail-title-arrow {
      transform: translateX(2px); opacity: 1; }
    .rail-list { list-style: none; padding: 0; margin: 0;
      display: flex; flex-direction: column; gap: 10px; }
    .rail-item { margin: 0; }
    /* Vertical layout: full-column-width image at the top, full headline
       text underneath (no line-clamp, no ellipsis). Even long headlines
       like "Stat Plus: Bristol Myers Squibb Partners with China's Higri
       Pharma..." now render in full. Slightly bigger gap between item
       rows because each item is taller. */
    /* Category color stripe on the left edge — uses the muted
       --cat-border (~40% alpha) rather than full --cat-color so the
       rail stays calm and doesn't pull the eye away from the main
       feed. Thinned to 2px for the same reason. Falls back to --line
       when no category is set. */
    .rail-link { display: flex; flex-direction: column; gap: 8px;
      padding: 6px 8px 6px 9px; border-radius: 6px;
      border-left: 2px solid var(--cat-border, var(--line));
      text-decoration: none;
      color: var(--fg); transition: background .15s, border-color .15s;
      align-items: stretch; }
    .rail-link:hover { background: var(--subtle); text-decoration: none; }
    .rail-thumb { width: 100%; height: auto; max-height: 140px;
      border-radius: 4px;
      object-fit: cover; object-position: 50% 50%;
      background: var(--subtle); display: block; }
    .rail-thumb--blank { width: 100%; aspect-ratio: 16/9; max-height: 110px;
      background: var(--subtle);
      border: 1px dashed var(--line); border-radius: 4px; }
    .rail-headline { font-size: 13px; line-height: 1.4; font-weight: 500;
      color: var(--fg); }
    /* Small relative-time stamp inside the meta row, sitting right
       after the source favicons (newspaper byline order: outlet,
       then date). Muted, monospace-numeric so digits align cleanly
       between cards of different age. */
    .rail-updated { font-size: 10.5px; color: var(--muted);
      font-variant-numeric: tabular-nums;
      letter-spacing: 0.02em; opacity: 0.78;
      white-space: nowrap; }
    /* Rail category pill — compact version of the meta-row .pill.cat,
       same per-category --cat-color so users can scan by topic. Sits
       between the thumbnail and the headline. */
    .rail-cat { display: inline-flex; align-items: center; gap: 4px;
      align-self: flex-start;
      font-size: 10px; font-weight: 600;
      text-transform: uppercase; letter-spacing: 0.06em;
      color: var(--cat-color, var(--muted));
      padding: 2px 7px; border-radius: 999px;
      background: var(--cat-soft, var(--subtle));
      border: 1px solid var(--cat-border, var(--line)); }
    .rail-cat .cat-glyph { display: inline-flex; width: 11px; height: 11px; }
    .rail-cat .cat-glyph svg { width: 11px; height: 11px; }
    /* Meta row: category pill + small source favicons on the same line.
       flex-wrap lets the row grow to multiple lines when the cluster has
       many sources, so each rail card simply expands vertically rather
       than overflowing the rail width. */
    .rail-meta { display: flex; flex-wrap: wrap; align-items: center;
      gap: 4px 6px; align-self: flex-start; max-width: 100%; }
    .rail-sources { display: inline-flex; flex-wrap: wrap;
      align-items: center; gap: 3px; }
    /* No overflow:hidden here — the [data-tooltip]::after pseudo would
       be clipped to the 16px box and never appear in the viewport. The
       inner favicon img is 12px (smaller than the 14px content box
       after border) so there's nothing visual to clip anyway. */
    /* Circular mask matching the article-card source-pill favicons —
       Gaither 2026-05-14. Same border-radius: 50% + dark-theme
       off-white backdrop so dark-ink favicons (NY Post, Daily Wire)
       stay visible against the dark card. */
    .rail-source { display: inline-flex; align-items: center;
      justify-content: center; width: 16px; height: 16px;
      border-radius: 50%; background: var(--card-bg);
      border: 1px solid var(--line); }
    .rail-source-icon { width: 12px; height: 12px; display: block;
      object-fit: contain; border-radius: 50%; }
    .rail-source-fallback { font-family: var(--sans); font-size: 8px;
      font-weight: 700; color: var(--muted); letter-spacing: 0;
      line-height: 1; text-align: center;
      display: inline-flex; align-items: center; justify-content: center;
      width: 100%; height: 100%; border-radius: 50%; }
    [data-theme="dark"] .rail-source {
      background: #f4f4f0;
      box-shadow: 0 0 0 1px rgba(255,255,255,0.08);
    }
    [data-theme="dark"] .rail-source-icon {
      filter: brightness(1.08) contrast(0.92); }
    .rail-list { gap: 16px !important; }
    /* Sticky-rail scrollbar — thin, low-contrast */
    .rail-filters::-webkit-scrollbar { width: 6px; }
    .rail-filters::-webkit-scrollbar-thumb { background: var(--line); border-radius: 3px; }
    .rail-filters::-webkit-scrollbar-track { background: transparent; }
  }

  /* ---- Filters ---- */
  .filters { padding: 14px 32px; display: flex; gap: 8px; flex-wrap: wrap;
    border-bottom: 1px solid var(--line); }
  /* Mark-all-read pill — used in the desktop rail AND the mobile
     drawer. Was previously scoped inside the @media (min-width:1180px)
     block, which left the mobile copy unstyled and the browser fell
     back to a white block with black text. Moved out so both viewports
     get the same muted-line treatment. */
  /* Pill style matching the Hide-read pill in the header, per Gaither
     2026-05-15: NOT full-width. Centered in the rail per follow-up
     2026-05-15. Wrapping the pill in a flex container centers it. */
  .mark-all-read-row { display: flex; justify-content: center;
    margin: 14px 0 0; }
  .mark-all-read-btn { display: inline-flex; align-items: center;
    margin: 0; padding: 6px 14px;
    background: transparent; color: var(--muted);
    border: 1px solid var(--line); border-radius: 999px;
    font-family: var(--sans); font-size: 12px; font-weight: 500;
    letter-spacing: 0.02em; cursor: pointer; line-height: 1.3;
    transition: border-color .15s, background .15s, color .15s; }
  .mark-all-read-btn:hover { border-color: var(--cyan);
    color: var(--cyan); background: var(--cyan-soft); }
  /* Mobile: chip rail is a wrap-pill mosaic, hidden behind a bottom
     chevron drawer. data-open=true on .filters--mobile lets the
     wrapped mosaic render. The drawer toggle (chevron) is fixed to
     the bottom of the viewport. On scroll the toggle's label
     collapses to icon-only via .scrolled class on body. */
  .filters--mobile {
    display: none;
    padding: 14px 18px;
    background: var(--card-bg);
    border-bottom: 1px solid var(--line);
    /* No max-height / overflow — let the drawer grow with content.
       The internal scrollbar made the drawer feel cramped and clipped
       tooltips. Page scrolls instead. */
  }
  /* Mosaic — flex-wrap with content-sized chips. Tighter rows where
     short labels (DIY, World) pack together; longer labels (Social
     issues, Entertainment) get the room they need. Gaither prefers
     this over the strict 2-column grid. */
  .filters--mobile[data-open="true"] {
    display: flex;
    flex-wrap: wrap;
    /* Tighter chip-to-chip gap so the mosaic packs more cards per row
       instead of leaving wide vertical channels. */
    gap: 6px 6px;
    align-items: flex-start;
  }
  .filters--mobile .filter[data-cat] {
    /* Trim padding so each chip's hit area is content-sized, packing
       2-3 short labels per row instead of 1-2. */
    padding: 5px 10px 5px 7px !important;
    font-size: 12.5px;
  }
  .filters--mobile .filters-mobile-tools,
  .filters--mobile .all-toggle,
  .filters--mobile .mark-all-read-btn,
  .filters--mobile .cats-drawer-close {
    flex: 1 1 100%;
  }
  /* Chip pill itself stays inline-flex sized to content, with a
     consistent right padding so the count + num-toggle don't overlap
     longer labels (the Health chip was overlapping with its eye in
     Gaither's mobile screenshot). */
  .filters--mobile .filter[data-cat] {
    flex: 0 0 auto;
    min-width: 0;
    padding: 6px 12px 6px 8px;
    column-gap: 6px;
  }
  .filters--mobile .filter[data-cat] .filter-label {
    max-width: none; white-space: nowrap;
  }
  .filters--mobile .all-toggle { font-weight: 600; }
  /* Tools header row inside the mobile drawer: 'Categories' label on
     the LEFT (Gaither 2026-05-13), then edit + reset pencils pushed
     to the right with margin-left: auto on the first action button. */
  .filters-mobile-tools {
    flex: 0 0 100%;
    display: flex; align-items: center; gap: 8px;
    padding: 0 0 8px;
    border-bottom: 1px solid var(--line);
    margin-bottom: 4px;
  }
  .filters-mobile-tools-label {
    font-family: var(--sans); font-size: 11px;
    text-transform: uppercase; letter-spacing: 0.22em;
    color: var(--muted); font-weight: 700;
  }
  .filters-mobile-tools .edit-categories-btn { margin-left: auto; }
  /* Close chevron at the bottom of the drawer. Spans the full row,
     centered. Tap target ~32px tall. */
  .cats-drawer-close {
    flex: 0 0 100%;
    display: flex; align-items: center; justify-content: center;
    margin: 12px 0 0; padding: 6px;
    background: transparent; border: 1px solid var(--line);
    border-radius: 8px; color: var(--muted);
    cursor: pointer;
    transition: border-color .15s, color .15s, background .15s;
  }
  .cats-drawer-close:hover {
    border-color: var(--cyan); color: var(--cyan);
    background: var(--cyan-soft);
  }
  .cats-drawer-close svg { width: 20px; height: 20px; }
  .cats-drawer-close-label { display: inline-block;
    font-family: var(--sans); font-size: 12px; font-weight: 600;
    letter-spacing: 0.06em; text-transform: uppercase;
    color: var(--muted); vertical-align: middle;
    white-space: nowrap; }
  /* Center the chevron+label pair as a self-sized pill, not edge-aligned.
     margin auto on the sides lets the button auto-center horizontally inside
     the drawer's flex column instead of leaning against the left edge
     where the label was getting visually clipped at narrow viewports. */
  .cats-drawer-close { display: inline-flex !important;
    align-items: center; gap: 8px;
    width: auto !important; flex: 0 0 auto !important;
    margin: 12px auto 0 !important;
    padding: 8px 18px !important;
    border-radius: 999px !important; }
  .cats-drawer-close:hover .cats-drawer-close-label { color: var(--cyan); }
  body.editing .filters-mobile-tools .reset-categories-btn {
    display: inline-flex;
  }
  /* Mark-all-read inside the mobile drawer: full-width pill below the
     chips so the user can see all three states of the cycle (all /
     none / actual). Matches the desktop rail's bottom button. */
  .filters--mobile .mark-all-read-btn {
    flex: 0 0 100%;
    margin-top: 8px;
  }
  .filter { background: transparent; border: 1px solid var(--line);
    color: var(--fg); padding: 6px 14px; border-radius: 999px;
    font-size: 13px; font-family: inherit; cursor: pointer;
    transition: border-color .15s, background .15s, color .15s; }
  .filter:hover { border-color: var(--cyan); }
  /* The eye icon is the single visual source of truth for active /
     hidden state per Gaither 2026-05-13. Chip itself stays neutral
     so the label reads as a label, not a toggle. The earlier
     opacity:0.55 dim was removed 2026-05-14 — Gaither saw it as a
     "dark mask" over the inactive-page chips, and the eye-slash icon
     already conveys the deactivated state. */
  /* Mobile + base filter-glyph sizing. The inline SVG would otherwise
     default to the browser intrinsic size (300x150) when no explicit
     width/height attributes are set — the desktop rule lives inside a
     min-width media query, so without this base mobile rendered them
     enormous. Both glyph wrapper and inner SVG are tightly capped. */
  .filter { display: inline-flex; align-items: center; gap: 6px; }
  .filter .filter-glyph { display: inline-flex; align-items: center;
    justify-content: center;
    width: 16px; min-width: 16px; height: 16px;
    color: var(--cat-color, var(--cyan));
    flex-shrink: 0; }
  .filter .filter-glyph svg { width: 14px; height: 14px; display: block; }
  /* Glyph dim on inactive chips removed 2026-05-14 — same reason as
     the parent chip dim: redundant with the eye-slash icon. */
  .filter .filter-label { display: inline; }
  /* Per-chip eye toggle. Open eye when active (showing), eye with
     slash when inactive (category hidden). Click the eye to flip. */
  .chip-eye { display: inline-flex; align-items: center; justify-content: center;
    width: 18px; min-width: 18px; height: 18px;
    color: var(--muted); margin-left: 4px;
    flex-shrink: 0; cursor: pointer;
    transition: color .15s; }
  .chip-eye:hover { color: var(--cyan); }
  .chip-eye svg { width: 14px; height: 14px; display: block; }
  .chip-eye .chip-eye-off { display: none; }
  .filter[data-active="false"] .chip-eye .chip-eye-on { display: none; }
  .filter[data-active="false"] .chip-eye .chip-eye-off { display: inline-block; }
  /* On mobile + tablet the chip-eye lives inside a pill-shaped chip
     where the small 14px glyph used to vanish. Bump size + color so
     the show/hide affordance is obvious. Also tighten margin so it
     sits flush against the category glyph. */
  .filters--mobile .chip-eye {
    width: 22px; min-width: 22px; height: 22px;
    margin-left: 0; margin-right: 4px;
    color: var(--cyan);
  }
  .filters--mobile .chip-eye svg { width: 17px; height: 17px; }
  .filters--mobile .filter[data-active="false"] .chip-eye {
    color: var(--muted);
  }
  /* Hide chip-eye during edit mode (grabber takes over). */
  body.editing .filter .chip-eye { display: none; }

  /* ----- Active / Inactive category pages -----
     Each filter container carries data-page="active" by default. The
     "Inactive categories →" pill flips it to "inactive"; "← Active
     categories" flips it back. Per Gaither 2026-05-14: page assignment
     is driven by the chip's eye state (data-active), not a fixed
     primary/secondary split — clicking the eye on any chip moves it
     between pages instantly. */
  .cats-page-toggle {
    background: var(--subtle, rgba(140,144,156,0.1));
    border: 1px solid var(--line-strong, var(--line));
    color: var(--fg);
    padding: 7px 14px;
    border-radius: 8px;
    font-size: 11.5px;
    font-weight: 600;
    text-transform: uppercase;
    letter-spacing: .04em;
    font-family: inherit;
    cursor: pointer;
    transition: border-color .15s, color .15s, background .15s;
    display: inline-flex;
    align-items: center;
    gap: 6px;
    /* At the TOP of the rail now (Gaither 2026-06-23: "move the Active/Inactive
       categories label at top so it's more obvious"). order:-1 + flex-basis
       100% put it first, on its own full-width row, above the chips. */
    order: -1;
    flex-basis: 100%;
    align-self: stretch;
    justify-content: center;
    margin: 0 0 10px;
  }
  .cats-page-toggle:hover {
    border-color: var(--cyan);
    color: var(--cyan);
  }
  /* Active page: hide inactive (eye-off) chips and the back-to-active
     pill. All-toggle (no data-active="false" possible) stays. */
  .filters[data-page="active"] .filter[data-cat][data-active="false"]:not(.all-toggle),
  .filters[data-page="active"] .cats-page-to-active { display: none; }
  /* Inactive page: hide active (eye-on) chips and the forward pill.
     The All toggle stays visible per Gaither 2026-05-14 so the user
     can deactivate or activate every chip from either page. */
  .filters[data-page="inactive"] .filter[data-cat][data-active="true"]:not(.all-toggle),
  .filters[data-page="inactive"] .cats-page-to-inactive { display: none; }
  /* When the inactive set is empty, hide the "Inactive categories →"
     pill so users aren't routed to an empty page. */
  .filters[data-page="active"]:not(:has(.filter[data-cat][data-active="false"])) .cats-page-to-inactive {
    display: none;
  }
  /* In edit mode the page toggle pills make no sense — hide them so
     the rail stays focused on the chips being reordered. Edit mode
     also flattens to a single view (show ALL chips regardless of eye
     state) so the user can drag any of them. */
  body.editing .cats-page-toggle { display: none; }
  body.editing .filters[data-page="active"] .filter[data-cat][data-active="false"]:not(.all-toggle),
  body.editing .filters[data-page="inactive"] .filter[data-cat][data-active="true"]:not(.all-toggle) {
    display: inline-flex;
  }

  /* Edit-categories pencil button. Sits inline with the rail title on
     desktop, and on its own row above the mobile filter strip. Toggle
     active state via the .editing class on body. */
  .edit-categories-btn { background: transparent; border: 1px solid var(--line);
    color: var(--muted); width: 28px; height: 28px; border-radius: 50%;
    cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
    padding: 0; transition: border-color .15s, background .15s, color .15s;
    margin-left: auto; }
  .edit-categories-btn:hover { border-color: var(--cyan); color: var(--cyan); }
  .edit-categories-btn svg { width: 14px; height: 14px; }
  body.editing .edit-categories-btn {
    background: var(--cyan); color: #fff; border-color: var(--cyan);
  }
  /* Reset-categories button: only visible in edit mode, but the SLOT
     is always reserved via visibility:hidden so the edit pencil
     doesn't shift sideways when reset appears. Gaither 2026-05-13
     flagged that the edit button was sliding into the reset slot
     and made accidental reset clicks too easy. Reset now sits to
     the immediate right of edit at all times; only its painting
     toggles. */
  .reset-categories-btn { background: transparent; border: 1px solid var(--line);
    color: var(--muted); width: 28px; height: 28px; border-radius: 50%;
    cursor: pointer; display: inline-flex; align-items: center; justify-content: center;
    padding: 0; transition: border-color .15s, background .15s, color .15s;
    visibility: hidden; pointer-events: none; }
  .reset-categories-btn:hover { border-color: var(--cyan); color: var(--cyan); }
  .reset-categories-btn svg { width: 14px; height: 14px; }
  body.editing .reset-categories-btn { visibility: visible; pointer-events: auto; }
  /* Stop the edit pencil from using margin-left:auto so it doesn't
     pull all the way to the rail's right edge — keep it tight
     against the reset button slot. */
  .rail-title .edit-categories-btn { margin-left: auto; margin-right: 0; }
  .rail-title .reset-categories-btn { margin-left: 0; }
  .rail-title { display: flex; align-items: center; gap: 8px; }

  /* Drag grip handle on each chip. Hidden by default. In edit mode
     the grip REPLACES the eye+glyph+count icons (but the LABEL stays
     visible so the user can see what they're dragging). Larger
     grabber for an obvious drag target. */
  .filter-grip { display: none; align-items: center; justify-content: center;
    width: 18px; min-width: 18px; height: 18px;
    color: var(--muted); flex-shrink: 0; cursor: grab; }
  .filter-grip svg { width: 16px; height: 16px; display: block; opacity: 0.9; }
  body.editing .filter[data-cat] .filter-grip {
    display: inline-flex; width: 22px; height: 22px;
    color: var(--fg);
  }
  body.editing .filter[data-cat] .filter-grip svg { width: 20px; height: 20px; }
  /* Hide the action icons during edit; keep glyph + label visible so
     the user can identify the chip they're moving. */
  body.editing .filter[data-cat] .chip-eye,
  body.editing .filter[data-cat] .filter-count { display: none; }
  body.editing .filter[data-cat] { cursor: grab; }
  body.editing .filter[data-cat]:active { cursor: grabbing; }
  body.editing .filter.dragging { opacity: 0.4; }
  body.editing .filter.drag-over { box-shadow: -3px 0 0 var(--cyan); }
  body.editing .filter.touch-dragging { opacity: 0.7; }
  /* All-toggle hidden during edit. */
  body.editing .all-toggle { display: none; }

  /* Source filter chips: hidden = struck through. */
  .source-chips { padding: 10px 32px; display: flex; gap: 6px; flex-wrap: wrap;
    border-bottom: 1px solid var(--line); align-items: center; }
  .source-chips .chips-label { font-size: 10px; text-transform: uppercase;
    letter-spacing: 0.12em; color: var(--muted); margin-right: 4px;
    font-weight: 600; }
  .source-chip { background: transparent; border: 1px solid var(--line);
    color: var(--fg); padding: 3px 10px; border-radius: 999px;
    font-size: 11px; font-family: inherit; cursor: pointer;
    transition: opacity .15s, background .15s; }
  .source-chip:hover { background: var(--subtle); }
  .source-chip[data-hidden="true"] { opacity: 0.4; text-decoration: line-through; }
  @media (max-width: 720px) {
    .filters, .source-chips { padding-left: 16px; padding-right: 16px; }
  }

  main { max-width: 980px; margin: 0 auto; padding: 32px 24px 60px; }
  /* Tablet (721-1180px): main fills the viewport instead of being capped
     at 980px + centered. The 980px cap left ~60px of dead space on each
     side at 1100px and made the column feel like it was "floating" as
     the viewport resized between breakpoints. Per Gaither 2026-05-17:
     "tablet view has a weird thing where it floats back and forth and
     doesn't fill window width." At 1180+ the desktop grid layout kicks
     in and main re-anchors inside the 220/1fr/260 grid. */
  @media (min-width: 721px) and (max-width: 1180px) {
    main { max-width: none; width: 100%;
      padding-left: 24px; padding-right: 24px; }
  }
  .page-heading { font-family: var(--serif); font-size: 28px;
    margin: 0 0 20px; letter-spacing: -0.01em; }
  .grid { display: flex; flex-direction: column; gap: 28px; }

  /* ---- View-mode CSS (Gaither 2026-05-16) ----
     Three layouts controlled by body[data-view-mode="..."]. Default
     (no attribute or "default") keeps the existing per-card design.
     Horizontal and grid are compact-card overrides where the card
     shows ONLY image + title + short excerpt + an expand affordance.
     Clicking the headline toggles the existing .expanded-state class
     (the same mechanism used to reveal the read-more box on default-
     view cards); when expanded, all the previously hidden parts
     (sources / Read original / Share to Social / Category / L/R
     panels / related coverage / full article body) become visible at
     the bottom of the card.

     Cards that are inherently visual (feature, cartoon, photo,
     wallpaper) opt OUT — they keep their bespoke layouts since they
     don't have body text to reshape.

     Selector shorthand (used repeatedly below): .vm-card targets the
     news/study cards that the view-mode overrides apply to. Defined
     here as a "real" alias so the rules stay readable. */
  .vm-card,
  .vm-card-h /* horizontal */,
  .vm-card-g /* grid */ { /* placeholder — overridden via :is() below */ }

  /* HORIZONTAL: image left (wider than default), title + 3-line excerpt
     right. Everything else hidden until the user expands. NOTE the
     class-name corrections from the previous attempt: share row is
     .action-row (not .share-row), category-pill row is .meta
     (not .meta-row). The earlier selectors missed both. */
  body[data-view-mode="horizontal"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) :is(
    .lr-section,
    .lr-row,
    .related-coverage-row,
    .related-row,
    .summary-mid,
    .deep-prose,
    .has-said-block,
    .action-row,
    .lean-meter,
    .prior-coverage-btn,
    .read-more-box
  ) {
    display: none;
  }
  /* In regular horizontal mode, .sources-row + .meta stay visible per
     Gaither 2026-05-17: "move category and news sources to under
     picture". .sources-row already lives inside .card-side (the image
     column) in the DOM, so it naturally sits under the picture.
     .meta also gets visually surfaced under the image: collapse it
     out of .body via display:contents and re-anchor the category pill
     via flex order at the bottom of card-side. */
  body[data-view-mode="horizontal"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .meta {
    /* Compact meta row: only the category pill matters here; the
       source-count + dates already live elsewhere on the card. */
    padding: 4px 0 0;
    margin: 0;
    gap: 6px;
    font-size: 11px;
  }
  /* Hide the L/R full-width row that lives in .card-bottom. The class
     for that container varies (.lr-row, .lr-section, .lr-grid); use
     a broader hit at the card-bottom level to be safe. */
  body[data-view-mode="horizontal"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .card-bottom {
    display: none;
  }
  /* Horizontal compact mode image width. Earlier pass set this to 380px
     per "pic a little wider"; user feedback 2026-05-16 follow-up is now
     "smaller pics on horizontal card mode", so reduced to 240px. The
     image still anchors the row but no longer dominates. */
  body[data-view-mode="horizontal"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .card-side {
    flex-basis: 240px;
    max-width: 240px;
  }
  body[data-view-mode="horizontal"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .preview {
    /* Per Gaither 2026-05-18: fit to width first, never crop top/bottom.
       contain instead of cover, height auto, generous max-height so
       tall portraits stretch. */
    width: 100%;
    height: auto;
    max-height: 360px;
    object-fit: contain;
    object-position: 50% 50%;
    background: var(--subtle);
  }
  body[data-view-mode="horizontal"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .summary {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  body[data-view-mode="horizontal"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) .headline {
    font-size: 20px;
    line-height: 1.25;
  }

  /* GRID: 3-column responsive tiles, image on top, title under image,
     short excerpt under title. Same expand contract as horizontal.
     Padding + gap bumped 2026-05-16 per "add a little more padding to
     the side of the cards in card view"; breakpoint moved earlier
     1100px -> 1280px so the 3-col layout drops to 2-col before the
     cards get cramped.
     grid-auto-flow: dense backfills the gaps that full-width cards
     (photos / This Day / cartoons / wallpapers / expanded cards)
     would otherwise leave behind. Without dense flow, a 3-col layout
     places two cards in cols 1+2, then jumps the full-width photo
     card to row 2 and leaves col 3 of row 1 empty. dense pulls the
     next available regular card into that empty slot so every column
     has a card. Per Gaither 2026-05-16. */
  body[data-view-mode="grid"] .grid,
  body[data-view-mode="grid-short"] .grid {
    display: grid;
    grid-template-columns: repeat(3, minmax(0, 1fr));
    gap: 28px;
    padding: 0 8px;
    grid-auto-flow: dense;
  }
  /* 3-col grid keeps at MBP 16 retina widths (1180-1920). Drops to
     2-col under 1180 (tablet) and 1-col on mobile. Per Gaither
     2026-05-17 "on macbook 16 retina screen view, if both left and
     right rails are collapsed, then should be 3 rows...barely...but
     just enough room". With BOTH rails collapsed at MBP retina, main
     caps at 1280px which fits 3 cards (each ~400px) with the 28px
     gap. Lower viewports (tablet/mobile) still drop to 2-col / 1-col. */
  @media (max-width: 1179px) {
    body[data-view-mode="grid"] .grid,
    body[data-view-mode="grid-short"] .grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  }
  /* MBP retina (1180-1919) card grid:
       0 rails hidden  → 2 columns (cards full-width-friendly with rails)
       1 rail hidden   → 2 columns (still a bit cramped with one rail)
       2 rails hidden  → 3 columns (full width, fits 3 cards)
     Per Gaither 2026-05-17 'on retina, two cards in card views with
     one rail collapsed, two rails collapsed, then 3 columns'. */
  @media (min-width: 1180px) and (max-width: 1919px) {
    body[data-view-mode="grid"] .grid,
    body[data-view-mode="grid-short"] .grid {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }
  /* Both-rails-hidden override at any desktop width: 3 columns.
     Must out-specificity the 2-col rule above, hence !important. */
  @media (min-width: 1180px) {
    body[data-view-mode="grid"] .layout.rail-hidden.right-rail-hidden .grid,
    body[data-view-mode="grid-short"] .layout.rail-hidden.right-rail-hidden .grid {
      grid-template-columns: repeat(3, minmax(0, 1fr)) !important;
    }
  }
  @media (max-width: 640px) {
    body[data-view-mode="grid"] .grid,
    body[data-view-mode="grid-short"] .grid { grid-template-columns: 1fr; }
  }
  body[data-view-mode="grid"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) {
    display: flex; flex-direction: column;
  }
  body[data-view-mode="grid"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) .card-top {
    flex-direction: column;
  }
  body[data-view-mode="grid"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) .card-side {
    width: 100%;
    max-width: 100%;
    flex-basis: auto;
  }
  body[data-view-mode="grid"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) .preview {
    /* Per Gaither 2026-05-18: fit to width first, never crop top/bottom. */
    width: 100%;
    height: auto;
    max-height: 480px;
    object-fit: contain;
    object-position: 50% 50%;
    background: var(--subtle);
  }
  body[data-view-mode="grid"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) .headline {
    font-size: 17px;
    line-height: 1.25;
    /* Padding moved up to .body now (per-element padding was making
       the card layout fragile per 2026-05-16 inspect). Zero here so
       the headline doesn't compound the body's padding. */
    padding: 0;
    margin: 0;
  }
  body[data-view-mode="grid"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) :is(
    .lr-section,
    .lr-row,
    .related-coverage-row,
    .related-row,
    .summary-mid,
    .deep-prose,
    .has-said-block,
    .sources-row,
    .action-row,
    .meta,
    .lean-meter,
    .prior-coverage-btn,
    .read-more-box
  ) {
    display: none;
  }
  body[data-view-mode="grid"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .card-bottom {
    display: none;
  }
  /* Clamp the excerpt to ~100 words in grid mode per Gaither 2026-05-16.
     ~100 words at the grid card's font size lands at about 8 visible
     lines before overflow ellipsis kicks in. Padding inherits from
     .body now (per the 2026-05-16 architecture cleanup) so no
     per-element side padding here. */
  body[data-view-mode="grid"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .summary {
    display: -webkit-box;
    -webkit-line-clamp: 8;
    -webkit-box-orient: vertical;
    overflow: hidden;
    padding: 8px 0 0;
    margin: 0;
  }
  /* Short modes: show a ~50-word teaser excerpt. The card's article
     text lives inside <div class="expanded"><h4>Article excerpt</h4>
     <p>...</p></div>. We hide the H4 label but keep the <p>,
     clamped to ~4 lines (~50 words at typical font size). Per
     Gaither 2026-05-17 "add 50 word excerpts to horizontal short
     cards and regular short cards". */
  body[data-view-mode="horizontal-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .expanded > h4,
  body[data-view-mode="grid-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .expanded > h4 {
    display: none;
  }
  body[data-view-mode="horizontal-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .expanded,
  body[data-view-mode="grid-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .expanded {
    display: block !important;
    padding: 6px 0 0;
    margin: 0;
    border: 0;
    background: transparent;
  }
  body[data-view-mode="horizontal-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .expanded > p,
  body[data-view-mode="grid-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .expanded > p {
    display: -webkit-box;
    -webkit-line-clamp: 4;
    -webkit-box-orient: vertical;
    overflow: hidden;
    margin: 0;
    font-size: 13.5px;
    line-height: 1.45;
    color: var(--muted);
  }
  /* Card body padding in grid mode. After two iterations of moving
     padding around per-element, the cleanest answer is a single
     .body padding source that every inner element inherits visually.
     20px top/bottom + 22px sides gives the text breathing room on
     all sides and removes the architectural fragility of per-element
     overrides. Per Gaither 2026-05-16 + chrome inspect confirming
     22px wasn't visible enough. */
  body[data-view-mode="grid"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) .body {
    padding: 20px 22px 22px;
  }
  /* Special-cards (Photos, This Day feature cards, Cartoons, Wallpapers)
     span ALL grid columns per Gaither 2026-05-16: "they should span all
     columns in the middle, whether it's 1, 2, or 3 depending on width."
     CSS grid grid-column: 1 / -1 spans every column regardless of
     column count, so this single rule covers the 3-col, 2-col, and
     1-col breakpoints. */
  body[data-view-mode="grid"] .grid > .photo-card,
  body[data-view-mode="grid"] .grid > .wallpaper-card,
  body[data-view-mode="grid"] .grid > .card--feature,
  body[data-view-mode="grid"] .grid > .card--cartoon {
    grid-column: 1 / -1;
  }
  /* When a grid-mode card is expanded, it spans all columns so the L/R
     panels have room to read without being squashed into a single
     column. Per Gaither 2026-05-16: "when person clicks Expand the
     full... it opens up just that card to full width of columns". */
  body[data-view-mode="grid"] .grid > .card.expanded-state:not(.photo-card):not(.wallpaper-card):not(.card--feature):not(.card--cartoon) {
    grid-column: 1 / -1;
  }
  /* In horizontal mode, give the same full-column treatment to the
     special cards so a Cartoons strip or wallpaper isn't squeezed into
     a narrow horizontal lane. */
  body[data-view-mode="horizontal"] .grid > .photo-card,
  body[data-view-mode="horizontal"] .grid > .wallpaper-card,
  body[data-view-mode="horizontal"] .grid > .card--feature,
  body[data-view-mode="horizontal"] .grid > .card--cartoon,
  body[data-view-mode="horizontal-short"] .grid > .photo-card,
  body[data-view-mode="horizontal-short"] .grid > .wallpaper-card,
  body[data-view-mode="horizontal-short"] .grid > .card--feature,
  body[data-view-mode="horizontal-short"] .grid > .card--cartoon {
    width: 100%;
  }

  /* HORIZONTAL-SHORT: same layout as horizontal but the excerpt is
     hidden, leaving only the image + title. Reuses every horizontal
     hide rule plus an extra .summary hide. Per Gaither 2026-05-16:
     "horizontal with pic and title only." */
  /* Keep .headline-hint visible per Gaither 2026-05-16: short cards need
     the cyan "Click here or title to expand" affordance because the
     headline-text click handler is unreliable on some build paths and
     the hint IS the expand button. */
  body[data-view-mode="horizontal-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) :is(
    .lr-section,
    .lr-row,
    .related-coverage-row,
    .related-row,
    .summary-mid,
    .deep-prose,
    .has-said-block,
    .action-row,
    .lean-meter,
    .prior-coverage-btn,
    .read-more-box
  ) {
    display: none;
  }
  body[data-view-mode="horizontal-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .card-bottom {
    display: none;
  }
  body[data-view-mode="horizontal-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .card-side {
    flex-basis: 200px;
    max-width: 200px;
  }
  body[data-view-mode="horizontal-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .preview {
    /* Per Gaither 2026-05-18: fit to width first, never crop top/bottom. */
    width: 100%;
    height: auto;
    max-height: 280px;
    object-fit: contain;
    object-position: 50% 50%;
    background: var(--subtle);
  }
  body[data-view-mode="horizontal-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) .headline {
    font-size: 18px;
    line-height: 1.25;
  }

  /* GRID-SHORT: 3-col tiles, image on top, title under image. NO
     excerpt. Same expand contract as grid. Per Gaither 2026-05-16:
     "like the card view but it only has pic and title under pic." */
  body[data-view-mode="grid-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) {
    display: flex; flex-direction: column;
  }
  body[data-view-mode="grid-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) .card-top {
    flex-direction: column;
  }
  body[data-view-mode="grid-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) .card-side {
    width: 100%;
    max-width: 100%;
    flex-basis: auto;
  }
  body[data-view-mode="grid-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) .preview {
    /* Per Gaither 2026-05-18: fit to width first, never crop top/bottom. */
    width: 100%;
    height: auto;
    max-height: 360px;
    object-fit: contain;
    object-position: 50% 50%;
    background: var(--subtle);
  }
  body[data-view-mode="grid-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) .body {
    padding: 16px 22px;
  }
  body[data-view-mode="grid-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch) .headline {
    font-size: 15.5px;
    line-height: 1.25;
    padding: 0;
    margin: 0;
  }
  /* Keep .headline-hint visible per Gaither 2026-05-16: short cards
     need the expand affordance. The hint IS the expand button in
     these compact modes. */
  body[data-view-mode="grid-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) :is(
    .lr-section,
    .lr-row,
    .related-coverage-row,
    .related-row,
    .summary-mid,
    .deep-prose,
    .has-said-block,
    .action-row,
    .lean-meter,
    .prior-coverage-btn,
    .read-more-box
  ) {
    display: none;
  }
  body[data-view-mode="grid-short"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .card-bottom {
    display: none;
  }
  body[data-view-mode="grid-short"] .grid > .photo-card,
  body[data-view-mode="grid-short"] .grid > .wallpaper-card,
  body[data-view-mode="grid-short"] .grid > .card--feature,
  body[data-view-mode="grid-short"] .grid > .card--cartoon {
    grid-column: 1 / -1;
  }
  body[data-view-mode="grid-short"] .grid > .card.expanded-state:not(.photo-card):not(.wallpaper-card):not(.card--feature):not(.card--cartoon) {
    grid-column: 1 / -1;
  }
  body[data-view-mode="grid"] .grid > .card:not(.card--feature):not(.card--cartoon):not(.photo-card):not(.wallpaper-card):not(.card--sudoku):not(.card--lesson):not(.card--wordle):not(.card--crossword):not(.card--wordsearch):not(.expanded-state) .summary {
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }

  /* When the user clicks the headline in horizontal/grid mode, the
     card receives .expanded-state. All the elements hidden above
     become visible again. Element order in the DOM puts source
     pills + share + L/R + category beneath the article body, which
     matches the user's request that those affordances go to the
     bottom of the expanded card. We also un-clamp the summary so
     the full neutral_summary reads. */
  body[data-view-mode="horizontal"] .card.expanded-state .summary,
  body[data-view-mode="grid"] .card.expanded-state .summary {
    display: block;
    -webkit-line-clamp: unset;
    overflow: visible;
  }

  /* ---- Hero photo + greeting (home page only) ----
     A full-width daily image with a soft time-of-day greeting overlay.
     The photo comes from NASA APOD (primary) or Bing IOTD (fallback)
     and is refreshed by the cron. Image is positioned with object-fit
     so portraits + landscapes both look right; gradient at bottom
     guarantees legible white text on any photo. */
  /* Hero matches the news-card box width exactly (it's the same flex
     parent), so it visually anchors to the card column rather than
     looking like an undersized banner above. aspect-ratio fixes the
     proportions to a wide-cinema 21:8 — drops to 16/9 at mobile so the
     hero still feels substantial on a narrow column. NO max-height,
     because max-height combined with aspect-ratio caps the WIDTH too,
     making the hero narrower than the cards below. */
  .hero { position: relative; overflow: hidden;
    border-radius: 12px;
    margin: 0 0 32px;
    width: 100%;
    aspect-ratio: 21/8;
    min-height: 320px; /* Defensive: even if width gets squeezed, hero stays visible. */
    background: var(--bg-alt);
    box-shadow: 0 2px 8px rgba(0,0,0,0.06);
    /* No cursor:zoom-in anymore — expand-photo moved to a dedicated
       .hero-zoom-btn (+) so the greeting name button is reachable.
       Per Gaither 2026-05-17. */
  }
  /* Hero shimmer placeholder. Per Gaither 2026-05-17 "add transition
     image before flickr so that it masks reaching out and waiting
     for the picture to load". Renders BEHIND .hero-img (which starts
     as a transparent 1x1 placeholder) until wxHero stamps a real
     data-wx-source on the img. Once that attribute lands, the
     shimmer fades out. */
  .hero::before {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 0;
    background:
      linear-gradient(135deg,
        rgba(1,204,255,0.18)   0%,
        rgba(1,204,255,0.06)  35%,
        rgba(60,60,80,0.18)   65%,
        rgba(1,204,255,0.20) 100%),
      linear-gradient(90deg,
        rgba(255,255,255,0)    0%,
        rgba(255,255,255,0.10) 40%,
        rgba(255,255,255,0.18) 50%,
        rgba(255,255,255,0.10) 60%,
        rgba(255,255,255,0)  100%);
    background-size: 100% 100%, 220% 100%;
    background-position: 0 0, -110% 0;
    animation: heroShimmerSlide 2.4s linear infinite;
    transition: opacity .35s ease;
    opacity: 1;
  }
  /* Once the wxHero pipeline has stamped data-wx-source on the img,
     fade out the shimmer over 350ms so the swap reads as 'image
     materializing' rather than 'flash + cut'. */
  .hero:has(.hero-img[data-wx-source])::before { opacity: 0; }
  [data-theme="dark"] .hero::before {
    background:
      linear-gradient(135deg,
        rgba(1,204,255,0.22)   0%,
        rgba(20,30,40,0.18)   35%,
        rgba(10,15,22,0.32)   65%,
        rgba(1,204,255,0.16) 100%),
      linear-gradient(90deg,
        rgba(255,255,255,0)    0%,
        rgba(255,255,255,0.06) 40%,
        rgba(255,255,255,0.12) 50%,
        rgba(255,255,255,0.06) 60%,
        rgba(255,255,255,0)  100%);
    background-size: 100% 100%, 220% 100%;
    background-position: 0 0, -110% 0;
  }
  @keyframes heroShimmerSlide {
    0%   { background-position: 0 0, -110% 0; }
    100% { background-position: 0 0, 210% 0; }
  }
  /* Hero img sits ABOVE the shimmer (z-index 1) so once Flickr loads
     it covers the gradient. Overlay stays above the img for the
     greeting/quote text. */
  .hero-img { z-index: 1; }
  .hero-overlay { z-index: 2; }
  /* Hero zoom-to-fullsize button. Mirrors the .hero-credit-toggle
     position/styling but on the opposite corner so the two icons
     read as a pair. */
  .hero > .hero-zoom-btn {
    position: absolute;
    top: 12px; right: 12px;
    width: 32px; height: 32px;
    display: inline-flex; align-items: center; justify-content: center;
    background: rgba(0,0,0,0.32); color: #fff;
    border: 0; border-radius: 50%;
    cursor: zoom-in; padding: 0;
    z-index: 4;
    opacity: 0.85;
    transition: opacity .15s, background .15s, transform .15s;
  }
  .hero > .hero-zoom-btn:hover,
  .hero > .hero-zoom-btn:focus-visible {
    opacity: 1; background: var(--cyan); outline: none;
    transform: scale(1.08);
  }
  .hero > .hero-zoom-btn svg { width: 16px; height: 16px; }
  /* The credit toggle was originally at top-right too; nudge it
     leftward 44px so the new zoom button can claim the corner. */
  .hero > .hero-credit-toggle {
    right: 56px !important;
  }
  .hero-img { position: absolute; inset: 0;
    width: 100%; height: 100%;
    /* Centered crop. Per Gaither 2026-05-17 "center hero photo
       vertically". Previously biased to 50% 25% to keep portrait
       subjects' faces visible, but Flickr text-search results
       (current wxHero pipeline) are mostly landscape weather photos
       where center crop reads better. */
    object-fit: cover; object-position: 50% 50%;
    display: block;
  }
  .hero-overlay { position: absolute; inset: 0;
    display: flex; flex-direction: column;
    justify-content: flex-end; align-items: flex-start;
    padding: 28px 32px;
    /* Stronger bottom-up gradient so the greeting stays legible on
       bright/light APOD images (galactic backgrounds, lunar surfaces).
       The 0.55-0.78 floor at the bottom is the difference between "I
       can read this" and "I'm squinting" on a typical pastel photo. */
    background: linear-gradient(180deg,
      rgba(0,0,0,0) 20%,
      rgba(0,0,0,0.55) 70%,
      rgba(0,0,0,0.78) 100%);
    color: #fff;
    /* No pointer-events:none anymore — that was disabling the
       greeting name button. Decorative text below uses individual
       pointer-events:none so the only clickable in the overlay is
       .hero-greeting-name. Per Gaither 2026-05-17 ("clicking on name
       still doesn't work...it's like there is a layer above it":
       this WAS the layer). */
  }
  /* Decorative overlay text gets pointer-events:none so accidental
     clicks fall through to where the user would expect (mostly
     nothing). Only the name button stays interactive. */
  .hero-section-label,
  .hero-date,
  .hero-quote { pointer-events: none; }
  .hero-greeting-name { pointer-events: auto; }
  /* "NEWS" / "RESEARCH" section label — top-left corner of the hero,
     white text, no pill background. Clean masthead-style label that
     identifies the section without competing with the greeting copy.
     Position absolute so it doesn't interact with the bottom-aligned
     greeting/date overlay. */
  .hero-section-label { position: absolute; top: 22px; left: 30px;
    font-family: var(--sans); font-size: 22px;
    letter-spacing: 0.34em; text-transform: uppercase; font-weight: 800;
    color: #fff;
    text-shadow: 0 1px 4px rgba(0,0,0,0.6);
    z-index: 2;
  }
  .hero-greeting { font-family: var(--serif); font-size: 40px;
    line-height: 1.1; font-weight: 600;
    margin: 0 0 6px;
    letter-spacing: -0.015em;
    text-shadow: 0 1px 3px rgba(0,0,0,0.35);
    color: #fff;
    display: flex; flex-direction: column; align-items: flex-start;
    /* Don't clip on narrow viewports — overlay shrinks the date and
       greeting at tablet widths but the greeting itself must always
       reflow cleanly instead of truncating mid-word. */
    overflow-wrap: break-word;
    max-width: 100%;
  }
  .hero-greeting-primary { display: block; }
  /* Click-to-edit name in the greeting. Inline button styled to read
     as part of the heading text but with a subtle dotted underline to
     hint clickability. Per Gaither 2026-05-17 "Good Morning, Gaither". */
  .hero-greeting-name {
    background: transparent;
    border: 0;
    padding: 0;
    margin: 0;
    color: inherit;
    font: inherit;
    cursor: pointer;
    border-bottom: 1px dashed rgba(255,255,255,0.55);
    transition: border-color .15s, color .15s;
  }
  .hero-greeting-name:hover,
  .hero-greeting-name:focus-visible {
    color: var(--cyan);
    border-bottom-color: var(--cyan);
    outline: none;
  }
  .hero-greeting-name.hero-greeting-name-empty {
    font-size: 0.7em;
    font-weight: 500;
    letter-spacing: 0.02em;
    opacity: 0.78;
    text-transform: uppercase;
    border-bottom-style: dotted;
  }
  /* English caption under non-English greetings. Sans, smaller, more
     muted so the primary script still leads. */
  .hero-greeting-translation {
    display: block;
    font-family: var(--sans); font-size: 13px; font-weight: 500;
    letter-spacing: 0.02em;
    color: rgba(255,255,255,0.78);
    text-shadow: 0 1px 2px rgba(0,0,0,0.42);
    margin-top: 4px; line-height: 1.3;
  }
  @media (max-width: 720px) {
    .hero-greeting-translation { font-size: 11.5px; }
  }
  .hero-date { font-family: var(--sans); font-size: 12px;
    margin: 0;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    opacity: 0.92;
    color: #fff;
    font-weight: 500;
    text-shadow: 0 1px 2px rgba(0,0,0,0.3);
  }
  /* Quote line below the date. Italic serif, soft white. Sized to sit
     as part of the hero composition. Extra-long quotes are cut from
     the pool itself (see ALL_QUOTES) rather than scaled down here. */
  .hero-quote { font-family: var(--serif); font-style: italic;
    font-size: 20px; font-weight: 400;
    margin: 14px 0 0; max-width: 86%;
    color: rgba(255,255,255,0.94);
    line-height: 1.36;
    text-shadow: 0 1px 3px rgba(0,0,0,0.5);
  }
  .hero-quote-attr { display: block; font-style: normal;
    font-family: var(--sans); font-size: 13px;
    letter-spacing: 0.04em;
    opacity: 0.78; margin-top: 5px;
  }
  @media (max-width: 720px) {
    .hero-quote { font-size: 15px; margin-top: 10px; }
    .hero-quote-attr { font-size: 11px; }
  }
  /* Image attribution converted to a small "eye" icon button in the
     bottom-right corner. On hover/focus the full title+credit slides
     out as a tooltip — keeps the hero clean of always-visible chrome. */
  /* Force absolute positioning with higher specificity than the later
     [data-tooltip] { position: relative } rule, which was overriding
     this and dropping the icon into the top-left of the hero. */
  .hero > .hero-credit-toggle,
  .photo-card > .hero-credit-toggle {
    position: absolute; bottom: 10px; right: 12px;
    width: 30px; height: 30px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: rgba(0,0,0,0.42);
    backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
    color: rgba(255,255,255,0.88);
    text-decoration: none;
    border: 1px solid rgba(255,255,255,0.12);
    pointer-events: auto;
    transition: background .15s, color .15s, border-color .15s;
    z-index: 3;
    top: auto; left: auto;
  }
  .hero > .hero-credit-toggle:hover,
  .hero > .hero-credit-toggle:focus-visible,
  .photo-card > .hero-credit-toggle:hover,
  .photo-card > .hero-credit-toggle:focus-visible {
    background: rgba(0,0,0,0.65); color: #fff;
    border-color: rgba(255,255,255,0.32);
    text-decoration: none;
  }
  .hero-credit-toggle svg { width: 16px; height: 16px; }
  /* Attribution tooltips can be long (full source + title + credit
     line). Override the default narrow nowrap tooltip with a wider,
     wrappable variant anchored to the icon's right edge so it never
     overflows the photograph's right edge and gets clipped by
     overflow:hidden on the photo-card. */
  .hero-credit-toggle::after {
    white-space: normal !important;
    max-width: 280px;
    width: max-content;
    text-align: left !important;
    left: auto !important;
    right: 0 !important;
    transform: translateY(2px) !important;
  }
  .hero-credit-toggle:hover::after,
  .hero-credit-toggle:focus-visible::after {
    transform: translateY(0) !important;
  }
  /* Download button on photo + wallpaper cards. Sits in the TOP RIGHT
     so it doesn't visually conflict with the bottom-right 'i' info
     button. Same glass-pill styling as the info toggle. Click triggers
     a fetch-as-blob then save-as via an anchor tag (see boot script). */
  /* Photo / wallpaper break card recency stamp. Bottom-left so it
     sits opposite the quote overlay (which floats centered + bottom).
     Same translucent dark pill treatment as the download button so
     the photo composition stays the dominant visual. Per Gaither
     2026-05-15. */
  .photo-card > .photo-card-updated {
    position: absolute; bottom: 10px; left: 12px;
    padding: 3px 9px; border-radius: 999px;
    background: rgba(0,0,0,0.42);
    backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
    color: rgba(255,255,255,0.85);
    border: 1px solid rgba(255,255,255,0.10);
    font-family: var(--sans);
    font-size: 10.5px;
    font-variant-numeric: tabular-nums;
    letter-spacing: 0.02em;
    pointer-events: none;
    z-index: 2;
  }
  .photo-card > .photo-download-btn {
    position: absolute; top: 10px; right: 12px;
    width: 30px; height: 30px; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: rgba(0,0,0,0.42);
    backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
    color: rgba(255,255,255,0.88);
    border: 1px solid rgba(255,255,255,0.12);
    cursor: pointer;
    pointer-events: auto;
    padding: 0;
    transition: background .15s, color .15s, border-color .15s;
    z-index: 3;
  }
  .photo-card > .photo-download-btn:hover,
  .photo-card > .photo-download-btn:focus-visible {
    background: rgba(0,0,0,0.65); color: #fff;
    border-color: rgba(255,255,255,0.32);
  }
  .photo-download-btn svg { width: 16px; height: 16px; }
  /* Tablet (721-1180px): the hero is wider than mobile but the side
     rails aren't shown yet, so the hero gets full-page width with
     generous padding. The greeting at 40px starts to clip at this
     range — drop to 34px so "Good afternoon." doesn't get truncated. */
  @media (min-width: 721px) and (max-width: 1180px) {
    .hero-greeting { font-size: 34px; }
    .hero-section-label { font-size: 14px; letter-spacing: 0.28em; }
  }
  @media (max-width: 720px) {
    .hero { aspect-ratio: 16/9;
      border-radius: 8px; margin-bottom: 24px; }
    .hero-overlay { padding: 20px 22px; }
    .hero-greeting { font-size: 26px; }
    .hero-date { font-size: 10.5px; letter-spacing: 0.14em; }
    .hero-section-label { font-size: 13px; letter-spacing: 0.26em;
      top: 14px; left: 18px; }
  }
  [data-theme="retro"] .hero { box-shadow: 0 2px 0 rgba(184,101,30,0.18),
    0 4px 14px rgba(184,101,30,0.10); }

  /* ---- Feed photo break cards ----
     Every Nth slot in the news grid is a whole-card photograph from the
     same APOD / Bing rotation that feeds the hero. Acts as a palate
     cleanser between news cards — same width as a news card, slightly
     shorter aspect so it doesn't dominate the column, soft gradient at
     the bottom for the caption overlay. */
  .photo-card { position: relative; overflow: hidden;
    border-radius: 12px;
    width: 100%;
    /* No aspect-ratio constraint. Per Gaither 2026-05-16: "I want to
       see full photos/wallpapers...max width and height so whole pic
       fits, max width and nothing cut off the top or bottom." Card
       height now follows the image's natural aspect ratio so the
       whole pic is always visible at column width. The earlier
       21/8 letterbox forced tall portrait shots to display tiny in
       the middle of the card. */
    background: var(--bg-alt);
    /* Glass/smoke rim instead of a flat --line border. Subtle white
       hairline + soft outer glow that picks up surrounding photo tones
       instead of looking like a hard outline. Per Gaither 2026-05-12. */
    border: 1px solid rgba(255,255,255,0.08);
    box-shadow:
      inset 0 0 0 1px rgba(255,255,255,0.04),
      0 2px 12px rgba(0,0,0,0.18),
      0 8px 28px rgba(0,0,0,0.08);
    display: block;
    text-decoration: none;
    color: #fff;
    cursor: zoom-in;
    transition: transform .15s ease, box-shadow .15s ease, border-color .15s;
  }
  [data-theme="light"] .photo-card {
    border: 1px solid rgba(255,255,255,0.6);
    box-shadow:
      inset 0 0 0 1px rgba(0,0,0,0.03),
      0 2px 12px rgba(0,0,0,0.08),
      0 8px 28px rgba(0,0,0,0.05);
  }
  .photo-card:hover { text-decoration: none; color: #fff;
    transform: translateY(-1px);
    border-color: var(--cyan);
    box-shadow: 0 2px 4px rgba(26,26,26,0.06), 0 8px 24px rgba(26,26,26,0.06);
  }
  .photo-card-img {
    /* Static-positioned so the card's height follows the image's
       natural aspect ratio. Per Gaither 2026-05-16 follow-up: "max
       width and height so whole pic fits, nothing cut off the top
       or bottom." The image flows at 100% width, height: auto, and
       the card resizes to match. No object-fit needed because the
       image isn't being squeezed into a fixed-size box anymore.
       max-height keeps absurdly tall portraits from dominating the
       column. */
    display: block;
    width: 100%;
    height: auto;
    max-height: 720px;
    object-fit: contain;
  }
  .photo-card-overlay { position: absolute; inset: 0;
    display: flex; flex-direction: column;
    justify-content: flex-end; align-items: flex-start;
    padding: 24px 28px;
    background: linear-gradient(180deg,
      rgba(0,0,0,0) 35%,
      rgba(0,0,0,0.55) 80%,
      rgba(0,0,0,0.78) 100%);
    pointer-events: none;
  }
  .photo-card-eyebrow { font-family: var(--sans); font-size: 10.5px;
    letter-spacing: 0.18em; text-transform: uppercase;
    color: rgba(255,255,255,0.92); font-weight: 600;
    margin: 0 0 6px;
    display: inline-flex; align-items: center; gap: 6px;
    text-shadow: 0 1px 2px rgba(0,0,0,0.4);
  }
  .photo-card-eyebrow::before { content: "\1F4F7\FE0E"; /* 📷 camera, text variant */
    font-size: 0.95em; opacity: 0.85; }
  .photo-card-title { font-family: var(--serif); font-size: 22px;
    line-height: 1.2; font-weight: 600;
    margin: 0 0 4px;
    max-width: 72%;
    color: #fff;
    text-shadow: 0 1px 3px rgba(0,0,0,0.45);
    letter-spacing: -0.01em;
  }
  .photo-card-source { font-family: var(--sans); font-size: 11px;
    color: rgba(255,255,255,0.78);
    margin: 0;
    text-shadow: 0 1px 2px rgba(0,0,0,0.35);
  }
  @media (max-width: 720px) {
    /* Mobile: keep the natural-aspect approach but tighten the border
       radius and overlay padding for the narrower viewport. Removed
       the 16/9 aspect-ratio override per Gaither 2026-05-16 — the
       whole image should be visible on mobile too. */
    .photo-card { border-radius: 8px; }
    .photo-card-overlay { padding: 18px 20px; }
    .photo-card-title { font-size: 17px; max-width: 88%; }
    .photo-card-eyebrow { font-size: 9.5px; letter-spacing: 0.16em; }
  }

  /* ---- Card: image LEFT (~220px), body RIGHT ---- */
  /* Card is now a vertical stack: card-top (image+side | article body)
     followed by card-bottom (L/R panels, layer buttons, related). The
     row flex now lives on card-top, not the card itself. */
  .card { background: var(--card-bg); border: 1px solid var(--line);
    border-radius: 12px; overflow: hidden; box-shadow: var(--card-shadow);
    display: flex; flex-direction: column;
    transition: border-color .15s, transform .15s, box-shadow .15s; }
  .card:hover { border-color: var(--cyan);
    box-shadow: 0 2px 4px rgba(26,26,26,0.06), 0 8px 24px rgba(26,26,26,0.06); }
  .card .card-top { display: flex; flex-direction: row; align-items: stretch; }
  .card .card-bottom { padding: 0 26px 22px; }
  /* L/R grid stays 50/50 with the new full-width parent. Slight extra
     margin-top so it doesn't hug the body's bottom. */
  .card .card-bottom .lr-grid { margin-top: 0; }
  /* Research details block (2026-06-10): structured study metadata on
     research cards. Compact inset box under the summary: type badge +
     labeled rows. Only renders when the extractor returned real
     fields, so no empty-box state is needed. */
  .research-meta { margin: 12px 0 2px; padding: 10px 14px;
    background: color-mix(in srgb, var(--cyan) 6%, var(--card-bg));
    border: 1px solid var(--line); border-left: 3px solid var(--cyan);
    border-radius: 8px; display: flex; flex-direction: column; gap: 6px; }
  .research-badge { align-self: flex-start; font-size: 11px;
    font-weight: 700; letter-spacing: 0.04em; text-transform: uppercase;
    color: var(--cyan); border: 1px solid var(--cyan);
    border-radius: 10px; padding: 1px 9px; }
  .research-row { display: flex; gap: 10px; font-size: 13.5px;
    line-height: 1.45; }
  .research-row .research-k { flex: 0 0 92px; font-size: 11px;
    font-weight: 700; letter-spacing: 0.03em; text-transform: uppercase;
    color: var(--muted); padding-top: 2px; }
  .research-row .research-v { flex: 1; color: var(--fg); }
  @media (max-width: 720px) {
    .research-row { flex-direction: column; gap: 1px; }
    .research-row .research-k { flex: none; }
  }
  /* Card side column: image on top, then sources, then share row. The
     stacked layout fills the vertical space under the picture instead
     of leaving a void next to long articles. padding-left aligns the
     row content with the image edge so sources/share don't visually
     "drift left" of the picture. Generous gap so sections breathe. */
  /* Inset on all sides — image, sources, share all sit away from the
     card edges instead of hugging them. Top padding adds breathing
     room above the image. Inter-section gap is moderate so labels
     and their content stay visually paired. */
  .card .card-side { width: 340px; flex-shrink: 0; align-self: stretch;
    margin: 0; padding: 22px 22px 14px 22px;
    display: flex; flex-direction: column; gap: 18px; }
  .card .card-side .sources-row { padding-top: 0; }
  .card .preview-wrap { margin: 0; display: flex; flex-direction: column;
    border-radius: 8px; overflow: hidden; }
  /* Image respects native aspect ratio — no forced crop. Portrait
     stays tall and narrow; landscape stays short and wide. Per
     Gaither 2026-05-17 'let card images be full height vertically
     and go to max width horizontally...no reason to cut off top of
     photos. Do this for all views'. object-fit:contain so the
     whole image is visible; max-height bumped 480 -> 720 so tall
     portraits can stretch up without being cropped. */
  .card .preview { width: 100%; height: auto; max-height: 720px;
    display: block; object-fit: contain; object-position: 50% 50%;
    background: var(--subtle);
    cursor: zoom-in;
    transition: opacity .15s; }
  .card .preview:hover { opacity: 0.92; }
  /* Per Gaither 2026-05-18 "so many research cards missing pics":
     when an event has no image, render a large category-icon tile in
     the image slot so the card still has a visual anchor. Used for
     research papers (arxiv, biorxiv) and essay-style news (Books,
     Opinion, History) where feeds rarely ship images. */
  .card .preview-wrap--no-image {
    width: 100%; aspect-ratio: 16 / 10;
    display: flex; align-items: center; justify-content: center;
    background: linear-gradient(135deg, var(--subtle), color-mix(in srgb, var(--cyan) 12%, var(--subtle)));
    border-radius: 8px;
  }
  .card .preview-cat-tile {
    width: 64px; height: 64px;
    color: var(--cyan);
    opacity: 0.85;
    display: flex; align-items: center; justify-content: center;
  }
  .card .preview-cat-tile svg {
    width: 100%; height: 100%;
    stroke-width: 1.5;
  }

  /* Lightbox: full-size view when a card image is clicked. */
  .image-lightbox { position: fixed; inset: 0; z-index: 200;
    background: rgba(0,0,0,0.88);
    display: flex; align-items: center; justify-content: center;
    padding: 32px; cursor: zoom-out;
    animation: fadeIn .15s ease; }
  .image-lightbox { cursor: zoom-out; }
  .image-lightbox img { max-width: 100%; max-height: 100%; cursor: zoom-out;
    width: auto; height: auto;
    object-fit: contain; display: block;
    border-radius: 4px;
    box-shadow: 0 10px 50px rgba(0,0,0,0.5); }
  .image-lightbox-close { position: absolute; top: 16px; right: 20px;
    background: rgba(0,0,0,0.5); border: 0; color: #fff;
    width: 40px; height: 40px; font-size: 26px; line-height: 1;
    border-radius: 50%; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    transition: background .15s; }
  .image-lightbox-close:hover { background: rgba(0,0,0,0.8); }
  .image-lightbox-caption { position: absolute; bottom: 24px; left: 0; right: 0;
    text-align: center; color: rgba(255,255,255,0.92);
    font-family: var(--sans); font-size: 13px; padding: 0 24px;
    text-shadow: 0 1px 3px rgba(0,0,0,0.6); }
  .image-lightbox-caption a { color: var(--cyan); margin-left: 8px;
    text-decoration: none; }
  .image-lightbox-caption a:hover { text-decoration: underline; }
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
  .card .photo-credit { font-size: 10.5px; color: var(--muted);
    padding: 4px 8px; line-height: 1.3;
    font-style: italic; letter-spacing: 0.01em;
    background: var(--subtle); }
  /* Sources + share rows live below the image in the side column. They
     drop their old top border / dashed divider since the column itself
     visually separates them from the body. */
  /* Source pills + share icons inside the wider 340px side column.
     Inter-item gap stays tight (8px) — the user does NOT want icons
     spread apart. Vertical breathing room comes from the column gap
     above + the label margin below. */
  .card .card-side .sources-row { margin: 0; padding: 0;
    border: 0; flex-wrap: wrap; gap: 8px; align-items: center; }
  .card .card-side .sources-row .sources-label { width: 100%;
    margin-bottom: 6px; font-size: 11px; }
  .card .card-side .source-pill { width: 44px; height: 44px; }
  .card .card-side .source-pill .favicon { width: 26px; height: 26px; }
  .card .card-side .source-pill .outlet-fallback { font-size: 13px; }
  .card .card-side .action-row { margin: 0; padding: 0; flex-wrap: wrap;
    align-items: center; gap: 8px; }
  .card .card-side .action-row .label { font-size: 11px;
    text-transform: uppercase; letter-spacing: 0.12em; color: var(--muted);
    font-weight: 600; width: 100%; margin-bottom: 6px; }
  .card .card-side .share-btn { width: 40px; height: 40px;
    font-size: 18px; }
  .card .card-side .share-btn svg { width: 18px; height: 18px; }
  .card .body { padding: 22px 26px 22px 22px; flex: 1; min-width: 0; }
  @media (max-width: 720px) {
    /* Mobile card stacks vertically. Reorder so the picture stays at
       top, the summary content comes right after, and the sources +
       share actions move to the bottom (under the read-more) — per
       Gaither 2026-05-12. */
    .card .card-top { flex-direction: column; }
    /* display:contents flattens .card-side so the picture / sources /
       share rows become direct children of card-top. Each then gets
       its own order — and the body slots in between picture and
       sources. */
    .card .card-side { display: contents; }
    .card .card-side .preview-wrap { order: 1; border-radius: 8px 8px 0 0; }
    .card .body { order: 2; padding: 18px 20px; }
    .card .card-side .sources-row { order: 3; padding: 12px 20px 4px;
      border-top: 1px solid var(--line); margin-top: 8px; }
    .card .card-side .action-row { order: 4; padding: 4px 20px 14px;
      flex-wrap: wrap; }
    /* Mobile: show full image height without zoom-crop. Book covers
       and tall portrait shots used to lose their top/bottom to the
       desktop "object-fit: cover; 50% 25%" crop on small screens.
       Use 'contain' so the native aspect ratio is preserved, and put
       a subtle tinted band behind so smaller images don't look
       orphaned. Bumped max-height because 'contain' on a portrait
       image now needs vertical room to actually breathe. */
    .card .preview-wrap { background: var(--subtle); align-items: center;
      justify-content: center; }
    .card .preview { max-height: 620px; width: auto; max-width: 100%;
      object-fit: contain; object-position: 50% 50%; margin: 0 auto; }
    .card .card-bottom { padding: 0 20px 18px; }
    main { padding: 20px 16px 40px; }
  }
  /* Tablet layout (721-1024px). Big picture on top, then Read-Original
     + Share actions as a thin strip under the image, then the body
     (category pills + headline + neutral summary) as its own full-width
     row below. Mirrors the mobile stacked layout but with a wider /
     taller image so the picture still feels like the lead element. */
  @media (min-width: 721px) and (max-width: 1024px) {
    .card .card-top { flex-direction: column; }
    .card .card-side { width: 100%; padding: 0; gap: 0;
      display: grid; grid-template-columns: 1fr;
      grid-template-areas:
        "preview"
        "sources"
        "action"; }
    .card .card-side .preview-wrap { grid-area: preview;
      border-radius: 8px 8px 0 0;
      /* Center small RSS images instead of stretching them across the
         full tablet card width (700-1000px wide). Many feed images are
         only 384-600px wide; forcing width:100% upscaled them 2-3x and
         made them look fuzzy. The tinted band behind keeps small images
         feeling intentional rather than orphaned. */
      align-items: center;
      background: var(--subtle);
      min-height: 220px; }
    .card .card-side .preview { width: auto; max-width: 100%;
      height: auto; max-height: 360px;
      object-fit: contain; object-position: 50% 50%; }
    .card .card-side .sources-row { grid-area: sources;
      padding: 12px 22px 0; flex-wrap: wrap; }
    .card .card-side .action-row  { grid-area: action;
      padding: 8px 22px 14px;
      border-bottom: 1px solid var(--line); }
    .card .body { padding: 18px 22px; }
    .card .card-bottom { padding: 0 22px 20px; }
  }

  /* Scroll-reveal: cards fade in as they enter view. Crucially,
     cards stay VISIBLE by default — only when JS adds .reveal do
     they go opacity:0. Without JS, cards render normally. Prevents
     the "empty page" appearance when JS is slow or blocked. */
  .card.reveal { opacity: 0; transform: translateY(10px);
    transition: opacity .32s ease-out, transform .32s ease-out; }
  .card.reveal.reveal-in { opacity: 1; transform: translateY(0); }
  @media (prefers-reduced-motion: reduce) {
    .card.reveal { opacity: 1; transform: none; transition: none; }
  }

  /* Tiny cyan affordance under the headline so the user knows
     clicking expands the summary. Hidden on permalink pages where
     the summary is already expanded by default. */
  .headline-hint { display: block; font-family: var(--sans);
    font-size: 11px; font-weight: 500; letter-spacing: 0.04em;
    color: var(--cyan); margin: 4px 0 8px; opacity: 0.85;
    cursor: pointer; transition: opacity .15s; }
  .headline-hint:hover { opacity: 1; }
  /* Once a card is in expanded state, the hint becomes "Click to collapse".
     Text flips based on data-noun ('summary' for AI-synthesized,
     'article' for raw-RSS singletons). */
  .card.expanded-state .headline-hint[data-noun="summary"]::before { content: "Click here or title to collapse summary"; }
  .card.expanded-state .headline-hint[data-noun="article"]::before { content: "Click here or title to collapse article"; }
  .card.expanded-state .headline-hint::before { content: "Click here or title to collapse summary"; }
  .card.expanded-state .headline-hint { font-size: 0; }
  .card.expanded-state .headline-hint::before { font-size: 11px; }
  /* The hint span is itself clickable — same gesture as clicking the
     title. Cursor + hover feedback so it doesn't read as decoration. */
  .headline-hint { cursor: pointer; }
  .headline-hint:hover, .headline-hint:focus-visible {
    color: var(--cyan); opacity: 1; outline: none; }
  .card:not(.permalink) .headline { cursor: pointer; }
  .card .headline { font-family: var(--serif); font-size: 24px; font-weight: 600;
    cursor: pointer;
    transition: color .15s;
    line-height: 1.22; margin: 0 0 10px; letter-spacing: -0.01em; color: var(--fg); }
  .card .headline:hover { color: var(--cyan); }
  .card .headline a { color: inherit; text-decoration: none; }
  .card .headline a:hover { color: var(--cyan); text-decoration: none; }
  @media (max-width: 600px) { .card .headline { font-size: 20px; } }
  .card .meta { font-size: 12px; color: var(--muted); display: flex;
    gap: 10px; margin-bottom: 14px; align-items: center; flex-wrap: wrap; }
  .pill { padding: 2px 10px; border-radius: 999px; border: 1px solid var(--line);
    font-size: 11px; font-weight: 500; }
  /* Category-color system: each category gets a distinctive hue, applied
     to its card pill, its filter-toggle track when ON, and any other
     places we surface the category name. CSS custom properties keep the
     hue + soft tint + border in sync per category. Each pill also gets
     a category-specific emoji prefix via ::before — matches the ☀
     "Good news" pill treatment, gives the eye a fast visual anchor
     before the text. */
  .pill.cat { color: var(--cat-color, var(--cyan));
    border-color: var(--cat-border, rgba(0,153,204,0.35));
    background: var(--cat-soft, var(--cyan-soft));
    font-weight: 600;
    display: inline-flex; align-items: center; gap: 5px; }
  .pill.cat .cat-glyph { display: inline-flex; align-items: center;
    color: var(--cat-color, var(--cyan));
    line-height: 1; }
  .pill.cat .cat-glyph svg { width: 13px; height: 13px; display: block; }
  /* Flat / monochrome category icons. Each glyph is followed by the
     U+FE0E variation selector (text presentation) so platforms that
     have an emoji renderer fall back to the line/glyph form instead
     of the colorful 3D emoji form. Where no good single-codepoint
     exists, prefer a flat dingbat / geometric shape over an emoji. */
  [data-cat="Politics"]      { --cat-icon: "\2696\FE0E "; }   /* ⚖ scales */
  [data-cat="Opinion"]       { --cat-icon: "\1F4AC\FE0E "; }  /* 💬 speech */
  [data-cat="World"]         { --cat-icon: "\1F30D\FE0E "; }  /* 🌍 globe (text variant) */
  [data-cat="Business"]      { --cat-icon: "\25A4\FE0E "; }   /* ▤ horizontal-fill */
  [data-cat="Technology"]    { --cat-icon: "\2318\FE0E "; }   /* ⌘ command */
  [data-cat="Science"]       { --cat-icon: "\269B\FE0E "; }   /* ⚛ atom */
  [data-cat="General"]       { --cat-icon: "\1F52C\FE0E "; }  /* 🔬 microscope */
  [data-cat="Health"]        { --cat-icon: "\271A\FE0E "; }   /* ✚ heavy cross */
  [data-cat="Culture"]       { --cat-icon: "\2746\FE0E "; }   /* ✆ wait, ✆? use ❉ -> 2749 */
  [data-cat="Entertainment"] { --cat-icon: "\25B6\FE0E "; }   /* ▶ play */
  [data-cat="Sports"]        { --cat-icon: "\25EF\FE0E "; }   /* ◯ circle */
  [data-cat="Good news"]     { --cat-icon: "\2600\FE0E "; }   /* ☀ sun (text variant) */
  [data-cat="Photos"]        { --cat-icon: "\1F4F7\FE0E "; }  /* 📷 camera */
  /* Research branches — disambiguating glyphs. Atom is reserved for Physics
     (was previously Science); Science legacy still uses atom because it
     remains a research-side fallback. */
  [data-cat="Astronomy"]     { --cat-icon: "\2606\FE0E "; }   /* ☆ white star */
  [data-cat="Biology"]       { --cat-icon: "\2740\FE0E "; }   /* ❀ flower */
  [data-cat="Chemistry"]     { --cat-icon: "\2697\FE0E "; }   /* ⚗ alembic */
  [data-cat="Climate"]       { --cat-icon: "\1F30E\FE0E "; }  /* 🌎 earth */
  [data-cat="Computing"]     { --cat-icon: "\2328\FE0E "; }   /* ⌨ keyboard */
  [data-cat="Engineering"]   { --cat-icon: "\2699\FE0E "; }   /* ⚙ gear */
  [data-cat="Mathematics"]   { --cat-icon: "\2211\FE0E "; }   /* ∑ summation */
  [data-cat="Medicine"]      { --cat-icon: "\2695\FE0E "; }   /* ⚕ medical */
  [data-cat="Neuroscience"]  { --cat-icon: "\274B\FE0E "; }   /* ❋ eight-spoke asterisk */
  [data-cat="Physics"]       { --cat-icon: "\269B\FE0E "; }   /* ⚛ atom */
  [data-cat="Psychology"]    { --cat-icon: "\03C8\FE0E "; }   /* ψ psi */
  [data-cat="Politics"]      { --cat-color: #c11577; --cat-border: rgba(193,21,119,0.35); --cat-soft: rgba(193,21,119,0.10); }
  [data-cat="Opinion"]       { --cat-color: #7a4a9b; --cat-border: rgba(122,74,155,0.35); --cat-soft: rgba(122,74,155,0.10); }
  [data-cat="World"]         { --cat-color: #2f6fbb; --cat-border: rgba(47,111,187,0.35);  --cat-soft: rgba(47,111,187,0.10);  }
  [data-cat="Business"]      { --cat-color: #1a8754; --cat-border: rgba(26,135,84,0.35);  --cat-soft: rgba(26,135,84,0.10);  }
  [data-cat="Technology"]    { --cat-color: #6b00c2; --cat-border: rgba(107,0,194,0.40);  --cat-soft: rgba(107,0,194,0.10);  }
  [data-cat="Science"]       { --cat-color: #0d9488; --cat-border: rgba(13,148,136,0.40); --cat-soft: rgba(13,148,136,0.10); }
  [data-cat="General"]       { --cat-color: #14b8a6; --cat-border: rgba(20,184,166,0.40); --cat-soft: rgba(20,184,166,0.10); }
  [data-cat="Health"]        { --cat-color: #b8451f; --cat-border: rgba(184,69,31,0.35);  --cat-soft: rgba(184,69,31,0.10);  }
  [data-cat="Culture"]       { --cat-color: #a87c26; --cat-border: rgba(168,124,38,0.40); --cat-soft: rgba(168,124,38,0.10); }
  [data-cat="Entertainment"] { --cat-color: #ec4899; --cat-border: rgba(236,72,153,0.40); --cat-soft: rgba(236,72,153,0.10); }
  [data-cat="Sports"]        { --cat-color: #059669; --cat-border: rgba(5,150,105,0.40);  --cat-soft: rgba(5,150,105,0.10);  }
  [data-cat="Good news"]     { --cat-color: #b58400; --cat-border: rgba(244,180,0,0.45);   --cat-soft: rgba(244,180,0,0.12);   }
  /* Photos toggle / pill: light warm grey so it reads as "neutral break"
     rather than a topical category. Distinct from --muted so the toggle
     state is still visible. */
  [data-cat="Photos"]        { --cat-color: #94a3b8; --cat-border: rgba(148,163,184,0.40); --cat-soft: rgba(148,163,184,0.10); }
  [data-cat="DIY"]           { --cat-color: #8b5a2b; --cat-border: rgba(139,90,43,0.45);   --cat-soft: rgba(139,90,43,0.12);   }
  /* This Day — cream-gold to match the feature card's wireframe accent. */
  [data-cat="This Day"]      { --cat-color: #c8a96a; --cat-border: rgba(200,169,106,0.45); --cat-soft: rgba(200,169,106,0.12); }
  /* Cartoons — playful coral so the chip reads as the "lighter" bucket. */
  [data-cat="Cartoons"]      { --cat-color: #f97316; --cat-border: rgba(249,115,22,0.40);  --cat-soft: rgba(249,115,22,0.10);  }
  /* Almanac palette (2026-06-23): a distinct hue per category so the
     color-coded sidebar glyphs and pills are scannable. Brightened for
     legibility on the dark theme. */
  [data-cat="On This Day"]       { --cat-color: #cc9a3a; --cat-border: rgba(204,154,58,0.40);  --cat-soft: rgba(204,154,58,0.10); }
  [data-cat="Art & Culture"]     { --cat-color: #d07a45; --cat-border: rgba(208,122,69,0.40);  --cat-soft: rgba(208,122,69,0.10); }
  [data-cat="Comics"]            { --cat-color: #f4793a; --cat-border: rgba(244,121,58,0.40);  --cat-soft: rgba(244,121,58,0.10); }
  [data-cat="Food Stories"]      { --cat-color: #e0605b; --cat-border: rgba(224,96,91,0.40);   --cat-soft: rgba(224,96,91,0.10); }
  [data-cat="History & Mystery"] { --cat-color: #7f8fc4; --cat-border: rgba(127,143,196,0.40); --cat-soft: rgba(127,143,196,0.10); }
  [data-cat="How Things Work"]   { --cat-color: #3f9ec0; --cat-border: rgba(63,158,192,0.40);  --cat-soft: rgba(63,158,192,0.10); }
  [data-cat="Jokes & Riddles"]   { --cat-color: #e0b22e; --cat-border: rgba(224,178,46,0.42);  --cat-soft: rgba(224,178,46,0.12); }
  [data-cat="Math & Logic"]      { --cat-color: #8f72f0; --cat-border: rgba(143,114,240,0.40); --cat-soft: rgba(143,114,240,0.10); }
  [data-cat="Nature & Animals"]  { --cat-color: #36ab66; --cat-border: rgba(54,171,102,0.40);  --cat-soft: rgba(54,171,102,0.10); }
  [data-cat="Remarkable People"] { --cat-color: #d0628c; --cat-border: rgba(208,98,140,0.40);  --cat-soft: rgba(208,98,140,0.10); }
  [data-cat="Space & Physics"]   { --cat-color: #5f84e8; --cat-border: rgba(95,132,232,0.40);  --cat-soft: rgba(95,132,232,0.10); }
  [data-cat="Strange Places"]    { --cat-color: #20b3aa; --cat-border: rgba(32,179,170,0.40);  --cat-soft: rgba(32,179,170,0.10); }
  [data-cat="Sudoku"]            { --cat-color: #6f93c8; --cat-border: rgba(111,147,200,0.40); --cat-soft: rgba(111,147,200,0.10); }
  [data-cat="The Mind"]          { --cat-color: #c069dd; --cat-border: rgba(192,105,221,0.40); --cat-soft: rgba(192,105,221,0.10); }
  [data-cat="True Crime"]        { --cat-color: #cd5266; --cat-border: rgba(205,82,102,0.40);  --cat-soft: rgba(205,82,102,0.10); }
  [data-cat="Words & Language"]  { --cat-color: #5588da; --cat-border: rgba(85,136,218,0.40);  --cat-soft: rgba(85,136,218,0.10); }
  [data-cat="Wordle"]            { --cat-color: #4fb89a; --cat-border: rgba(79,184,154,0.40);  --cat-soft: rgba(79,184,154,0.10); }
  [data-cat="Crossword"]         { --cat-color: #a98bd8; --cat-border: rgba(169,139,216,0.40); --cat-soft: rgba(169,139,216,0.10); }
  [data-cat="Word Search"]       { --cat-color: #d98f5a; --cat-border: rgba(217,143,90,0.40);  --cat-soft: rgba(217,143,90,0.10); }
  [data-cat="Did You Know"]      { --cat-color: #46b3b0; --cat-border: rgba(70,179,176,0.40);  --cat-soft: rgba(70,179,176,0.10); }
  [data-cat="Stories"]           { --cat-color: #c69552; --cat-border: rgba(198,149,82,0.40);  --cat-soft: rgba(198,149,82,0.10); }
  /* New Reader's-Digest / Farmer's-Almanac sections (Gaither 2026-06-23). */
  [data-cat="Strange But True"]     { --cat-color: #e08a2e; --cat-border: rgba(224,138,46,0.42);  --cat-soft: rgba(224,138,46,0.12); }
  [data-cat="Drama in Real Life"]   { --cat-color: #cf5648; --cat-border: rgba(207,86,72,0.42);   --cat-soft: rgba(207,86,72,0.12); }
  [data-cat="Whatever Happened To"] { --cat-color: #5a9bb5; --cat-border: rgba(90,155,181,0.42);  --cat-soft: rgba(90,155,181,0.12); }
  [data-cat="Roadside America"]     { --cat-color: #c2546e; --cat-border: rgba(194,84,110,0.42);  --cat-soft: rgba(194,84,110,0.12); }
  [data-cat="This Week in the Sky"] { --cat-color: #6a74d8; --cat-border: rgba(106,116,216,0.42); --cat-soft: rgba(106,116,216,0.12); }
  /* Research-branch palette. Each branch a recognizable hue (deep-blue
     for astronomy, leaf-green for biology, etc.) so the research feed
     reads as a discipline-coded mosaic the same way news reads as a
     topic-coded one. */
  [data-cat="Astronomy"]     { --cat-color: #3b4ec5; --cat-border: rgba(59,78,197,0.40);   --cat-soft: rgba(59,78,197,0.10);   }
  [data-cat="Biology"]       { --cat-color: #2d8c4a; --cat-border: rgba(45,140,74,0.40);   --cat-soft: rgba(45,140,74,0.10);   }
  [data-cat="Chemistry"]     { --cat-color: #d97706; --cat-border: rgba(217,119,6,0.40);   --cat-soft: rgba(217,119,6,0.10);   }
  [data-cat="Climate"]       { --cat-color: #0d9488; --cat-border: rgba(13,148,136,0.40);  --cat-soft: rgba(13,148,136,0.10);  }
  [data-cat="Computing"]     { --cat-color: #7c3aed; --cat-border: rgba(124,58,237,0.40);  --cat-soft: rgba(124,58,237,0.10);  }
  [data-cat="Engineering"]   { --cat-color: #475569; --cat-border: rgba(71,85,105,0.40);   --cat-soft: rgba(71,85,105,0.10);   }
  [data-cat="Mathematics"]   { --cat-color: #64748b; --cat-border: rgba(100,116,139,0.40); --cat-soft: rgba(100,116,139,0.10); }
  [data-cat="Medicine"]      { --cat-color: #c2410c; --cat-border: rgba(194,65,12,0.40);   --cat-soft: rgba(194,65,12,0.10);   }
  [data-cat="Neuroscience"]  { --cat-color: #db2777; --cat-border: rgba(219,39,119,0.40);  --cat-soft: rgba(219,39,119,0.10);  }
  [data-cat="Physics"]       { --cat-color: #1d4ed8; --cat-border: rgba(29,78,216,0.40);   --cat-soft: rgba(29,78,216,0.10);   }
  [data-cat="Psychology"]    { --cat-color: #be185d; --cat-border: rgba(190,24,93,0.40);   --cat-soft: rgba(190,24,93,0.10);   }
  /* Dark mode: lift each hue a touch so it pops on the midnight bg. */
  [data-theme="dark"] [data-cat="Politics"]      { --cat-color: #f06bb9; --cat-soft: rgba(240,107,185,0.12); --cat-border: rgba(240,107,185,0.40); }
  [data-theme="dark"] [data-cat="Opinion"]       { --cat-color: #c89aea; --cat-soft: rgba(200,154,234,0.12); --cat-border: rgba(200,154,234,0.40); }
  [data-theme="dark"] [data-cat="World"]         { --cat-color: #7eaad5; --cat-soft: rgba(126,170,213,0.12); --cat-border: rgba(126,170,213,0.40); }
  [data-theme="dark"] [data-cat="Business"]      { --cat-color: #5ec88c; --cat-soft: rgba(94,200,140,0.12);  --cat-border: rgba(94,200,140,0.40); }
  [data-theme="dark"] [data-cat="Technology"]    { --cat-color: #b384f0; --cat-soft: rgba(179,132,240,0.12); --cat-border: rgba(179,132,240,0.40); }
  [data-theme="dark"] [data-cat="Science"]       { --cat-color: #5eead4; --cat-soft: rgba(94,234,212,0.12);  --cat-border: rgba(94,234,212,0.40); }
  [data-theme="dark"] [data-cat="General"]       { --cat-color: #5eead4; --cat-soft: rgba(94,234,212,0.12);  --cat-border: rgba(94,234,212,0.40); }
  [data-theme="dark"] [data-cat="Health"]        { --cat-color: #f08e60; --cat-soft: rgba(240,142,96,0.12);  --cat-border: rgba(240,142,96,0.40); }
  [data-theme="dark"] [data-cat="Culture"]       { --cat-color: #e0b566; --cat-soft: rgba(224,181,102,0.12); --cat-border: rgba(224,181,102,0.40); }
  [data-theme="dark"] [data-cat="Entertainment"] { --cat-color: #f9a8d4; --cat-soft: rgba(249,168,212,0.12); --cat-border: rgba(249,168,212,0.40); }
  [data-theme="dark"] [data-cat="Sports"]        { --cat-color: #34d399; --cat-soft: rgba(52,211,153,0.12);  --cat-border: rgba(52,211,153,0.40); }
  [data-theme="dark"] [data-cat="Good news"]     { --cat-color: #ffd24a; --cat-soft: rgba(255,210,74,0.12);  --cat-border: rgba(255,210,74,0.40); }
  [data-theme="dark"] [data-cat="Photos"]        { --cat-color: #e5e7eb; --cat-soft: rgba(229,231,235,0.10); --cat-border: rgba(229,231,235,0.30); }
  [data-theme="dark"] [data-cat="DIY"]           { --cat-color: #d4a574; --cat-soft: rgba(212,165,116,0.12); --cat-border: rgba(212,165,116,0.40); }
  [data-theme="dark"] [data-cat="Astronomy"]     { --cat-color: #93a8f0; --cat-soft: rgba(147,168,240,0.12); --cat-border: rgba(147,168,240,0.40); }
  [data-theme="dark"] [data-cat="Biology"]       { --cat-color: #6dd089; --cat-soft: rgba(109,208,137,0.12); --cat-border: rgba(109,208,137,0.40); }
  [data-theme="dark"] [data-cat="Chemistry"]     { --cat-color: #fbbf24; --cat-soft: rgba(251,191,36,0.12);  --cat-border: rgba(251,191,36,0.40); }
  [data-theme="dark"] [data-cat="Climate"]       { --cat-color: #5eead4; --cat-soft: rgba(94,234,212,0.12);  --cat-border: rgba(94,234,212,0.40); }
  [data-theme="dark"] [data-cat="Computing"]     { --cat-color: #b384f0; --cat-soft: rgba(179,132,240,0.12); --cat-border: rgba(179,132,240,0.40); }
  [data-theme="dark"] [data-cat="Engineering"]   { --cat-color: #cbd5e1; --cat-soft: rgba(203,213,225,0.10); --cat-border: rgba(203,213,225,0.30); }
  [data-theme="dark"] [data-cat="Mathematics"]   { --cat-color: #94a3b8; --cat-soft: rgba(148,163,184,0.10); --cat-border: rgba(148,163,184,0.30); }
  [data-theme="dark"] [data-cat="Medicine"]      { --cat-color: #fb923c; --cat-soft: rgba(251,146,60,0.12);  --cat-border: rgba(251,146,60,0.40); }
  [data-theme="dark"] [data-cat="Neuroscience"]  { --cat-color: #f472b6; --cat-soft: rgba(244,114,182,0.12); --cat-border: rgba(244,114,182,0.40); }
  [data-theme="dark"] [data-cat="Physics"]       { --cat-color: #93c5fd; --cat-soft: rgba(147,197,253,0.12); --cat-border: rgba(147,197,253,0.40); }
  [data-theme="dark"] [data-cat="Psychology"]    { --cat-color: #f9a8d4; --cat-soft: rgba(249,168,212,0.12); --cat-border: rgba(249,168,212,0.40); }
  .view-pill { font-size: 11px; color: var(--muted); }
  .date-pill { font-size: 11px; color: var(--muted); }

  /* Read more: cyan pill button styled exactly like the "How did we get
     here?" layer-btn — same border, radius, padding, hover. The leading
     sparkle SVG signals "this content is AI-generated" without needing
     a label. Keeps visual rhythm consistent across the analysis-action
     row at the bottom of every card. */
  .read-more-btn { background: transparent;
    border: 1px solid rgba(0,153,204,0.4);
    color: var(--cyan); padding: 8px 14px; border-radius: 8px;
    font-family: inherit; font-size: 13px; font-weight: 500;
    cursor: pointer;
    display: inline-flex; align-items: center; gap: 8px;
    margin-top: 6px;
    transition: border-color .15s, background .15s, color .15s; }
  .read-more-btn:hover { border-color: var(--cyan); background: var(--cyan-soft); }
  .read-more-btn .ai-glyph { display: inline-flex; width: 14px; height: 14px; flex-shrink: 0; }
  .read-more-btn .ai-glyph svg { width: 14px; height: 14px; display: block; }
  .read-more-btn[data-loaded="true"] { display: none; }
  .read-more-box { display: none; margin-top: 6px; padding-top: 12px;
    border-top: 1px solid var(--line);
    font-size: 15px; line-height: 1.6; color: var(--fg); }
  .read-more-box[data-visible="true"] { display: block; }
  .read-more-box p { margin: 0 0 10px; }
  .read-more-box .read-more-loading { color: var(--muted); font-style: italic;
    display: flex; flex-direction: column; align-items: center; gap: 12px;
    padding: 14px 0; }

  /* Lean meter */
  .meter { height: 5px; background: var(--line); border-radius: 3px;
    position: relative; margin-top: 12px; max-width: 280px; }
  .meter::before { content: ""; position: absolute; top: -2px; bottom: -2px;
    left: 50%; width: 1px; background: var(--muted); }
  .meter .marker { position: absolute; top: -3px; width: 11px; height: 11px;
    border-radius: 50%; background: var(--cyan); transform: translateX(-50%);
    box-shadow: 0 0 0 2px var(--card-bg); }
  .meter-labels { display: flex; justify-content: space-between;
    font-size: 10px; color: var(--muted); margin-top: 4px;
    text-transform: uppercase; letter-spacing: 0.08em; max-width: 280px; }

  /* Neutral summary block */
  .expanded { margin-top: 14px; font-size: 15px; color: var(--fg); line-height: 1.6; }
  .expanded h4 { font-size: 11px; text-transform: uppercase; letter-spacing: 0.12em;
    color: var(--muted); margin: 16px 0 6px; font-weight: 600; }
  .expanded p { margin: 0 0 8px; }

  /* Every card gets a left-edge stripe in its category color, like the
     gold Good-news stripe. The category color matches the toggle/pill
     palette so the page reads as a single color system at a glance. */
  /* Thinner (3px) + slightly muted stripes — the 4px saturated bars
     were reading as decorative noise rather than category cues. Each
     hue is desaturated ~15% from the pill version so the eye gets
     the signal without the carnival. */
  .card[data-category="Politics"]      { border-left: 3px solid #a83d80; }
  .card[data-category="Opinion"]       { border-left: 3px solid #7a4a9b; }
  .card[data-category="World"]         { border-left: 3px solid #5482b8; }
  .card[data-category="Business"]      { border-left: 3px solid #3e9268; }
  .card[data-category="Technology"]    { border-left: 3px solid #7b3eb5; }
  .card[data-category="Science"]       { border-left: 3px solid #3a9489; }
  .card[data-category="General"]       { border-left: 3px solid #14b8a6; }
  .card[data-category="Health"]        { border-left: 3px solid #b1593a; }
  .card[data-category="Culture"]       { border-left: 3px solid #a18142; }
  .card[data-category="Entertainment"] { border-left: 3px solid #d96fa1; }
  .card[data-category="Sports"]        { border-left: 3px solid #3b9275; }
  .card[data-category="Good news"]     { border-left: 3px solid #d4a83a; }
  .card[data-category="DIY"]           { border-left: 3px solid #8b5a2b; }
  .card[data-category="Photos"],
  .photo-card                          { border-left: 3px solid #94a3b8; }
  /* Hash-anchor highlight: card the user just jumped to from a rail
     click gets a brief cyan glow + scale-up so the landing is obvious. */
  .card[data-just-arrived="true"] {
    box-shadow: 0 0 0 3px var(--cyan), 0 8px 24px rgba(0,153,204,0.18);
    transition: box-shadow .4s ease;
  }
  .card[data-category="Astronomy"]     { border-left: 3px solid #3b4ec5; }
  .card[data-category="Biology"]       { border-left: 3px solid #2d8c4a; }
  .card[data-category="Chemistry"]     { border-left: 3px solid #d97706; }
  .card[data-category="Climate"]       { border-left: 3px solid #0d9488; }
  .card[data-category="Computing"]     { border-left: 3px solid #7c3aed; }
  .card[data-category="Engineering"]   { border-left: 3px solid #475569; }
  .card[data-category="Mathematics"]   { border-left: 3px solid #64748b; }
  .card[data-category="Medicine"]      { border-left: 3px solid #c2410c; }
  .card[data-category="Neuroscience"]  { border-left: 3px solid #db2777; }
  .card[data-category="Physics"]       { border-left: 3px solid #1d4ed8; }
  .card[data-category="Psychology"]    { border-left: 3px solid #be185d; }
  [data-theme="dark"] .card[data-category="Politics"]      { border-left-color: #d488b6; }
  [data-theme="dark"] .card[data-category="Opinion"]       { border-left-color: #c89aea; }
  [data-theme="dark"] .card[data-category="World"]         { border-left-color: #8eb4d9; }
  [data-theme="dark"] .card[data-category="Business"]      { border-left-color: #82c7a3; }
  [data-theme="dark"] .card[data-category="Technology"]    { border-left-color: #b69ad9; }
  [data-theme="dark"] .card[data-category="Science"]       { border-left-color: #82ddca; }
  [data-theme="dark"] .card[data-category="General"]       { border-left-color: #82ddca; }
  [data-theme="dark"] .card[data-category="Health"]        { border-left-color: #de9b7a; }
  [data-theme="dark"] .card[data-category="Culture"]       { border-left-color: #d4b885; }
  [data-theme="dark"] .card[data-category="Entertainment"] { border-left-color: #e9b4ce; }
  [data-theme="dark"] .card[data-category="Sports"]        { border-left-color: #6bc4a3; }
  [data-theme="dark"] .card[data-category="Good news"]     { border-left-color: #e8c477; }
  [data-theme="dark"] .card[data-category="DIY"]           { border-left-color: #d4a574; }
  [data-theme="dark"] .card[data-category="Photos"],
  [data-theme="dark"] .photo-card                          { border-left-color: #e5e7eb; }

  /* Positive-tone card — warm border accent + soft surface tint.
     Visually distinguishable at a glance from normal news cards, but
     not loud. The "Good news" pill in the meta row does the heavy
     lifting; the surface tint is just a subtle "this is different". */
  .card--positive { border-left: 4px solid #f4b400; }
  .card--positive .preview-wrap { background: rgba(244,180,0,0.04); }

  /* ---- Daily feature card (rotating birthdays / on-this-day / etc.) ----
     Theme-aware per Gaither 2026-05-14: feature cards take their colors
     from the page theme so dark mode reads as dark and light reads as
     light, while staying visually distinct from regular news cards via
     the cream-gold accent stripe + wireframe watermark. */
  .card--feature {
    border-left: 4px solid #c8a96a;
    background-color: var(--card-bg);
    background-image: linear-gradient(180deg, rgba(200,169,106,0.10), rgba(200,169,106,0.03));
    background-repeat: no-repeat;
    background-position: right -20px center;
    background-size: 220px 220px;
    color: var(--fg);
  }
  .card--feature .card-top { padding: 18px 20px; padding-right: 32px; }
  .card--feature .body { width: 100%; position: relative; z-index: 2; }
  /* Cream-gold pill that reads against any theme's --card-bg without
     inverting to the old dark-on-white look. Border + slight tint
     gives it presence; the text stays the theme's foreground color. */
  .card--feature .feature-pill {
    background: rgba(200,169,106,0.20);
    color: var(--fg);
    border: 1px solid rgba(200,169,106,0.55);
    font-weight: 600; letter-spacing: 0.03em;
    padding: 3px 10px; text-transform: uppercase;
    font-size: 11px;
  }
  .card--feature .feature-pill .cat-glyph { display: none; }
  /* Date / "updated" pill on feature cards. The site default
     (.pill.date-pill) uses var(--subtle) bg + var(--muted) text,
     which read as gray-on-gray on the feature card's tinted
     background. Bump contrast so the timestamp is legible. */
  .card--feature .meta .date-pill {
    background: rgba(200,169,106,0.10);
    color: var(--fg);
    border: 1px solid rgba(200,169,106,0.30);
    opacity: 0.85;
  }
  .card--feature .feature-headline {
    font-size: 1.18rem; line-height: 1.35;
    margin: 10px 0 8px 0;
    font-weight: 700;
    color: var(--fg);
    cursor: default;
  }
  .card--feature .feature-body {
    font-size: 0.97rem; line-height: 1.55;
    color: var(--fg); opacity: 0.92;
    margin: 0;
    max-width: 88%;
  }
  .card--feature .feature-body p { margin: 0 0 8px 0; }
  .card--feature .feature-body p:last-child { margin-bottom: 0; }

  /* Daily trivia: question is the headline; answer + explanation live
     behind a native <details> reveal. Click-to-reveal pattern, no JS. */
  .card--trivia .trivia-question {
    font-style: italic;
  }
  .card--feature .trivia-reveal {
    margin: 0;
    border: 1px dashed var(--line);
    border-radius: 6px;
    background: rgba(1, 204, 255, 0.04);
    padding: 0;
  }
  .card--feature .trivia-reveal-toggle {
    list-style: none;
    cursor: pointer;
    padding: 10px 14px;
    font-family: var(--sans);
    font-size: 0.9rem;
    font-weight: 600;
    color: var(--cyan);
    user-select: none;
    display: flex;
    align-items: center;
    gap: 8px;
  }
  .card--feature .trivia-reveal-toggle::-webkit-details-marker { display: none; }
  .card--feature .trivia-reveal-toggle::before {
    content: "▶";
    font-size: 0.7em;
    transition: transform 0.15s ease;
  }
  .card--feature .trivia-reveal[open] .trivia-reveal-toggle::before {
    transform: rotate(90deg);
  }
  .card--feature .trivia-reveal[open] .trivia-reveal-toggle {
    color: var(--fg);
    border-bottom: 1px dashed var(--line);
  }
  .card--feature .trivia-answer {
    padding: 12px 14px 4px;
    font-family: var(--serif);
    font-size: 1.05rem;
    color: var(--fg);
  }
  .card--feature .trivia-answer strong {
    color: var(--cyan);
    margin-right: 4px;
  }
  .card--feature .trivia-explanation {
    padding: 4px 14px 12px;
    font-size: 0.93rem;
    color: var(--fg);
    opacity: 0.88;
  }
  .card--feature .trivia-explanation p { margin: 0 0 6px 0; }
  .card--feature .trivia-explanation p:last-child { margin-bottom: 0; }
  .card--feature .trivia-explanation a {
    color: var(--cyan);
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
  }
  .card--feature .feature-body strong { color: var(--fg); font-weight: 700; opacity: 1; }
  .card--feature .feature-body em { font-style: italic; }
  .card--feature .feature-body a { color: var(--cyan); text-decoration: underline;
    text-decoration-thickness: 1px; text-underline-offset: 2px;
    transition: color .12s; }
  .card--feature .feature-body a:hover { color: var(--fg); }
  /* "Learn more →" link — inline, subtle cyan text + arrow. No pill
     background per Gaither 2026-05-14; reads as a link, not a button. */
  .card--feature .feature-body a.feature-learn-more {
    color: var(--cyan);
    text-decoration: none;
    border: 0;
    background: transparent;
    padding: 0;
    margin: 0 0 0 2px;
    /* Slightly smaller than the surrounding body copy per Gaither
       2026-05-14 — keeps the link recessive instead of competing
       with the prose. */
    font-size: 0.85em;
    font-weight: 500;
    white-space: nowrap;
    transition: color .12s, opacity .12s;
    opacity: 0.85;
  }
  .card--feature .feature-body a.feature-learn-more:hover {
    text-decoration: underline;
    text-decoration-thickness: 1px;
    text-underline-offset: 2px;
    opacity: 1;
  }
  .card--feature .feature-list { list-style: none; padding: 0;
    margin: 4px 0 0 0; display: flex; flex-direction: column; gap: 6px; }
  .card--feature .feature-list li { padding-left: 14px; position: relative;
    line-height: 1.5; }
  .card--feature .feature-list li::before {
    content: ""; position: absolute; left: 0; top: 0.55em;
    width: 6px; height: 6px; border-radius: 50%; background: #c8a96a;
  }
  /* Cartoons category card: image-only, no summary chrome.
     Per Gaither 2026-05-14: "they don't need a summary or expand
     full summary or any of that. They just need the picture full
     width of the card and full height." */
  .card--cartoon { padding: 0; overflow: hidden;
    background: var(--card-bg); }
  .card--cartoon .cartoon-card-header {
    padding: 10px 16px 8px;
    text-align: left;
  }
  .card--cartoon .cartoon-strip-name {
    color: var(--fg); font-family: var(--serif);
    font-size: 16px; font-weight: 700;
    letter-spacing: 0.01em; text-decoration: none;
  }
  .card--cartoon .cartoon-strip-name:hover { color: var(--cyan); }
  .card--cartoon .cartoon-card-link { display: block; line-height: 0;
    background: var(--card-bg); cursor: zoom-in;
    margin: 0 14px; border-radius: 4px; overflow: hidden; }
  .card--cartoon .cartoon-card-img { display: block;
    max-width: 100%; width: auto; height: auto;
    margin: 0 auto;
    background: var(--card-bg);
    transition: transform .15s ease; }
  .card--cartoon .cartoon-card-link:hover .cartoon-card-img { transform: scale(1.005); }
  /* Daily-game logo banner (Gaither 2026-06-23: "logos for things that are
     going to be every day like the word search, crossword, etc."). Flat,
     on-brand lockup: a colored emblem badge (the category icon) + a bold
     wordmark + a small uppercase tagline. Tinted with the per-game category
     color via data-cat, so each daily puzzle has its own recognizable mark.
     Replaces the old plain glyph + title head on the four game cards. */
  .game-logo { display: flex; align-items: center; gap: 12px;
    margin: 0 0 14px; padding: 11px 14px; border-radius: 12px;
    background: linear-gradient(135deg, var(--cat-soft, var(--subtle)), transparent 78%);
    border: 1px solid var(--cat-border, var(--line)); }
  .game-logo-mark { flex: 0 0 auto; width: 44px; height: 44px; border-radius: 11px;
    display: inline-flex; align-items: center; justify-content: center;
    background: var(--cat-soft, var(--subtle)); color: var(--cat-color, var(--cyan)); }
  .game-logo-mark svg { width: 26px; height: 26px; }
  .game-logo-txt { display: flex; flex-direction: column; line-height: 1.1; min-width: 0; }
  .game-logo-name { font-family: var(--serif, Georgia), serif; font-size: 21px;
    font-weight: 700; letter-spacing: -0.01em; color: var(--fg); }
  .game-logo-sub { margin-top: 3px; font-size: 10.5px; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.14em; color: var(--cat-color, var(--cyan)); }

  /* Daily Sudoku card (almanac). Nested grid: the outer grid is 3x3 boxes
     whose --fg background shows through a 2px gap (thick box lines); each
     box is a 3x3 of cells whose grey background shows through a 1px gap
     (thin cell lines). This guarantees every line renders. */
  .card--sudoku { display: block; padding: 18px 20px 20px; }
  .card--sudoku .sudoku-head { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; }
  .card--sudoku .sudoku-glyph { display: inline-flex; width: 20px; height: 20px; color: var(--cyan); }
  .card--sudoku .sudoku-glyph svg { width: 20px; height: 20px; }
  .card--sudoku .sudoku-title { font-family: var(--serif, Georgia), serif; font-size: 19px; margin: 0; }
  .sudoku-diffs { display: flex; gap: 6px; justify-content: center; margin: 0 0 14px; }
  .sudoku-diff-btn { font: inherit; font-size: 12px; padding: 5px 14px; border-radius: 999px; border: 1px solid var(--line-strong, var(--line)); background: var(--card-bg); color: var(--muted); cursor: pointer; transition: background .12s ease, border-color .12s ease, color .12s ease; }
  .sudoku-diff-btn:hover { border-color: var(--cyan); color: var(--cyan); }
  .sudoku-diff-btn.active { border-color: var(--cyan); color: var(--cyan); background: rgba(1,204,255,0.12); }
  .sudoku-grid { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); gap: 2px; width: min(376px, 90vw); aspect-ratio: 1 / 1; margin: 0 auto; background: var(--fg); border: 2px solid var(--fg); }
  .sudoku-box { display: grid; grid-template-columns: repeat(3, 1fr); grid-template-rows: repeat(3, 1fr); gap: 1px; background: rgba(150,154,166,0.6); min-width: 0; min-height: 0; }
  .sudoku-cell { background: var(--card-bg); display: flex; align-items: center; justify-content: center; font-size: clamp(15px, 4.4vw, 19px); min-width: 0; min-height: 0; box-sizing: border-box; }
  .sudoku-given { font-weight: 700; color: var(--fg); }
  input.sudoku-input { width: 100%; height: 100%; text-align: center; border: 0; outline: none; background: var(--card-bg); color: var(--cyan); font: inherit; font-size: clamp(15px, 4.4vw, 19px); padding: 0; -moz-appearance: textfield; border-radius: 0; }
  input.sudoku-input:focus { background: rgba(1,204,255,0.16); }
  input.sudoku-input.bad { color: var(--right); background: rgba(226,107,135,0.18); }
  .sudoku-actions { display: flex; gap: 8px; justify-content: center; margin-top: 16px; }
  .sudoku-btn { font: inherit; font-size: 13px; padding: 7px 16px; border-radius: 999px; border: 1px solid var(--line-strong, var(--line)); background: var(--card-bg); color: var(--fg); cursor: pointer; transition: background .12s ease, border-color .12s ease; }
  .sudoku-btn:hover { border-color: var(--cyan); color: var(--cyan); }
  .sudoku-msg { text-align: center; margin-top: 12px; font-size: 14px; min-height: 18px; color: var(--muted); }
  /* Almanac lesson card: self-contained short read with a gallery. */
  .card--lesson { display: block; position: relative; }
  .card--lesson .lesson-body { padding: 18px 22px 20px; }
  .card--lesson .lesson-meta { display: flex; align-items: center; gap: 10px; margin-bottom: 11px; }
  .card--lesson .lesson-tag { font-size: 11px; letter-spacing: .09em; text-transform: uppercase; color: var(--muted); }
  .card--lesson .lesson-title { font-family: var(--serif, Georgia), serif; font-size: clamp(20px, 3.2vw, 26px); line-height: 1.22; margin: 0 0 15px; }
  .card--lesson .lesson-hero { margin: 0 0 16px; border-radius: 10px; overflow: hidden; background: var(--line); display: flex; justify-content: center; }
  /* Full-size pictures (Gaither 2026-06-23): show at natural size, shrinking
     for WIDTH first (max-width 100% of the card), then for HEIGHT only if
     still too tall (generous max-height). width/height:auto keeps aspect and
     never upscales. */
  .card--lesson .lesson-hero img { display: block; max-width: 100%; width: auto; height: auto; max-height: 78vh; }
  .card--lesson .lesson-text { font-size: 16px; line-height: 1.72; color: var(--fg); }
  .card--lesson .lesson-text p { margin: 0 0 14px; }
  .card--lesson .lesson-text p:last-child { margin-bottom: 0; }
  .card--lesson .lesson-text::after { content: ""; display: block; clear: both; }
  .card--lesson .lesson-fig { width: 46%; max-width: 440px; margin: 4px 0 14px; border-radius: 8px; overflow: hidden; background: var(--line); text-align: center; }
  .card--lesson .lesson-fig img { display: block; max-width: 100%; width: auto; height: auto; max-height: 560px; margin: 0 auto; }
  .card--lesson .lesson-fig.fig-right { float: right; margin-left: 20px; }
  .card--lesson .lesson-fig.fig-left { float: left; margin-right: 20px; }
  @media (max-width: 620px) {
    .card--lesson .lesson-fig { float: none !important; width: 100%; max-width: none; margin: 6px 0 14px; }
  }
  .card--lesson .lesson-gallery { display: grid; grid-template-columns: repeat(auto-fit, minmax(150px, 1fr)); gap: 8px; margin: 18px 0 0; }
  .card--lesson .lesson-gallery img { width: 100%; height: 170px; object-fit: contain; border-radius: 8px; background: var(--line); }
  .card--lesson .lesson-source { margin-top: 15px; font-size: 12.5px; color: var(--muted); }
  .card--lesson .lesson-source a { color: var(--muted); text-decoration: underline; text-underline-offset: 2px; }
  .card--lesson .lesson-source a:hover { color: var(--cyan); }
  /* On the wide Almanac layout, keep the running text at a readable measure
     (centered) even though the card itself is wide; the hero image is allowed
     to span wider for a magazine/textbook feel. */
  .layout--almanac .card--lesson .lesson-title,
  .layout--almanac .card--lesson .lesson-text,
  .layout--almanac .card--lesson .lesson-meta,
  .layout--almanac .card--lesson .lesson-source { max-width: 880px; margin-left: auto; margin-right: auto; }
  .layout--almanac .card--lesson .lesson-hero { max-width: 100%; margin-left: auto; margin-right: auto; }
  /* Daily Wordle card (almanac). */
  .card--wordle { display: block; padding: 18px 20px 20px; }
  .card--wordle .wordle-head { display: flex; align-items: center; gap: 8px; margin: 0 0 14px; }
  .card--wordle .wordle-glyph { display: inline-flex; width: 20px; height: 20px; color: var(--cyan); }
  .card--wordle .wordle-glyph svg { width: 20px; height: 20px; }
  .card--wordle .wordle-title { font-family: var(--serif, Georgia), serif; font-size: 19px; margin: 0; }
  .wordle-grid { display: grid; grid-template-rows: repeat(6, 1fr); gap: 5px; width: min(300px, 78vw); margin: 0 auto; }
  .wordle-row { display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px; }
  .wordle-tile { aspect-ratio: 1 / 1; border: 2px solid var(--line-strong, var(--line)); display: flex; align-items: center; justify-content: center; font-size: clamp(20px, 7vw, 26px); font-weight: 700; text-transform: uppercase; color: var(--fg); border-radius: 4px; }
  .wordle-tile.filled { border-color: var(--muted); }
  .wordle-tile.correct { background: #2f9e5e; border-color: #2f9e5e; color: #fff; }
  .wordle-tile.present { background: #d9a521; border-color: #d9a521; color: #fff; }
  .wordle-tile.absent { background: #3a424c; border-color: #3a424c; color: #fff; }
  .wordle-msg { text-align: center; margin: 14px 0 10px; font-size: 14px; min-height: 18px; color: var(--muted); }
  .wordle-kb { display: flex; flex-direction: column; gap: 6px; align-items: center; }
  .wordle-kb-row { display: flex; gap: 5px; justify-content: center; }
  .wordle-key { font: inherit; font-size: 13px; font-weight: 600; min-width: 28px; height: 44px; padding: 0 7px; border-radius: 5px; border: 0; background: var(--line-strong, #3d454e); color: var(--fg); cursor: pointer; text-transform: uppercase; }
  .wordle-key-wide { font-size: 11px; min-width: 50px; }
  .wordle-key:hover { filter: brightness(1.15); }
  .wordle-key.correct { background: #2f9e5e; color: #fff; }
  .wordle-key.present { background: #d9a521; color: #fff; }
  .wordle-key.absent { background: #2a3138; color: var(--muted); }
  /* Daily Mini Crossword card (almanac). All-white 5x5, our own grids. */
  .card--crossword { display: block; padding: 18px 20px 20px; }
  .card--crossword .xw-head { display: flex; align-items: center; gap: 8px; margin: 0 0 14px; }
  .card--crossword .xw-glyph { display: inline-flex; width: 20px; height: 20px; color: var(--cyan); }
  .card--crossword .xw-glyph svg { width: 20px; height: 20px; }
  .card--crossword .xw-title { font-family: var(--serif, Georgia), serif; font-size: 19px; margin: 0; }
  .card--crossword .xw-diffs { display: flex; gap: 6px; justify-content: center; margin: 0 0 14px; }
  .card--crossword .xw-diff-btn { font: inherit; font-size: 12px; padding: 5px 14px; border-radius: 999px; border: 1px solid var(--line-strong, var(--line)); background: var(--card-bg); color: var(--muted); cursor: pointer; transition: background .12s ease, border-color .12s ease, color .12s ease; }
  .card--crossword .xw-diff-btn:hover { border-color: var(--cyan); color: var(--cyan); }
  .card--crossword .xw-diff-btn.active { border-color: var(--cyan); color: var(--cyan); background: rgba(1,204,255,0.12); }
  input.xw-input.xw-given { color: var(--fg); font-weight: 800; background: rgba(140,144,156,0.16); cursor: default; }
  /* Grid + clues side by side on wide cards (Gaither 2026-06-23: use the
     card layout better); wraps to stacked on narrow widths. */
  .xw-body { display: flex; gap: 30px; align-items: flex-start; justify-content: center; flex-wrap: wrap; }
  .xw-left { flex: 0 0 auto; }
  .xw-grid { display: grid; grid-template-columns: repeat(5, 1fr); grid-template-rows: repeat(5, 1fr); gap: 2px; width: min(400px, 86vw); aspect-ratio: 1 / 1; margin: 0 auto; background: var(--fg); border: 2px solid var(--fg); }
  .xw-cell { position: relative; background: var(--card-bg); min-width: 0; min-height: 0; }
  .xw-num { position: absolute; top: 1px; left: 2px; font-size: 9px; line-height: 1; color: var(--muted); pointer-events: none; z-index: 1; }
  input.xw-input { width: 100%; height: 100%; text-align: center; border: 0; outline: none; background: var(--card-bg); color: var(--cyan); font: inherit; font-size: clamp(15px, 5vw, 20px); font-weight: 700; text-transform: uppercase; padding: 0; border-radius: 0; }
  input.xw-input:focus { background: rgba(1,204,255,0.18); }
  input.xw-input.xw-hl { background: rgba(1,204,255,0.07); }
  input.xw-input.xw-bad { color: var(--right); background: rgba(226,107,135,0.18); }
  input.xw-input.xw-good { color: #2f9e5e; }
  .xw-actions { display: flex; gap: 8px; justify-content: center; margin-top: 16px; }
  .xw-btn { font: inherit; font-size: 13px; padding: 7px 16px; border-radius: 999px; border: 1px solid var(--line-strong, var(--line)); background: var(--card-bg); color: var(--fg); cursor: pointer; transition: background .12s ease, border-color .12s ease; }
  .xw-btn:hover { border-color: var(--cyan); color: var(--cyan); }
  .xw-msg { text-align: center; margin: 12px 0 0; font-size: 14px; min-height: 18px; color: var(--muted); }
  .xw-clues { flex: 1 1 340px; min-width: 280px; display: grid; grid-template-columns: 1fr 1fr; gap: 14px 26px; margin-top: 4px; align-self: stretch; }
  .xw-cluecol h4 { font-family: var(--serif, Georgia), serif; font-size: 13px; margin: 0 0 8px; text-transform: uppercase; letter-spacing: .07em; color: var(--muted); }
  .xw-clue-list { list-style: none; margin: 0; padding: 0; font-size: 13.5px; line-height: 1.5; }
  .xw-clue-list li { margin: 0 0 7px; color: var(--fg); }
  .xw-cnum { display: inline-block; min-width: 16px; font-weight: 700; color: var(--cyan); }
  @media (max-width: 480px) { .xw-clues { grid-template-columns: 1fr; gap: 12px; } }
  /* Daily Word Search card (almanac). Drag across letters to find words. */
  .card--wordsearch { display: block; padding: 18px 20px 20px; }
  .card--wordsearch .ws-head { display: flex; align-items: center; gap: 8px; margin: 0 0 6px; }
  .card--wordsearch .ws-glyph { display: inline-flex; width: 20px; height: 20px; color: var(--cyan); }
  .card--wordsearch .ws-glyph svg { width: 20px; height: 20px; }
  .card--wordsearch .ws-title { font-family: var(--serif, Georgia), serif; font-size: 19px; margin: 0; }
  .ws-theme { text-align: center; font-size: 12px; color: var(--muted); margin: 0 0 10px; text-transform: uppercase; letter-spacing: .07em; }
  .ws-diffs { display: flex; gap: 6px; justify-content: center; margin: 0 0 12px; }
  .ws-diff-btn { font: inherit; font-size: 12px; padding: 5px 14px; border-radius: 999px; border: 1px solid var(--line-strong, var(--line)); background: var(--card-bg); color: var(--muted); cursor: pointer; transition: background .12s ease, border-color .12s ease, color .12s ease; }
  .ws-diff-btn:hover { border-color: var(--cyan); color: var(--cyan); }
  .ws-diff-btn.active { border-color: var(--cyan); color: var(--cyan); background: rgba(1,204,255,0.12); }
  .ws-grid { display: grid; width: min(444px, 92vw); aspect-ratio: 1 / 1; margin: 0 auto; touch-action: none; user-select: none; -webkit-user-select: none; border: 1px solid var(--line-strong, var(--line)); border-radius: 8px; overflow: hidden; background: var(--card-bg); }
  .ws-cell { display: flex; align-items: center; justify-content: center; font-size: clamp(11px, 2.7vw, 16px); font-weight: 600; text-transform: uppercase; color: var(--fg); cursor: pointer; min-width: 0; box-shadow: inset 0 0 0 0.5px var(--line); }
  .ws-cell.ws-sel { background: rgba(1,204,255,0.30); }
  .ws-cell.ws-found { background: rgba(79,184,154,0.32); }
  .ws-msg { text-align: center; margin: 14px 0 4px; font-size: 14px; color: var(--muted); min-height: 18px; }
  .ws-words { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-wrap: wrap; gap: 7px 16px; justify-content: center; }
  .ws-word { font-size: 13.5px; color: var(--fg); letter-spacing: .03em; }
  .ws-word.found { color: var(--muted); text-decoration: line-through; }
  .ws-actions { display: flex; gap: 8px; justify-content: center; margin-top: 16px; }
  .ws-btn { font: inherit; font-size: 13px; padding: 7px 16px; border-radius: 999px; border: 1px solid var(--line-strong, var(--line)); background: var(--card-bg); color: var(--fg); cursor: pointer; transition: background .12s ease, border-color .12s ease; }
  .ws-btn:hover { border-color: var(--cyan); color: var(--cyan); }
  .card--cartoon .cartoon-card-caption {
    padding: 10px 16px 14px;
    text-align: center;
    font-family: var(--sans); font-size: 12.5px;
    line-height: 1.4;
  }
  .card--cartoon .cartoon-card-title {
    color: var(--muted); font-style: italic;
    letter-spacing: 0.01em;
  }
  /* XKCD-style hover-text caption. Shows under the strip title in a
     slightly dimmer tone, with a small visual gap so it reads as a
     separate "the joke" line rather than part of the title. */
  .card--cartoon .cartoon-card-hover {
    display: block;
    margin-top: 6px;
    color: var(--fg);
    opacity: 0.78;
    font-family: var(--sans);
    font-size: 13px;
    font-style: normal;
    line-height: 1.45;
    letter-spacing: 0;
  }

  /* Cartoon-of-the-day image. Full card width with the normal card
     padding as buffer. Aspect ratio is preserved (object-fit: contain)
     so top and bottom never get cropped. Max-height keeps very tall
     historical Sunday pages from dominating the viewport. Per Gaither
     2026-05-14: "show more of the cartoon, full card width, don't cut
     off top or bottom." */
  .card--feature .feature-cartoon-link { display: block; margin: 10px 0 12px;
    text-align: center; }
  .card--feature .feature-cartoon-img {
    width: 100%;
    height: auto;
    max-height: 80vh;
    object-fit: contain;
    /* Theme-aware paper backdrop. Was hardcoded #fff which left a
       stark white slab on dark / retro80s themes. var(--card-bg)
       blends with the surrounding card chrome on every palette.
       Per Gaither 2026-05-15. */
    background: var(--card-bg);
    border: 1px solid var(--line);
    border-radius: 6px;
    box-shadow: 0 2px 6px rgba(0,0,0,0.08);
    transition: transform .15s;
  }
  .card--feature .feature-cartoon-link:hover .feature-cartoon-img {
    transform: scale(1.005);
  }
  /* (Older theme-locked .date-pill rule removed 2026-05-14 — the new
     theme-aware rule above takes over.) */
  /* Dark theme: keep the feature card light by design — the cream bg
     legacy dark-theme override removed 2026-05-14 — the card now
     inherits var(--card-bg) + var(--fg) so each theme reads cleanly. */
  /* Per-type SVG wireframe backgrounds. Each is a single-color outline
     rendered as a data: URI so no external request is needed. The fill
     'none' stroke '#c8a96a' opacity '0.4' style keeps them subtle and
     of-a-piece with the cream background. */
  .card--feature[data-feature-type="famous_birthdays"],
  .card--feature[data-feature-type="historical_figure_birthdays"] {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200' fill='none' stroke='%23c8a96a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' opacity='0.55'><path d='M40 160 L160 160 L160 100 L40 100 Z'/><path d='M50 100 L50 70 L150 70 L150 100'/><path d='M70 70 L70 50 M100 70 L100 50 M130 70 L130 50'/><path d='M65 45 Q70 35 75 45 M95 45 Q100 35 105 45 M125 45 Q130 35 135 45'/><path d='M40 130 L160 130'/><path d='M55 130 Q60 120 65 130 Q70 120 75 130 Q80 120 85 130 Q90 120 95 130 Q100 120 105 130 Q110 120 115 130 Q120 120 125 130 Q130 120 135 130 Q140 120 145 130'/></svg>");
  }
  .card--feature[data-feature-type="this_day_history"],
  .card--feature[data-feature-type="on_this_day_feature"],
  .card--feature[data-feature-type="on_this_day_list"] {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200' fill='none' stroke='%23c8a96a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' opacity='0.55'><rect x='40' y='50' width='120' height='110' rx='4'/><path d='M40 80 L160 80'/><path d='M70 35 L70 65 M100 35 L100 65 M130 35 L130 65'/><circle cx='65' cy='105' r='2' fill='%23c8a96a'/><path d='M75 105 L140 105 M75 125 L130 125 M75 145 L120 145'/><circle cx='65' cy='125' r='2' fill='%23c8a96a'/><circle cx='65' cy='145' r='2' fill='%23c8a96a'/></svg>");
  }
  /* Cartoon slot uses the actual XKCD image — no wireframe behind it. */
  .card--feature[data-feature-type="cartoon"] {
    background-image: none;
  }
  .card--feature[data-feature-type="on_this_day_science"] {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200' fill='none' stroke='%23c8a96a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' opacity='0.55'><ellipse cx='100' cy='100' rx='60' ry='22'/><ellipse cx='100' cy='100' rx='60' ry='22' transform='rotate(60 100 100)'/><ellipse cx='100' cy='100' rx='60' ry='22' transform='rotate(-60 100 100)'/><circle cx='100' cy='100' r='6' fill='%23c8a96a'/><circle cx='160' cy='100' r='3' fill='%23c8a96a'/><circle cx='70' cy='148' r='3' fill='%23c8a96a'/><circle cx='130' cy='52' r='3' fill='%23c8a96a'/></svg>");
  }
  .card--feature[data-feature-type="weekly_reading"] {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200' fill='none' stroke='%23c8a96a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' opacity='0.55'><path d='M40 50 Q100 40 100 60 L100 160 Q100 140 40 150 Z'/><path d='M160 50 Q100 40 100 60 L100 160 Q100 140 160 150 Z'/><path d='M55 70 L90 65 M55 85 L90 80 M55 100 L90 95 M55 115 L90 110'/><path d='M110 65 L145 70 M110 80 L145 85 M110 95 L145 100 M110 110 L145 115'/></svg>");
  }
  .card--feature[data-feature-type="word_of_the_day"] {
    background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 200 200' fill='none' stroke='%23c8a96a' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' opacity='0.55'><rect x='40' y='50' width='120' height='110' rx='3'/><path d='M40 50 L160 50 L160 60 L40 60 Z' fill='%23c8a96a' fill-opacity='0.18'/><path d='M55 85 L145 85 M55 100 L145 100 M55 115 L120 115 M55 130 L135 130 M55 145 L100 145'/><circle cx='130' cy='115' r='8'/><path d='M134 119 L142 127'/></svg>");
  }
  /* Mobile: shrink the wireframe so it doesn't crowd the copy. */
  @media (max-width: 720px) {
    .card--feature { background-size: 140px 140px; background-position: right -10px center; }
    .card--feature .feature-body { max-width: 100%; }
  }
  .pill-positive { background: rgba(244,180,0,0.12); color: #b58400;
    border: 1px solid rgba(244,180,0,0.35); font-weight: 600; }
  [data-theme="dark"] .pill-positive { color: #ffd24a;
    background: rgba(255,210,74,0.10); border-color: rgba(255,210,74,0.30); }

  /* Left vs Right two-column */
  /* Latent political framing note — sits above the L/R grid on events
     whose political_state="latent". Subtle so it doesn't dominate but
     readable so the reader knows the framings below are typical
     patterns rather than a current partisan fight. */
  /* Politically-charged-subject pill. Centered, single tri-color
     dashed border with alternating red/white/blue dashes around
     the entire pill perimeter. Implemented as a 2-layer background
     trick: the pill body fills the inner padding-box with card-bg
     color, and the BORDER area shows a repeating-linear-gradient
     of red dash, gap, white dash, gap, blue dash, gap. The "gaps"
     are transparent so the parent card's background shows through,
     giving the dashed effect. Per Gaither 2026-05-14. */
  .lr-latent-note { display: flex; align-items: center; justify-content: center;
    gap: 6px; padding: 5px 14px; border-radius: 999px;
    margin: 0 auto 12px; width: fit-content; max-width: 90%;
    color: var(--fg);
    border: 2px solid transparent;
    background:
      linear-gradient(var(--card-bg), var(--card-bg)) padding-box,
      repeating-linear-gradient(90deg,
        #c23b5e 0 5px,
        transparent 5px 8px,
        #ffffff 8px 13px,
        transparent 13px 16px,
        #2f6fbb 16px 21px,
        transparent 21px 24px
      ) border-box;
    font-family: var(--sans); font-size: 11px; font-weight: 700;
    letter-spacing: 0.05em; text-transform: uppercase;
    cursor: help; }
  .lr-latent-note svg { width: 12px; height: 12px; opacity: 0.8; }
  .lr-grid:not(:has(.lr-col + .lr-col)) { grid-template-columns: 1fr !important; }
  .lr-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 14px;
    margin-top: 14px; }
  @media (max-width: 700px) { .lr-grid { grid-template-columns: 1fr; gap: 10px; } }
  .lr-col { padding: 12px 14px; border-radius: 8px;
    border: 1px solid var(--line); }
  .lr-col h4 { margin: 0 0 6px; font-size: 11px; font-weight: 700;
    text-transform: uppercase; letter-spacing: 0.1em; }
  .lr-col p { margin: 0; font-size: 14px; line-height: 1.55; }
  .lr-col p.lr-headline { font-family: var(--serif); font-size: 15px;
    font-weight: 600; font-style: italic; line-height: 1.3;
    margin-bottom: 8px; color: var(--fg); }
  .lr-col.left  p.lr-headline { color: var(--left); }
  .lr-col.right p.lr-headline { color: var(--right); }
  /* Politically-charged left/right panel borders. Reduced 3px -> 2px ->
     1px in successive passes per Gaither 2026-05-16. Hair-line reads
     as a quiet accent that complements the soft-tinted background fill
     rather than competing with the centered "politically charged" pill.
     The dashed prior-coverage variant below stays at 3px so the "has
     said" panels still register as a visually distinct treatment. */
  .lr-col.left  { border-left: 1px solid var(--left); background: var(--left-soft); }
  .lr-col.right { border-left: 1px solid var(--right); background: var(--right-soft); }
  .lr-col.left  h4 { color: var(--left); }
  .lr-col.right h4 { color: var(--right); }
  /* Lean intensity meter: shows how far left / right the framing
     leans. 4 bars per side, filled outward from center. Color
     matches the panel's --left / --right. */
  .lean-meter { display: inline-flex; align-items: center; gap: 6px;
    margin: 0 0 8px; font-size: 11px; font-weight: 500;
    letter-spacing: 0.04em; }
  .lean-meter .lean-bars { display: inline-flex; gap: 2px; align-items: center; }
  .lean-bar { display: inline-block; width: 4px; height: 10px;
    background: var(--line); border-radius: 1px; opacity: 0.5; }
  .lean-meter--left .lean-bar.on { background: var(--left); opacity: 1; }
  .lean-meter--right .lean-bar.on { background: var(--right); opacity: 1; }
  .lean-meter--left { color: var(--left); }
  .lean-meter--right { color: var(--right); }
  .lean-meter-label { white-space: nowrap; text-transform: uppercase;
    font-size: 10px; }
  /* Empty column WITHOUT prior coverage — just an "outlet hasn't covered
     this yet" note. Dashed left border + italic + muted to read clearly
     as "missing data". */
  /* Empty L/R column — by default uses the same surface + typography
     as the populated left/right columns so the two panels read as a
     matched pair. text-align: left explicitly because the global
     .empty rule centers (used for the no-events landing state) was
     bleeding into here. The only visual difference comes from the
     has-prior dashed border + the orange Prior badge below. */
  .lr-col.empty { background: var(--subtle); text-align: left;
    border-left: 3px dashed var(--line); }
  .lr-col.empty p { color: var(--fg); font-size: 14px; font-style: normal; }
  .lr-col.empty h4 { color: var(--muted); }

  /* Empty column WITH prior coverage populated — fully visible (no opacity
     dim), but every side of the box is dashed so the reader recognizes the
     content as "based on prior coverage, not current". A small badge at
     the top names the state explicitly. When real current coverage later
     arrives, this whole block is replaced by the regular .lr-col.left/
     right styling — no visual ambiguity between "prior" and "current". */
  /* Match the populated .lr-col exactly: 1px solid all sides + a 3px
     accent border-left, but the LEFT border is dashed (instead of solid)
     to signal "prior coverage" rather than current. Same padding, same
     inner geometry — the two columns visually line up. */
  /* Prior-coverage panel — the "WHAT THE LEFT/RIGHT HAS SAID" version
     of the L/R card. SAME blue/red surface + h4 coloring as the live
     'SAYS' panel, so the side reads as left/right at a glance and the
     visual distinction between live vs historical is carried by the
     bar style alone: 3px SOLID = real source filed for THIS story,
     5px DOTTED = no source yet; we're pulling the typical framing
     from prior coverage cache. Gaither 2026-05-13 flagged the
     previous styling as "grayscale" because the empty bg overrode
     the colored tint. */
  .lr-col.empty.has-prior {
    opacity: 1;
    border: 1px solid var(--line);
    border-left-width: 4px;
    border-left-style: dashed;
    border-left-color: var(--line-strong, var(--line));
  }
  .lr-col.empty.has-prior.left-empty  { background: var(--left-soft);
    border-left-color: var(--left); }
  .lr-col.empty.has-prior.right-empty { background: var(--right-soft);
    border-left-color: var(--right); }
  .lr-col.empty.has-prior.left-empty  h4 { color: var(--left); }
  .lr-col.empty.has-prior.right-empty h4 { color: var(--right); }
  .lr-col.empty.has-prior p,
  .lr-col.empty.has-prior .prior-coverage-inline { color: var(--fg); }
  /* Earlier-articles row beneath the prior-coverage prose. Tiny inline
     anchor pills so the reader can click through to the older events
     that the pattern was characterized from. */
  .prior-refs {
    display: flex; flex-wrap: wrap; gap: 4px;
    margin-top: 8px; padding-top: 6px;
    border-top: 1px dotted var(--line);
    font-size: 11px; color: var(--muted);
  }
  .prior-refs .prior-refs-label {
    font-weight: 600; text-transform: uppercase;
    letter-spacing: 0.05em; margin-right: 4px;
  }
  .prior-refs a {
    color: var(--cyan); text-decoration: none;
    padding: 1px 6px; border-radius: 999px;
    border: 1px solid var(--line);
  }
  .prior-refs a:hover { border-color: var(--cyan); background: var(--cyan-soft); }
  .prior-coverage-label { display: inline-block; font-size: 10px;
    text-transform: uppercase; letter-spacing: 0.1em;
    color: var(--muted); font-style: normal; font-weight: 700;
    background: var(--card-bg); padding: 2px 8px; border-radius: 999px;
    border: 1px dashed var(--line-strong, var(--line));
    margin: 0 0 10px; }
  .lr-col.empty.has-prior.left-empty .prior-coverage-label { color: var(--left); border-color: var(--left); }
  .lr-col.empty.has-prior.right-empty .prior-coverage-label { color: var(--right); border-color: var(--right); }
  /* Prior-coverage body shares the lr-col.right/left body styling
     exactly. Same font weight, color, and size as the populated side
     so the two columns look like a matched pair. */
  .prior-coverage-inline { font-size: 14px; line-height: 1.55;
    color: var(--fg); font-style: normal; font-weight: 400; }
  .prior-coverage-inline p { margin: 0 0 8px; font-style: normal;
    color: var(--fg); font-weight: 400; }
  .prior-coverage-inline p:last-child { margin-bottom: 0; }
  /* Fallback body when neither cluster sources nor prior-coverage
     cache turned up anything for this side. Mild + honest about the
     absence rather than printing the cache placeholder verbatim. */
  .lr-body.lr-missing { color: var(--muted); font-size: 13px;
    line-height: 1.5; font-style: italic; }

  /* Prior-coverage lazy-button layer (fallback for any one-sided event
     that hasn't yet had the ingest pass populate it). */
  .prior-coverage-btn { margin-top: 8px; background: transparent; border: 0;
    padding: 0; color: var(--cyan); font-family: inherit; font-size: 12px;
    font-weight: 600; cursor: pointer; text-align: left; line-height: 1.4;
    transition: opacity .15s; }
  .prior-coverage-btn:hover { opacity: 0.8; text-decoration: underline; }
  .prior-coverage-btn[data-loaded="true"] { display: none; }
  .prior-coverage-box { display: none; margin-top: 8px; padding-top: 10px;
    border-top: 1px dashed var(--line); font-size: 13px; line-height: 1.55;
    color: var(--fg); font-style: normal; }
  .prior-coverage-box[data-visible="true"] { display: block; }
  .prior-coverage-box p { margin: 0 0 8px; font-style: normal; color: var(--fg);
    font-size: 13px; }
  .prior-coverage-box ul { margin: 0 0 8px; padding-left: 18px; }
  .prior-coverage-box li { margin: 0 0 6px; }
  .prior-coverage-box .prior-coverage-loading { color: var(--muted);
    font-style: italic; display: flex; flex-direction: column; align-items: center;
    gap: 10px; padding: 10px 0; }

  /* Sources. Tightened 2026-05-17 per Gaither: "tighten up the Read
     Original Article and Share to Social areas so they aren't so
     padded and take up so much room." margin-top 18 -> 10,
     padding-top 16 -> 8, label margin-bottom 6 -> 3. */
  .sources-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 10px;
    padding-top: 8px; border-top: 1px dashed var(--line);
    align-items: center; }
  .sources-row .sources-label { font-size: 10px; text-transform: uppercase;
    letter-spacing: 0.12em; color: var(--muted); width: 100%; margin-bottom: 3px;
    font-weight: 600; }
  .source-pill { position: relative; display: inline-flex; align-items: center;
    justify-content: center; width: 32px; height: 32px;
    border-radius: 50%; background: var(--card-bg);
    border: 1px solid var(--line); color: var(--fg); text-decoration: none;
    transition: border-color .15s, background .15s, transform .15s; }
  .source-pill:hover { border-color: var(--cyan); background: var(--cyan-soft);
    transform: translateY(-1px); text-decoration: none; }
  /* Favicons get a slight desaturation + contrast bump on idle so the
     wall of colorful logos reads quieter; restored to full color on
     hover so the source is still recognizable when reaching for it. */
  .source-pill .favicon { width: 18px; height: 18px; display: block;
    /* Full color (no saturate filter). User asked for the source icons
       to keep their original brand color rather than the muted form. */
    border-radius: 50%;
    /* filter: removed (was saturate 0.55) — keep brand color */
    transition: filter .15s ease; }
  /* Dark-theme favicon mask: many outlet favicons (Daily Wire, NY Post,
     plain text marks) are dark-ink-on-transparent and disappear into the
     dark card background. Off-white circular backdrop lifts them off the
     surface. Most full-color favicons (Atlantic, Reuters, etc.) already
     ship with a colored fill so the backdrop is invisible against them. */
  [data-theme="dark"] .source-pill .favicon {
    background: #f4f4f0;
    box-shadow: 0 0 0 1px rgba(255,255,255,0.08);
  }
  /* .source-pill:hover .favicon — no transition needed now that the
     base state already shows full brand color. */
  .source-pill .outlet-fallback { font-size: 10px; font-weight: 700;
    color: var(--muted); line-height: 1; }
  .source-pill .lean-dot { position: absolute; bottom: -2px; right: -2px;
    width: 9px; height: 9px; border-radius: 50%; flex-shrink: 0;
    box-shadow: 0 0 0 2px var(--card-bg); }
  /* Lean dots use the Dem-blue / Rep-red convention so the partisan
     coding is unambiguous at a glance. Right side shifted from orange
     to a muted red (not full crimson — needs to read well on light AND
     dark theme without screaming). */
  .source-pill .lean-dot[data-lean="-2"] { background: #2f6fbb; }
  .source-pill .lean-dot[data-lean="-1"] { background: #6a9fe2; }
  .source-pill .lean-dot[data-lean="0"]  { background: #888; }
  .source-pill .lean-dot[data-lean="1"]  { background: #d97b8c; }
  .source-pill .lean-dot[data-lean="2"]  { background: #c23b5e; }
  .source-pill .lag { position: absolute; top: -6px; left: 50%;
    transform: translateX(-50%);
    font-size: 9px; color: var(--muted);
    background: var(--card-bg); padding: 0 4px; border-radius: 999px;
    border: 1px solid var(--line); line-height: 1.3; }

  /* Action row: share buttons only. Tightened 2026-05-17:
     margin-top 14 -> 8 so the share row sits close under the
     sources row instead of opening a big gap. */
  .action-row { display: flex; flex-wrap: wrap; gap: 6px; margin-top: 8px;
    align-items: center; }
  .action-row .label { font-size: 10px; text-transform: uppercase;
    letter-spacing: 0.12em; color: var(--muted); margin-right: 4px;
    font-weight: 600; }
  /* Utility cluster (mail + copy-link) at the END of the share row.
     Kept tight together visually so they read as the "private share"
     pair, separate from the social platforms before them. */
  .action-row-utility { display: inline-flex; gap: 4px; margin-left: auto; }
  .action-row-utility .action-btn { border-color: var(--line-strong, var(--line)); }
  .action-btn { background: transparent; border: 1px solid var(--line);
    color: var(--fg); padding: 4px 10px; border-radius: 999px;
    font-size: 11px; font-family: inherit; cursor: pointer;
    text-decoration: none; display: inline-flex; align-items: center; gap: 5px;
    transition: border-color .15s, background .15s, color .15s; }
  .action-btn:hover { border-color: var(--cyan); color: var(--cyan);
    background: var(--cyan-soft); text-decoration: none; }
  .action-btn svg { width: 12px; height: 12px; }
  /* Share buttons: 28px circular, icon-only. */
  .share-btn { width: 28px; height: 28px; padding: 0; border-radius: 50%;
    justify-content: center; font-size: 14px; line-height: 1; }
  .share-btn svg { width: 13px; height: 13px; }
  .share-btn:hover { border-color: var(--cyan); color: var(--cyan);
    background: var(--cyan-soft); }
  /* Save-for-later heart button. Same circular icon-only size as
     the share buttons. Filled red-pink when active, outline when not.
     Persists to localStorage under gn:saved (a JSON array of event ids). */
  .save-btn { width: 28px; height: 28px; padding: 0; border-radius: 50%;
    justify-content: center; line-height: 1; }
  .save-btn svg { width: 14px; height: 14px; fill: none; stroke: currentColor;
    stroke-width: 1.8; transition: fill .15s ease, transform .15s ease; }
  .save-btn:hover { border-color: #e94e7b; color: #e94e7b;
    background: rgba(233,78,123,0.08); }
  .save-btn[data-saved="true"] { border-color: #e94e7b; color: #e94e7b; }
  .save-btn[data-saved="true"] svg { fill: #e94e7b; }
  .save-btn[data-saved="true"]:hover svg { transform: scale(1.1); }
  /* Card corner heart — promoted out of the Share-to-Social row up to
     the top-right of the article so users can save without scrolling
     past the share strip. Sits above the card image with a translucent
     dark plate so it reads on photo-heavy cards.
     Selector raised to .card > .card-save-corner (specificity 0,1,1)
     so it BEATS [data-tooltip] { position: relative } — same trick we
     use for rail-toggle-tab and hero-credit-toggle. Without this the
     heart drops back into the document flow, top-left of the card. */
  .card { position: relative; }
  .card > .card-save-corner { position: absolute; top: 10px; right: 10px; z-index: 6;
    width: 32px; height: 32px; padding: 0; border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: rgba(0,0,0,0.42); color: #fff; border: 1px solid rgba(255,255,255,0.25);
    backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
    cursor: pointer; transition: background .15s, border-color .15s, color .15s; }
  .card > .card-save-corner svg { width: 16px; height: 16px;
    fill: none; stroke: currentColor; stroke-width: 1.8;
    transition: fill .15s ease, transform .15s ease; }
  .card > .card-save-corner:hover { background: rgba(233,78,123,0.85);
    border-color: #e94e7b; color: #fff; }
  .card > .card-save-corner[data-saved="true"] { color: #ffd9e4;
    border-color: #e94e7b; background: rgba(233,78,123,0.78); }
  .card > .card-save-corner[data-saved="true"] svg { fill: #ffd9e4; }
  /* Flag-an-issue corner button. Bottom-right of each card; same
     visual shell as the save-corner heart but anchored to the
     opposite vertical edge so the two affordances don't overlap.
     Outline icon by default; fill when the user has flagged. */
  .card > .card-flag-corner { position: absolute; bottom: 10px; right: 10px; z-index: 6;
    width: 32px; height: 32px; border-radius: 50%;
    background: rgba(255,255,255,0.08);
    border: 0; padding: 0; cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    color: var(--muted);
    transition: background .15s, color .15s, transform .12s; }
  .card > .card-flag-corner svg { width: 14px; height: 14px;
    fill: none; stroke: currentColor; }
  /* Muted red per Gaither 2026-05-14 — the previous vivid red read as
     too assertive. Lower saturation + lower opacity on idle. */
  .card > .card-flag-corner:hover { background: rgba(176,80,80,0.65);
    color: #fff; transform: scale(1.06); }
  .card > .card-flag-corner[data-flagged="true"] { color: #b85454;
    background: rgba(176,80,80,0.55); }
  .card > .card-flag-corner[data-flagged="true"]:hover {
    background: rgba(176,80,80,0.72); }
  .card > .card-flag-corner[data-flagged="true"] svg {
    fill: #fff; stroke: #fff; }
  [data-theme="light"] .card > .card-flag-corner { background: rgba(0,0,0,0.06); }
  [data-theme="light"] .card > .card-flag-corner:hover { background: rgba(176,80,80,0.72); color: #fff; }
  /* Photo / wallpaper / hero photo flag button. Sits TOP-LEFT of image
     cards per Gaither 2026-05-18 (moved from bottom-left so it can't
     get confused with the expand-photo affordance). z-index above
     the photo expand-trigger so clicks register on the flag button
     itself instead of bubbling up to the overlay.
     Originally added per Gaither 2026-05-17 "add a flag button system
     for photos, hero, and wallpaper that's unique to that category
     since they have pics". */
  .photo-card > .photo-flag-corner,
  .hero > .hero-flag-corner {
    position: absolute;
    top: 10px; left: 12px;
    width: 30px; height: 30px;
    border-radius: 50%;
    display: inline-flex; align-items: center; justify-content: center;
    background: rgba(0,0,0,0.42);
    backdrop-filter: blur(4px); -webkit-backdrop-filter: blur(4px);
    color: rgba(255,255,255,0.88);
    border: 1px solid rgba(255,255,255,0.12);
    padding: 0; cursor: pointer;
    pointer-events: auto;
    z-index: 6; /* above .photo-card-overlay + .hero-overlay so clicks
                   never propagate up to a parent expand-trigger */
    transition: background .15s, color .15s, border-color .15s;
  }
  /* Hero-specific override per Gaither 2026-05-18: at the top-left the
     flag was overlapping the 'News' section label. Move it next to the
     'i' info icon in the bottom-right (right side of 'i' = rightmost
     spot). 'i' shifts left to right:50px to make room for the flag. */
  .hero > .hero-flag-corner {
    top: auto; left: auto;
    bottom: 10px; right: 12px;
  }
  .hero > .hero-credit-toggle {
    right: 50px;
  }
  .photo-card > .photo-flag-corner:hover,
  .hero > .hero-flag-corner:hover {
    background: rgba(176,80,80,0.7);
    color: #fff;
    border-color: rgba(255,255,255,0.24);
  }
  .photo-card > .photo-flag-corner svg,
  .hero > .hero-flag-corner svg { width: 14px; height: 14px; }
  .photo-card > .photo-flag-corner[data-flagged="true"],
  .hero > .hero-flag-corner[data-flagged="true"] {
    background: rgba(176,80,80,0.85);
    color: #fff;
  }
  /* Photo flag popover marker — same base styling as the card flag
     popover, but the title reads 'Flag a photo issue'. */
  .flag-popover.photo-flag-popover {
    min-width: 260px;
  }
  [data-theme="light"] .card > .card-flag-corner[data-flagged="true"] {
    background: rgba(176,80,80,0.72); color: #fff; }
  [data-theme="light"] .card > .card-flag-corner[data-flagged="true"] svg {
    fill: #fff; stroke: #fff; }

  /* Flag-reason popover. Anchored to the flag button via JS; the
     panel itself sits in document.body so it can escape the card's
     overflow. */
  .flag-popover { position: absolute; z-index: 100;
    background: var(--card-bg); border: 1px solid var(--line);
    border-radius: 10px; padding: 6px;
    box-shadow: 0 8px 24px rgba(0,0,0,0.25);
    font-family: var(--sans); font-size: 13px;
    min-width: 220px; max-width: 260px; }
  .flag-popover-title { padding: 6px 10px 4px; color: var(--muted);
    font-size: 11px; text-transform: uppercase; letter-spacing: 0.05em;
    font-weight: 700; }
  .flag-popover-option { display: flex; align-items: center; gap: 8px;
    width: 100%; text-align: left; padding: 7px 10px;
    background: transparent; border: 0; border-radius: 6px;
    color: var(--fg); font-family: inherit; font-size: 13px;
    cursor: pointer; transition: background .12s; }
  .flag-popover-option:hover { background: var(--subtle); }
  .flag-popover-option .flag-popover-dot { width: 8px; height: 8px;
    border-radius: 50%; background: var(--muted); flex-shrink: 0; }
  /* Sub-menu chevron on parent options; back arrow at top of sub menu;
     "Not sure / just flag it" option appears at the bottom of sub menus
     so the user always has a way to submit without picking a specific
     sub-reason. Per Gaither 2026-05-16. */
  .flag-popover-chev { margin-left: auto; color: var(--muted); font-size: 14px; }
  /* Small unobtrusive count badge next to each flag reason, shown
     only when the user has flagged that reason >=1 time. Per Gaither
     2026-05-17 "track flags and show number of flags unobtrusively
     per flagged item in picker". */
  .flag-popover-count {
    margin-left: auto;
    min-width: 18px; height: 16px;
    padding: 0 6px;
    background: var(--subtle);
    color: var(--muted);
    border-radius: 999px;
    font-size: 10.5px; font-weight: 600;
    line-height: 16px; text-align: center;
    font-variant-numeric: tabular-nums;
  }
  .flag-popover-option:hover .flag-popover-count {
    background: var(--cyan-soft);
    color: var(--cyan);
  }
  /* If the option ALSO has a chevron (parent reasons), push count
     before the chevron with margin-right. */
  .flag-popover-option .flag-popover-count + .flag-popover-chev {
    margin-left: 6px;
  }
  /* Site-issue popover variant. Same .flag-popover base styles +
     this class lets us widen it for the longer site-issue reason
     labels and pin it to the footer corner.
     z-index 300 sits above the expanded footer sheet (z-index 200)
     so the popover renders in front, not behind it. */
  .site-issue-popover {
    min-width: 280px;
    max-width: 360px;
    max-height: 70vh;
    overflow-y: auto;
    z-index: 300;
  }
  @media (max-width: 720px) {
    .site-issue-popover {
      min-width: 0;
      max-width: calc(100vw - 24px);
      left: 12px !important;
      right: 12px !important;
    }
  }
  .flag-popover-back { background: transparent; border: 0; color: var(--muted);
    font-size: 14px; cursor: pointer; padding: 2px 6px; margin-right: 4px;
    border-radius: 4px; }
  .flag-popover-back:hover { background: var(--subtle); color: var(--fg); }
  /* Close (×) button — anchored top-right of the popover header so the
     user can dismiss without tapping the version pill again or clicking
     outside the popover. */
  .flag-popover-title { display: flex; align-items: center; gap: 6px;
    padding-right: 28px; position: relative; }
  .flag-popover-close { position: absolute; top: 2px; right: 4px;
    background: transparent; border: 0; color: var(--muted);
    font-size: 20px; line-height: 1; cursor: pointer;
    padding: 2px 7px; border-radius: 4px;
    -webkit-tap-highlight-color: transparent; }
  .flag-popover-close:hover { background: var(--subtle); color: var(--fg); }
  .flag-popover-option-skip { color: var(--muted); font-style: italic;
    border-top: 1px solid var(--line); margin-top: 4px; padding-top: 8px; }
  /* "Something else" textarea + buttons. Appears after the user picks
     the "other" reason. Styled to match the rest of the popover. */
  .flag-popover-note {
    width: 100%; box-sizing: border-box;
    margin: 4px 0 8px;
    padding: 8px 10px;
    background: var(--bg);
    color: var(--fg);
    border: 1px solid var(--line);
    border-radius: 6px;
    font-family: inherit;
    font-size: 13px;
    line-height: 1.45;
    resize: vertical;
    min-height: 64px;
  }
  .flag-popover-note:focus {
    outline: none;
    border-color: var(--cyan);
    box-shadow: 0 0 0 2px color-mix(in srgb, var(--cyan) 25%, transparent);
  }
  .flag-popover-actions {
    display: flex; gap: 8px; justify-content: flex-end;
    padding: 0 2px 2px;
  }
  .flag-popover-cancel, .flag-popover-submit {
    background: transparent; border: 1px solid var(--line);
    color: var(--fg); padding: 5px 12px; border-radius: 5px;
    font-family: inherit; font-size: 12.5px; cursor: pointer;
    transition: background .12s, border-color .12s;
  }
  .flag-popover-cancel:hover { background: var(--subtle); }
  .flag-popover-submit {
    background: var(--cyan); border-color: var(--cyan); color: #fff;
  }
  .flag-popover-submit:hover { filter: brightness(1.08); }
  .flag-popover-photo { display: flex; align-items: center; gap: 8px; padding: 4px 2px 0; }
  .flag-popover-photobtn {
    background: transparent; border: 1px solid var(--line); color: var(--muted);
    padding: 4px 10px; border-radius: 5px; font-family: inherit; font-size: 12px; cursor: pointer;
  }
  .flag-popover-photobtn:hover { background: var(--subtle); color: var(--fg); }
  .flag-popover-photoname { font-size: 11.5px; color: var(--cyan); }

  [data-theme="light"] .card > .card-save-corner { background: rgba(255,255,255,0.78);
    color: var(--fg); border-color: var(--line); }
  [data-theme="light"] .card > .card-save-corner:hover { background: rgba(233,78,123,0.92);
    color: #fff; }
  /* Reading-time pill, sits in the card meta row. */
  .pill.read-time { background: transparent; border: 1px solid var(--line);
    color: var(--muted); }
  /* Missing-from-X coverage badge. Reds-out missing-right, blues-out
     missing-left so the political asymmetry reads at a glance. */
  .pill.missing-coverage { font-weight: 600; letter-spacing: 0.02em; }
  .pill.missing-coverage[data-missing="left"] { color: var(--left);
    border-color: var(--left); background: var(--left-soft); }
  .pill.missing-coverage[data-missing="right"] { color: var(--right);
    border-color: var(--right); background: var(--right-soft); }

  /* ------------------------------------------------------------------
     PRINT STYLESHEET — when a reader hits Cmd+P they get a clean
     long-form layout: no header, no rails, no share buttons, no lazy-
     layer call-to-action, expanded L/R panels inline. Each card breaks
     to a new page so the printout reads like a magazine.
     ------------------------------------------------------------------ */
  @media print {
    @page { margin: 0.6in; }
    body { background: #fff !important; color: #111 !important; font-family: 'Source Serif 4', Georgia, serif; }
    header, footer, .rail, .filters, .load-more-row, .modal-overlay,
    .back-to-top, .rail-toggle-tab, .layer-buttons, .read-more-btn,
    .action-row, .hero-credit-toggle, .hero-quote, .hero-overlay .greeting,
    .hero-section-label, .next-refresh, .photo-card { display: none !important; }
    .layout { display: block !important; padding: 0 !important; }
    main, .layout > main { max-width: 100% !important; width: 100% !important;
      padding: 0 !important; margin: 0 !important; }
    .hero { aspect-ratio: auto; min-height: 0; height: auto !important;
      page-break-after: avoid; margin-bottom: 24px; }
    .grid { display: block !important; }
    .card { page-break-inside: avoid; page-break-after: always;
      border: 0 !important; box-shadow: none !important;
      border-left: 3px solid #888 !important;
      padding: 16px 0 !important; margin: 0 0 24px !important;
      background: #fff !important; color: #111 !important; }
    .card-top { display: block !important; }
    .card-side { display: none !important; }
    .card .headline { font-size: 22pt !important; color: #000 !important;
      margin: 0 0 8pt !important; }
    .card .meta { color: #555 !important; font-size: 10pt !important; }
    .card .expanded p { font-size: 12pt !important; line-height: 1.45;
      color: #111 !important; }
    .read-more-box { display: block !important; data-loaded: "true"; }
    .lr-grid { display: grid !important; grid-template-columns: 1fr 1fr !important;
      gap: 16pt; margin-top: 16pt; }
    .lr-col { border: 1pt solid #ddd; padding: 10pt; background: #f8f8f8 !important;
      color: #111 !important; }
    .lr-col h5, .lr-col .lr-headline { color: #000 !important; font-weight: 700; }
    .source-pill { border-color: #999 !important; color: #111 !important; }
    a { color: #00529c !important; text-decoration: underline; }
    a[href]::after { content: " (" attr(href) ")"; font-size: 9pt;
      color: #555; word-break: break-all; }
    a[href^="/"]::after, a[href^="#"]::after { content: ""; }
  }

  /* Coverage gaps + related chips */
  .coverage-gap { margin-top: 14px; font-size: 12px; color: var(--muted); font-style: italic; }
  /* Web-prior-coverage row uses a slightly different label tint so
     readers can tell at a glance these are off-site links. */
  .related-row.related-row-web .related-label { color: var(--cyan); }
  .related-row { margin-top: 14px; padding-top: 12px;
    border-top: 1px dashed var(--line); display: flex; flex-wrap: wrap;
    gap: 6px; align-items: center; }
  .related-row .related-label { font-size: 10px; text-transform: uppercase;
    letter-spacing: 0.12em; color: var(--muted); margin-right: 4px;
    font-weight: 600; width: 100%; margin-bottom: 4px; }
  .related-chip { padding: 4px 10px; border-radius: 6px;
    background: var(--subtle); border: 1px solid var(--line);
    font-size: 12px; color: var(--fg); text-decoration: none;
    transition: border-color .15s, background .15s; }
  .related-chip:hover { border-color: var(--cyan); background: var(--cyan-soft);
    color: var(--cyan); text-decoration: none; }

  /* Custom tooltip system. Any element carrying [data-tooltip] gets a
     small badge above it on hover. Faster + better-looking than the
     native title attribute (which takes ~1.5s to appear). Native title
     stays on the element as an a11y / fallback for keyboard-only users
     who don't trigger :hover. Z-index sits above lightboxes etc. */
  /* Wrapped in :where() so the rule has SPECIFICITY 0,0,0. The tooltip
     ::after needs a positioned ancestor, but any class-level position
     rule (.back-to-top, .rail-toggle-tab, .card-save-corner, etc.) now
     wins automatically — no per-element specificity escalation needed.
     This was the source of the heart-top-left, rail-toggle-flat, and
     back-to-top-missing bugs. */
  :where([data-tooltip]) { position: relative; }
  [data-tooltip]::after {
    content: attr(data-tooltip);
    position: absolute;
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%) translateY(2px);
    background: #0f1419;
    color: #f5f1e8;
    font-family: var(--sans);
    font-size: 11.5px;
    font-weight: 500;
    letter-spacing: 0.02em;
    text-transform: none;
    padding: 5px 9px;
    border-radius: 6px;
    white-space: nowrap;
    pointer-events: none;
    opacity: 0;
    transition: opacity .08s ease, transform .08s ease;
    transition-delay: 0s;
    z-index: 100;
    box-shadow: 0 2px 8px rgba(0,0,0,0.28);
  }
  [data-tooltip]:hover::after,
  [data-tooltip]:focus-visible::after {
    opacity: 1;
    transform: translateX(-50%) translateY(0);
    transition-delay: 0s;
  }
  /* Multi-line tooltip variant. Used for weather + any other tooltip
     whose content is long enough that a single-line treatment looks
     terrible and gets clipped at the viewport edge. Source attribute
     uses literal "
" line breaks; pre-line honors them while still
     collapsing runs of spaces. max-width caps horizontal growth so
     the tooltip wraps cleanly instead of stretching off-screen. Per
     Gaither 2026-05-16: weather tooltips "all one long horizontal
     line" + "getting cutoff terribly". */
  [data-tooltip][data-tooltip-multiline]::after {
    white-space: pre-line;
    /* width: max-content lets the tooltip expand to the natural width
       of its longest line, then max-width caps it. Without max-content
       the tooltip was shrinking to its absolute-positioning anchor
       width (~47px on a narrow weather-day tile) instead of expanding
       to readable widths. */
    width: max-content;
    max-width: 260px;
    text-align: left;
    line-height: 1.45;
    padding: 8px 11px;
  }
  /* Weather-day tooltips have a lot of facts so cap wider at 340px
     per Gaither 2026-05-16. The max-content sizing above also has to
     be ANCHORED here so the tooltip can extend past the tiny weather
     tile. */
  .weather-day[data-tooltip][data-tooltip-multiline]::after {
    max-width: 340px;
    min-width: 220px;
  }
  /* Forecast-day tooltips anchor to a narrow tile and tend to overflow
     the right edge of the viewport on the last weekday. The viewport-
     aware tooltip JS auto-flips; for the rare static cases use
     data-tooltip-anchor="right" or "left" on the trigger. */
  /* When the header is in its scrolled/compact state, anything inside
     it that fires a tooltip would push the tooltip ABOVE the header,
     where the body sticky region clips it. Force these tooltips BELOW
     the element so they appear in the page area where there's room. */
  header.scrolled [data-tooltip]::after {
    bottom: auto;
    top: calc(100% + 6px);
    transform: translateX(-50%) translateY(-2px);
  }
  header.scrolled [data-tooltip]:hover::after,
  header.scrolled [data-tooltip]:focus-visible::after {
    transform: translateX(-50%) translateY(0);
  }
  /* Suppress a parent tooltip when one of its [data-tooltip]
     descendants is being hovered. Without this, hovering the count
     badge inside the All chip fires BOTH the chip's "Show / hide all"
     tooltip AND the count's "Total unread" tooltip — they cascade
     because :hover is true for every ancestor. Gaither 2026-05-13. */
  [data-tooltip]:has([data-tooltip]:hover)::after,
  [data-tooltip]:has([data-tooltip]:focus-visible)::after {
    opacity: 0 !important;
  }
  /* Tooltip placed BELOW the element instead of above. Use this on
     elements at the top of a clipping container (like the rail header
     edit pencil) where the upward-floating tooltip otherwise gets cut
     off by the rail's overflow-y: auto. */
  [data-tooltip-place="below"]::after {
    bottom: auto;
    top: calc(100% + 6px);
    transform: translateX(-50%) translateY(-2px);
  }
  [data-tooltip-place="below"]:hover::after,
  [data-tooltip-place="below"]:focus-visible::after {
    transform: translateX(-50%) translateY(0);
  }
  [data-theme="light"] [data-tooltip]::after {
    background: #1a1a1a;
    color: #fafaf7;
  }
  /* Tooltip edge-clipping fix. On narrow viewports, a centered tooltip
     on a header icon near the left or right edge gets clipped off the
     viewport. Add edge-aware variants the JS can flip on per-element
     when it detects the tooltip would overflow. */
  [data-tooltip-anchor="left"]::after {
    left: 0;
    transform: translateX(0) translateY(2px) !important;
  }
  [data-tooltip-anchor="left"]:hover::after,
  [data-tooltip-anchor="left"]:focus-visible::after {
    transform: translateX(0) translateY(0) !important;
  }
  [data-tooltip-anchor="right"]::after {
    left: auto;
    right: 0;
    transform: translateX(0) translateY(2px) !important;
  }
  [data-tooltip-anchor="right"]:hover::after,
  [data-tooltip-anchor="right"]:focus-visible::after {
    transform: translateX(0) translateY(0) !important;
  }

  /* Wireframe-SVG tooltip popovers — replace the data-tooltip + emoji
     CSS-pseudo tooltip for weather day tiles and the today chip. The
     CSS pseudo can only render attr() text so emojis were the only way
     to include glyphs. A real HTML child lets us inline the same line-
     art SVGs the day tiles use. Per Gaither 2026-05-17. */
  .weather-day-tip, .weather-now-tip {
    display: none; position: absolute;
    top: calc(100% + 6px); left: 50%;
    transform: translateX(-50%);
    background: #0f1419; color: #f5f1e8;
    font-family: var(--sans); font-size: 11.5px;
    font-weight: 500; letter-spacing: 0.02em;
    text-align: left; line-height: 1.45;
    padding: 8px 11px; border-radius: 8px;
    width: max-content; max-width: 260px;
    pointer-events: none; z-index: 100;
    box-shadow: 0 2px 8px rgba(0,0,0,0.28);
    white-space: normal;
  }
  .weather-day { position: relative; }
  .weather-now { position: relative; }
  .weather-day-tip { min-width: 200px; max-width: 280px; }
  .weather-now-tip { min-width: 200px; max-width: 280px; }
  .weather-day:hover .weather-day-tip,
  .weather-day:focus-within .weather-day-tip,
  .weather-now:hover .weather-now-tip,
  .weather-now:focus-within .weather-now-tip { display: block; }
  /* Edge-aware anchors: first tile in the row pins left, last few pin
     right, matching the data-tooltip-anchor logic this replaced. */
  .weather-day[data-tip-anchor="left"] .weather-day-tip {
    left: 0; transform: translateX(0);
  }
  .weather-day[data-tip-anchor="right"] .weather-day-tip {
    left: auto; right: 0; transform: translateX(0);
  }
  /* Tooltip rows: more vertical spacing + slightly larger icons per
     Gaither 2026-05-17 ("make them a little more vertical for the six
     individual things"). Each row reads as a discrete metric card. */
  .weather-day-tip .wdt-row,
  .weather-now-tip .wdt-row {
    display: flex; align-items: center; gap: 8px;
    padding: 4px 0;
    line-height: 1.3;
  }
  .weather-day-tip .wdt-row svg,
  .weather-now-tip .wdt-row svg {
    width: 15px; height: 15px; color: var(--cyan);
    flex-shrink: 0;
  }
  .weather-day-tip .wdt-row.wdt-head,
  .weather-now-tip .wdt-row.wdt-head {
    font-weight: 700; padding-bottom: 3px;
    margin-bottom: 3px; border-bottom: 1px solid rgba(255,255,255,0.12);
  }
  .weather-day-tip .wdt-row .wdt-label,
  .weather-now-tip .wdt-row .wdt-label {
    color: #b8b3a3; flex: 1;
  }
  .weather-day-tip .wdt-row .wdt-value,
  .weather-now-tip .wdt-row .wdt-value {
    color: #f5f1e8; font-weight: 600;
  }
  [data-theme="light"] .weather-day-tip,
  [data-theme="light"] .weather-now-tip {
    background: #1a1a1a; color: #fafaf7;
  }

  /* Layer buttons */
  .layer-buttons { display: flex; gap: 8px; margin-top: 16px; flex-wrap: wrap; }
  .layer-btn { background: transparent; border: 1px solid var(--line);
    color: var(--fg); padding: 8px 14px; border-radius: 8px;
    font-size: 13px; cursor: pointer; font-family: inherit; font-weight: 500;
    transition: border-color .15s, background .15s, color .15s; }
  .layer-btn:hover { background: var(--subtle); }
  .layer-btn[data-kind="cynic"] { border-color: rgba(193,21,119,0.4); color: var(--magenta); }
  .layer-btn[data-kind="cynic"]:hover { border-color: var(--magenta);
    background: rgba(193,21,119,0.08); }
  [data-theme="dark"] .layer-btn[data-kind="cynic"]:hover { background: rgba(238,43,139,0.10); }
  .layer-btn[data-kind="differences"] { border-color: rgba(107,0,194,0.4); color: var(--purple); }
  .layer-btn[data-kind="differences"]:hover { border-color: var(--purple);
    background: rgba(107,0,194,0.08); }
  [data-theme="dark"] .layer-btn[data-kind="differences"]:hover { background: rgba(165,96,238,0.10); }
  .layer-btn[data-kind="money"] { border-color: rgba(168,124,38,0.45); color: #a87c26; }
  [data-theme="dark"] .layer-btn[data-kind="money"] { color: #d6b866; border-color: rgba(214,184,102,0.45); }
  .layer-btn[data-kind="money"]:hover { border-color: #a87c26; background: rgba(168,124,38,0.10); }
  [data-theme="dark"] .layer-btn[data-kind="money"]:hover { background: rgba(214,184,102,0.12); }
  .layer-btn[data-kind="compare"] { border-color: rgba(58,170,100,0.4); color: #2f8a55; }
  [data-theme="dark"] .layer-btn[data-kind="compare"] { color: #6fcb95; border-color: rgba(111,203,149,0.45); }
  .layer-btn[data-kind="compare"]:hover { border-color: #2f8a55; background: rgba(58,170,100,0.10); }
  [data-theme="dark"] .layer-btn[data-kind="compare"]:hover { background: rgba(111,203,149,0.12); }
  /* Counterpoint layer (2026-06-10) — brand cyan family. */
  .layer-btn[data-kind="counterpoint"] { border-color: rgba(0,153,204,0.45); color: var(--cyan); }
  .layer-btn[data-kind="counterpoint"]:hover { border-color: var(--cyan); background: rgba(0,153,204,0.08); }
  [data-theme="dark"] .layer-btn[data-kind="counterpoint"]:hover { background: rgba(1,204,255,0.10); }
  .layer-btn[data-loaded="true"] { opacity: 0.6; }
  .layer-box { display: none; margin-top: 12px; padding: 16px;
    border: 1px solid var(--line); border-radius: 10px;
    background: var(--subtle); font-size: 14px; line-height: 1.6; color: var(--fg); }
  .layer-box[data-visible="true"] { display: block; }
  .layer-box.cynic       { border-color: rgba(193,21,119,0.3); }
  .layer-box.differences { border-color: rgba(107,0,194,0.35); }
  .layer-box.money       { border-color: rgba(168,124,38,0.4); }
  .layer-box.compare     { border-color: rgba(58,170,100,0.35); }
  .layer-box.counterpoint { border-color: rgba(0,153,204,0.4); }
  .layer-box .layer-head { display: flex; justify-content: space-between;
    align-items: center; margin-bottom: 8px; }
  .layer-box .layer-title { font-size: 11px; text-transform: uppercase;
    letter-spacing: 0.12em; color: var(--muted); font-weight: 600; }
  .layer-box.cynic       .layer-title { color: var(--magenta); }
  .layer-box.differences .layer-title { color: var(--purple); }
  .layer-box.money       .layer-title { color: #a87c26; }
  [data-theme="dark"] .layer-box.money .layer-title { color: #d6b866; }
  .layer-box.counterpoint .layer-title { color: var(--cyan); }
  .layer-box.compare     .layer-title { color: #2f8a55; }
  [data-theme="dark"] .layer-box.compare .layer-title { color: #6fcb95; }
  .layer-box .layer-hide { background: transparent; border: 0;
    color: var(--muted); font-size: 12px; cursor: pointer;
    padding: 2px 6px; font-family: inherit; }
  .layer-box .layer-hide:hover { color: var(--fg); }
  .layer-box h1, .layer-box h2, .layer-box h3 {
    font-family: var(--serif); font-size: 16px; margin: 14px 0 6px;
    letter-spacing: -0.01em; font-weight: 600; }
  .layer-box h1 { font-size: 18px; }
  .layer-box p { margin: 0 0 10px; }
  .layer-box ul { margin: 6px 0 10px; padding-left: 20px; }
  .layer-box li { margin: 0 0 4px; }
  .layer-box strong { color: var(--fg); font-weight: 600; }
  .layer-box em { color: var(--fg); opacity: 0.85; }
  .layer-box code { background: var(--subtle); padding: 1px 5px;
    border-radius: 3px; font-size: 12px; font-family: ui-monospace, monospace; }
  .layer-loading { color: var(--muted); font-style: italic;
    display: flex; flex-direction: column; align-items: center; gap: 12px;
    padding: 18px 0; }
  .layer-loading-hint { font-size: 12px; opacity: 0.75; font-style: normal; }
  .layer-spinner { width: 22px; height: 22px; border-radius: 50%;
    border: 2px solid var(--line); border-top-color: var(--cyan);
    animation: gn-spin 0.7s linear infinite; }
  @keyframes gn-spin { to { transform: rotate(360deg); } }
  .dots span { display: inline-block; opacity: 0.3;
    animation: gn-dots 1.2s infinite; }
  .dots span:nth-child(2) { animation-delay: 0.15s; }
  .dots span:nth-child(3) { animation-delay: 0.3s; }
  @keyframes gn-dots { 0%, 80%, 100% { opacity: 0.3; } 40% { opacity: 1; } }
  /* Rename to .feed-empty so the global landing-state styling no longer
     collides with .lr-col.empty (which inherits padding from .lr-col).
     The 80px padding-top here was bleeding into every prior-coverage
     panel and pushing the heading way down inside the right card. */
  .feed-empty { padding: 80px 20px; text-align: center; color: var(--muted);
    grid-column: 1 / -1; /* span full grid width so the message centers under the whole feed area instead of being trapped in the first column */
    max-width: 720px; margin: 0 auto;
  }

  /* Modal (keyboard-shortcut help) */
  .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5);
    display: none; align-items: center; justify-content: center; z-index: 100; }
  .modal-overlay[data-visible="true"] { display: flex; }
  .modal { background: var(--card-bg); border: 1px solid var(--line);
    border-radius: 12px; padding: 24px 28px; max-width: 460px; width: 90%;
    box-shadow: 0 8px 32px rgba(0,0,0,0.2); color: var(--fg); }
  .modal h2 { font-family: var(--serif); font-size: 20px; margin: 0 0 16px; }
  .modal kbd { background: var(--subtle); border: 1px solid var(--line);
    border-radius: 4px; padding: 2px 6px; font-family: ui-monospace, monospace;
    font-size: 12px; }
  .modal table { width: 100%; border-collapse: collapse; }
  .modal td { padding: 6px 0; font-size: 13px; }
  .modal td:first-child { width: 50px; }
  .modal-close { background: transparent; border: 0; color: var(--muted);
    cursor: pointer; float: right; font-size: 18px; padding: 0 4px;
    margin-top: -8px; font-family: inherit; }

  /* Card focus highlight (j/k navigation). */
  .card[data-focused="true"] { border-color: var(--cyan);
    box-shadow: 0 0 0 2px var(--cyan-soft), var(--card-shadow); }

  /* Static-ish prose pages (privacy, methodology, api). */
  .prose { font-size: 16px; line-height: 1.65; color: var(--fg); }
  .prose h1 { font-family: var(--serif); font-size: 32px; letter-spacing: -0.02em;
    margin: 0 0 20px; }
  .prose h2 { font-family: var(--serif); font-size: 22px; margin: 28px 0 8px;
    letter-spacing: -0.01em; }
  .prose h3 { font-family: var(--serif); font-size: 17px; margin: 18px 0 6px; }
  .prose p { margin: 0 0 12px; }
  .prose ul { margin: 6px 0 14px; padding-left: 22px; }
  .prose li { margin: 0 0 4px; }
  .prose code { background: var(--subtle); padding: 1px 5px; border-radius: 3px;
    font-family: ui-monospace, monospace; font-size: 13px; }
  .prose pre { background: var(--subtle); padding: 12px 14px; border-radius: 8px;
    overflow-x: auto; font-size: 12.5px; }

  /* Compare-outlets picker */
  .compare-picker { display: flex; flex-wrap: wrap; gap: 10px;
    align-items: center; }
  .compare-picker select { background: var(--card-bg); color: var(--fg);
    border: 1px solid var(--line); border-radius: 6px;
    padding: 6px 10px; font-family: inherit; font-size: 13px;
    max-width: 100%; }
  .compare-picker .compare-go { background: var(--cyan); color: #fff;
    border: 0; padding: 6px 16px; border-radius: 999px;
    font-family: inherit; font-size: 13px; font-weight: 600;
    cursor: pointer; transition: opacity .15s; }
  .compare-picker .compare-go:hover { opacity: 0.9; }

  /* Compare side-by-side page */
  .compare-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
    margin-top: 14px; }
  @media (max-width: 720px) { .compare-grid { grid-template-columns: 1fr; } }
  .compare-col { background: var(--card-bg); border: 1px solid var(--line);
    border-radius: 10px; padding: 18px 20px; }
  .compare-col h2 { font-family: var(--serif); font-size: 22px;
    font-weight: 600; line-height: 1.25; margin: 0 0 10px;
    letter-spacing: -0.01em; color: var(--fg); }
  .compare-col .source-name { font-size: 13px; color: var(--cyan);
    font-weight: 600; margin: 0 0 6px; }
  .compare-col .compare-meta { display: flex; gap: 8px; flex-wrap: wrap;
    font-size: 11px; color: var(--muted); margin: 6px 0 12px; }
  .compare-col .compare-meta .pill { font-size: 11px; }
  .compare-col p.compare-desc { font-size: 14px; line-height: 1.6;
    margin: 6px 0 10px; color: var(--fg); }
  .compare-col p.compare-summary { font-size: 14px; line-height: 1.6;
    margin: 6px 0 10px; color: var(--fg); }
  .compare-col h4 { font-size: 11px; text-transform: uppercase;
    letter-spacing: 0.12em; color: var(--muted); margin: 14px 0 4px;
    font-weight: 600; }
  .compare-col .compare-link { font-size: 13px; word-break: break-word; }

  footer-deprecated { /* legacy <footer> tag no longer used directly */ }
  /* Unified bottom strip. Closed (default): only the persistent stats
     row is visible — the row itself is the click target to expand.
     Open: chevron at the top (close affordance), expandable content
     panel in the middle (links/support/network), stats row at the
     bottom. Pinned to viewport bottom so it stays anchored regardless
     of main-content length. Per Gaither 2026-05-14. */
  .footer-bar {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 50;
    background: var(--card-bg);
    border-top: 1px solid var(--line);
    color: var(--muted);
    font-family: var(--sans); font-size: 11px;
    box-shadow: 0 -2px 10px rgba(0,0,0,0.12);
    backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px);
    display: flex;
    flex-direction: column;
    max-height: 40vh;
  }
  /* Scroll-slim state (mobile/tablet, toggled by JS): a thin stats line so the
     fixed footer stops covering the bottom of the viewport. */
  .footer-bar.footer-slim { font-size: 9px; box-shadow: none; }
  .footer-bar.footer-slim .footer-toggle { display: none; }
  .footer-bar.footer-slim .footer-statsrow { padding-top: 2px !important; padding-bottom: 2px !important; }
  .footer-bar a { color: var(--muted); text-decoration: none; }
  .footer-bar a:hover { color: var(--cyan); }
  /* Chevron toggle at the top edge of the strip. Centered, small. */
  .footer-toggle {
    align-self: center;
    width: 44px;
    height: 16px;
    padding: 0;
    margin: 2px 0 0;
    background: transparent;
    border: 0;
    color: var(--muted);
    cursor: pointer;
    display: inline-flex; align-items: center; justify-content: center;
    transition: color .15s;
  }
  .footer-toggle:hover { color: var(--cyan); }
  .footer-chevron {
    width: 18px; height: 18px; display: block;
    transition: transform .2s ease;
  }
  .footer-bar[data-open="true"] .footer-chevron { transform: rotate(180deg); }
  .footer-bar[data-open="true"] .footer-toggle { color: var(--cyan); }
  /* Closed state: chevron is hidden entirely; the stats row itself
     becomes the click/keyboard target to expand the footer. Open
     state shows the chevron (pointing DOWN after rotation) so users
     have a clear "close" affordance. Per Gaither 2026-05-14. */
  .footer-bar[data-open="false"] .footer-toggle { display: none; }
  .footer-bar[data-open="false"] .footer-statsrow {
    cursor: pointer;
  }
  .footer-bar[data-open="false"] .footer-statsrow:focus-visible {
    outline: 2px solid var(--cyan); outline-offset: -2px;
  }
  /* Expandable middle panel: zero height collapsed; just enough to
     show 4 compact rows when open. The bar grows upward because
     bottom is anchored. */
  .footer-expanded {
    max-height: 0; overflow: hidden;
    text-align: center;
    color: var(--muted); font-size: 13px;
    transition: max-height .25s ease, padding .25s ease;
    padding: 0 24px;
  }
  .footer-bar[data-open="true"] .footer-expanded {
    max-height: 140px; padding: 4px 24px 6px;
  }
  .footer-expanded > * { margin-top: 4px; }
  .footer-expanded > *:first-child { margin-top: 0; }
  .footer-tagline { margin: 0; }
  /* Stats row at the bottom — always visible, identical to the old
     sticky-stats layout. */
  .footer-statsrow {
    display: grid; align-items: center;
    grid-template-columns: auto 1fr auto;
    gap: 12px;
    padding: 6px 18px;
  }
  /* Copyright (left) | article numbers (center, 1fr) | version + flag (right).
     Grouping the version + flag buttons keeps them in the single right grid
     cell instead of wrapping the version onto a second row. */
  .footer-right { display: flex; align-items: center; gap: 8px; justify-self: end; }
  /* Gaither Network row: small label + horizontal site links. */
  .footer-network { display: flex; flex-wrap: wrap;
    justify-content: center; align-items: baseline;
    gap: 6px 14px; font-size: 12.5px;
    padding-top: 4px; border-top: 1px solid var(--line);
    margin-top: 8px; }
  .footer-network-label { font-weight: 600; letter-spacing: 0.04em;
    text-transform: uppercase; color: var(--fg); opacity: 0.7;
    font-size: 11px; margin-right: 4px; }
  .footer-network a { color: var(--cyan); text-decoration: none; }
  .footer-network a:hover { text-decoration: underline; }
  /* Slim sticky stats bar — always visible at the bottom of the
     viewport. Mirrors the footer-stats numbers in a compact strip
     so the user always knows how fresh the feed is. Hidden on
     mobile (where vertical space is precious). */
  .sticky-stats { position: fixed; left: 0; right: 0; bottom: 0;
    z-index: 50;
    display: grid; align-items: center;
    /* Three columns: copyright (left) | stats (center, takes the
       remaining width and wraps) | version (right). The 1fr middle
       lets the stats expand and absorb any extra space so the side
       items stay anchored to the corners. */
    grid-template-columns: auto 1fr auto;
    gap: 12px;
    padding: 6px 18px;
    background: var(--card-bg);
    color: var(--muted);
    font-family: var(--sans); font-size: 11px;
    border-top: 1px solid var(--line);
    box-shadow: 0 -2px 10px rgba(0,0,0,0.12);
    backdrop-filter: blur(6px); -webkit-backdrop-filter: blur(6px); }
  .sticky-stats-copy { white-space: nowrap;
    font-size: 10.5px; opacity: 0.7;
    letter-spacing: 0.03em;
    cursor: pointer; position: relative; }
  .sticky-stats-copy:hover { opacity: 1; color: var(--cyan); }
  /* Gaither Network site menu — pops up above the copyright when
     clicked. Per Gaither 2026-05-15. */
  .network-menu { position: fixed; bottom: 32px; left: 12px;
    z-index: 60;
    display: flex; flex-direction: column;
    background: var(--card-bg);
    border: 1px solid var(--line);
    border-radius: 8px;
    box-shadow: 0 4px 18px rgba(0,0,0,0.18);
    padding: 6px 0;
    min-width: 200px;
    font-family: var(--sans); font-size: 13px; }
  .network-menu[hidden] { display: none; }
  .network-menu a { color: var(--fg); text-decoration: none;
    padding: 8px 14px; display: block; }
  .network-menu a:hover { background: var(--line);
    color: var(--cyan); }
  /* Version number is the clickable site-issue button. No separate
     flag icon. Per Gaither 2026-05-17 "i didn't want an additional
     flag icon for website issues...just replace the version number
     with a clickable version number". Looks identical to the old
     read-only version string but is interactive. */
  .footer-version-btn {
    background: transparent; border: 0; padding: 0;
    cursor: pointer; position: relative;
    font: inherit; color: inherit;
    transition: color .15s, opacity .15s;
  }
  .footer-version-btn:hover,
  .footer-version-btn:focus-visible {
    color: var(--cyan); opacity: 1; outline: none;
  }
  .site-issue-count {
    display: inline-block;
    margin-left: 5px;
    min-width: 16px; height: 14px;
    padding: 0 4px;
    background: var(--cyan); color: var(--bg);
    border-radius: 999px;
    font-family: var(--sans); font-size: 9.5px; font-weight: 700;
    line-height: 14px; text-align: center;
    vertical-align: middle;
  }
  .site-issue-count[hidden] { display: none; }
  /* Footer-expanded version button row — centered. Appears on
     mobile/tablet where the inline footer strip's version is
     hidden. */
  .footer-version-row {
    display: flex; justify-content: center;
    margin-top: 8px;
  }
  .footer-version-row .footer-version-btn {
    font-size: 11px;
    opacity: 0.7;
    letter-spacing: 0.03em;
    font-variant-numeric: tabular-nums;
  }
  .sticky-stats-version { white-space: nowrap;
    font-size: 10.5px; opacity: 0.7;
    letter-spacing: 0.03em;
    font-variant-numeric: tabular-nums; }
  .sticky-stats-body { display: flex; justify-content: center;
    align-items: center; gap: 3px 8px; flex-wrap: wrap;
    min-width: 0; }
  .sticky-stats-label { font-weight: 600; color: var(--fg);
    opacity: 0.85; margin-right: 2px; }
  .sticky-stats-pair { white-space: nowrap; }
  .sticky-stats strong { color: var(--fg); font-weight: 700; }
  .sticky-stats .sticky-stats-sep { opacity: 0.4; padding: 0 2px; }
  /* Tablet (721-1024) footer mirrors mobile per Gaither 2026-05-18:
     copyright tap expands a bottom-sheet, network menu hidden,
     desktop-only inline links removed. Collapsed strip shows ONLY
     copyright; expanded panel contains version + links + flag system. */
  @media (min-width: 721px) and (max-width: 1024px) {
    .footer-statsrow .sticky-stats-body,
    .footer-statsrow #footerVersion,
  .footer-statsrow #footerFlag,
  .footer-statsrow .footer-right { display: none !important; }
  .footer-flag-icon { margin-right: 5px; }
  .footer-flag-icon svg { display: inline-block; vertical-align: -1px; }
    /* Per Gaither 2026-05-18: hide the inline Gaither Network popover
       on tablet — it's a desktop-only affordance. Tablet uses the
       expanded sheet to surface the network links instead. */
    .footer-statsrow .network-menu { display: none !important; }
    .footer-statsrow .sticky-stats-copy {
      display: inline-flex !important;
      font-size: 13px;
      letter-spacing: 0.04em;
      font-weight: 500;
      color: var(--fg); opacity: 0.85;
    }
    .footer-statsrow {
      justify-content: center;
      grid-template-columns: auto;
      gap: 10px;
      cursor: pointer;
      min-height: 44px;
    }
    /* Tablet expanded = bottom-sheet, same pattern as mobile. */
    .footer-bar[data-open="true"] {
      position: fixed;
      top: auto;
      bottom: 0; left: 0; right: 0;
      height: auto;
      max-height: 80vh;
      z-index: 200;
      background: var(--bg);
      border-top: 1px solid var(--line);
      box-shadow: 0 -12px 36px rgba(0,0,0,0.35);
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }
    .footer-bar[data-open="true"] .footer-expanded {
      flex: 1 1 auto;
      min-height: 0;
      max-height: none;
      overflow-y: auto;
      overscroll-behavior: contain;
      -webkit-overflow-scrolling: touch;
      scrollbar-width: none;
      padding: 24px 24px 18px;
      gap: 16px;
      display: flex;
      flex-direction: column;
    }
    .footer-bar[data-open="true"] .footer-expanded::-webkit-scrollbar {
      display: none;
    }
    .footer-bar[data-open="true"] .footer-statsrow {
      flex: 0 0 auto;
      border-top: 1px solid var(--line);
    }
  }
  /* Page-bottom clearance for the unified fixed footer-bar in its
     CLOSED state. Closed = stats row only (no chevron above it):
     ~30px (12px padding + ~16px text + 1px border). Per Gaither 2026-05-14. */
  body { padding-bottom: 36px; }
  /* Narrow viewports: collapse the stats row to a copyright-only
     strip. Tapping anywhere on the strip expands the full footer.
     Per Gaither 2026-05-15. */
  @media (max-width: 720px) {
    .footer-statsrow .sticky-stats-body,
    .footer-statsrow .sticky-stats-version,
    .footer-statsrow .footer-right { display: none; }
    /* Network-menu popover is desktop-only; on mobile the tap should
       expand the full footer (which contains the same network links). */
    .footer-statsrow .network-menu { display: none !important; }
    .footer-statsrow {
      justify-content: center;
      grid-template-columns: auto;
      padding: 14px 16px;
      cursor: pointer;
      min-height: 44px;
    }
    .footer-statsrow .sticky-stats-copy {
      font-size: 13px; opacity: 1; color: var(--fg);
      letter-spacing: 0.04em; font-weight: 500;
      cursor: pointer;
    }
    /* Suppress the tooltip on mobile — the copy itself is the affordance,
       and tooltips don't render usefully on touch anyway. Per Gaither
       2026-05-15. */
    .footer-statsrow[data-tooltip],
    .footer-statsrow .sticky-stats-copy[data-tooltip],
    .footer-statsrow[role="button"][data-tooltip]::after,
    .footer-statsrow[role="button"][data-tooltip]::before {
      /* CSS-only tooltip system uses ::after on the [data-tooltip] target,
         not the attribute itself — kill both attribute lookup + pseudo. */
      content: none !important;
    }
    .footer-bar[data-open="false"] .footer-toggle { display: none; }
    body { padding-bottom: 48px; }
    /* Expanded footer must fit fully in the viewport on mobile so the
       whole panel is reachable without clipping. Cap to 80vh and let
       it scroll internally if content is taller than that. Per Gaither
       2026-05-15: 'make sure it expands up enough to be fully visible
       when expanded'. */
    /* Mobile expanded = TRUE bottom-sheet per Gaither 2026-05-18.
       Earlier version anchored top:0 which made the panel slide down
       from the top of the screen — wrong direction. Now anchored
       bottom:0 with a viewport-cap height; the sheet expands UPWARD
       from the collapsed strip, the strip stays visible as the close
       affordance, and the panel content scrolls internally. */
    .footer-bar[data-open="true"] {
      position: fixed;
      top: auto;
      bottom: 0; left: 0; right: 0;
      /* Per Gaither 2026-05-18: auto-fit to content, not full viewport.
         Sheet grows only as tall as it needs. 80vh cap so very tall
         content still scrolls inside instead of pushing the close
         strip off-screen. */
      height: auto;
      max-height: 80vh;
      z-index: 200;
      background: var(--bg);
      border-top: 1px solid var(--line);
      box-shadow: 0 -12px 36px rgba(0,0,0,0.35);
      display: flex;
      flex-direction: column;
      overflow: hidden; /* outer never scrolls; inner panel does */
    }
    /* Inner panel content does the scrolling. flex:1 so it claims all
       space above the collapsed strip; min-height:0 lets the flex
       child actually shrink + scroll inside. */
    .footer-bar[data-open="true"] .footer-expanded {
      flex: 1 1 auto;
      min-height: 0;
      max-height: none;
      overflow-y: auto;
      overscroll-behavior: contain;
      -webkit-overflow-scrolling: touch;
      scrollbar-width: none;
      padding: 24px 20px 18px;
      gap: 18px;
      display: flex;
      flex-direction: column;
    }
    .footer-bar[data-open="true"] .footer-expanded::-webkit-scrollbar {
      display: none;
    }
    .footer-bar[data-open="true"] .footer-version-row {
      margin-top: 8px;
      padding-bottom: 4px;
    }
    /* When expanded the inline collapsed strip sits at the BOTTOM
       and stays visible as the close affordance. flex-shrink:0 so it
       never collapses, plus a thin top border for separation. */
    .footer-bar[data-open="true"] .footer-statsrow {
      flex: 0 0 auto;
      border-top: 1px solid var(--line);
      padding: 10px 16px calc(10px + env(safe-area-inset-bottom, 0px));
    }
    .footer-bar[data-open="true"] .footer-expanded > * { margin-top: 0; }
    .footer-bar[data-open="true"] .footer-tagline { font-size: 14.5px; line-height: 1.45; }
    .footer-bar[data-open="true"] .footer-links { gap: 10px 18px; font-size: 13.5px; }
    .footer-bar[data-open="true"] .footer-network { gap: 8px 16px; font-size: 13px; }
    .footer-bar[data-open="true"] .footer-support { padding: 6px 0 2px; }
    .footer-bar[data-open="true"] .footer-version-row { margin-top: 10px; padding-bottom: 6px; }
  }
  .footer-bar .footer-links { margin-top: 8px; display: flex; justify-content: center;
    flex-wrap: wrap; gap: 6px 14px; }
  /* Footer support pill — sits BELOW the footer-links row so it reads
     as a soft "if you've gotten value from this, here's how to help"
     ask, not a hard CTA competing with the Methodology / Sources nav.
     Uses brand cyan to match the rest of the site rather than BMC's
     default purple, so it doesn't jar against the calm palette. */
  .footer-bar .footer-support { margin-top: 18px; display: flex;
    justify-content: center; }
  .footer-bar .footer-support .bmc-pill { display: inline-flex; align-items: center;
    gap: 8px; padding: 6px 14px; border-radius: 999px;
    background: var(--cyan); color: #fff;
    font-family: var(--sans); font-size: 12px; font-weight: 600;
    letter-spacing: 0.01em; text-decoration: none;
    box-shadow: 0 1px 2px rgba(0,0,0,0.08), 0 4px 14px rgba(0,153,204,0.18);
    transition: transform .12s ease, box-shadow .12s ease, background .15s; }
  .footer-bar .footer-support .bmc-pill:hover { transform: translateY(-1px);
    background: #007ba8;
    box-shadow: 0 2px 4px rgba(0,0,0,0.10), 0 6px 18px rgba(0,153,204,0.28); }
  .footer-bar .footer-support .bmc-pill svg { width: 16px; height: 16px; flex: 0 0 16px; }
  [data-theme="dark"] .footer-bar .footer-support .bmc-pill { background: var(--cyan);
    color: #0a1419;
    box-shadow: 0 1px 2px rgba(0,0,0,0.45), 0 4px 14px rgba(1,204,255,0.20); }
  [data-theme="dark"] .footer-bar .footer-support .bmc-pill:hover { background: #4dd8ff; }

  /* Header support pill — lives in .header-icon-row alongside font-size,
     help, theme. Deliberately muted: transparent background, --line
     border, --muted text. Matches the .icon-btn chrome so it blends
     instead of shouting. Same cyan hover treatment the other header
     icons already use. Replaced the old bottom-right .bmc-floating
     (removed 2026-05-12 per Gaither — neon and off-brand). */
  /* Buy Me a Coffee brand yellow so the Support action draws the eye
     (matches the BMC button colour). Per network flag 2026-06-22. */
  .bmc-header { display: inline-flex; align-items: center; gap: 6px;
    padding: 6px 12px; border-radius: 999px;
    background: #FFDD00; border: 1px solid #FFDD00;
    color: #11150c;
    font-family: var(--sans); font-size: 12.5px; font-weight: 600;
    letter-spacing: 0.01em; text-decoration: none;
    transition: filter .15s, transform .15s; }
  .bmc-header:hover { filter: brightness(1.06); transform: translateY(-1px);
    background: #FFDD00; border-color: #FFDD00; color: #11150c; text-decoration: none; }
  .bmc-header svg { width: 14px; height: 14px; }
  [data-theme="dark"] .bmc-header { background: #FFDD00; border-color: #FFDD00; color: #11150c; }
  [data-theme="dark"] .bmc-header:hover { background: #FFDD00; border-color: #FFDD00;
    color: #11150c; filter: brightness(1.06); }
  /* Tablet scroll-collapse: drop the label so the pill matches the
     compressed icon-only header strip. Icon stays visible. */
  @media (max-width: 1024px) {
    header.scrolled .bmc-header { padding: 6px 10px; }
    header.scrolled .bmc-header-label { display: none; }
  }
  /* Tablet AT ANY SCROLL STATE hides the "Support" label per Gaither
     2026-05-16. Icon stays so the action is still discoverable. */
  @media (max-width: 1024px) {
    .bmc-header-label { display: none; }
    .bmc-header { padding: 6px 10px; }
  }