/* ══════════════════════════════════════════════════════════════════════════
   UBH — DESKTOP "SPECTRUM" VIEW MODE  (approved design: Option C, v5)
   ══════════════════════════════════════════════════════════════════════════
   Source of truth: /data/nicos/scratch/ubh-desktop-ux/lib/c-system.css
   (C-spectrum-grid.html, C-spectrum-loggedin.html, C-category-*.html,
    C-topic-iran.html)

   ⛔ MOBILE SAFETY CONTRACT — read before editing this file.
   ---------------------------------------------------------------------------
   EVERY rule in this file is gated by `@media (min-width: 769px)` — never
   evaluated at 768px or below. `grep -c` for `@media` vs. top-level rules is
   part of the PR evidence (verified: exactly ONE top-level block, the media
   query).

   ⛔ 769 IS ONE OF THREE COUPLED NUMBERS. It must equal MIN_WIDTH in
   public/js/desktop-spectrum.js and SPECTRUM_MIN_WIDTH in public/js/app.js.
   A mismatch produces a HYBRID: JS relocates the chrome at a width where these
   rules do not apply (Spectrum top strip + lead block sitting above a legacy
   grid), or the inverse. tests/spectrum-threshold-agreement.test.js parses all
   three files and fails on divergence.

   Rules come in TWO scopes, and the difference is deliberate:

   A. `.articles-grid.spectrum-view …`  — the FEED GRID and the cards in it.
      Gated by the media query + the `spectrum-view` class, which the SERVER
      now emits (server.js SSR_GRID_CLASSES). These must NOT require
      `body.ubh-spectrum`, because that class is added by JS — requiring it
      would mean the correct layout could not exist until JS ran, which is
      precisely the FOUC being fixed. `spectrum-view` is applied only to
      #articles-grid, so no other container can match.

   B. `body.ubh-spectrum …`             — the RELOCATED CHROME (top strip, tops
      block, control strip, grouphead, hero suppression, toggle hiding).
      These stay body-scoped because the nodes they style only EXIST after
      desktop-spectrum.js has moved them. Styling them earlier would be
      meaningless — there is nothing to style.

   MOBILE is unreachable either way: the media query alone already excludes it,
   and desktop-spectrum.js refuses to run below 769px.

   There is deliberately NOT ONE selector in this file outside the 769px media
   query. Mobile (<=768px) therefore cannot be reached by anything here, at any
   specificity, in any cascade order. `grep -c` for `@media` vs. top-level rules
   is part of the PR evidence.

   BREAKPOINTS (documented contract, revised 2026-08-25):
     <= 768px     mobile  — scroll / list / card(grid) / video. UNCHANGED.
     769-1023px   tablet  — Spectrum, 2-column grid, stacked lead block.
     1024-1199px  desktop — Spectrum, 3-column grid.
     1200-1439px  desktop — Spectrum, 3-column grid.
     >= 1440px    desktop — Spectrum, 4-column grid.

   ⛔ THE TABLET BAND CHANGED. It previously fell through to the retired desktop
   card/list layout. Nicolas 2026-08-25: "it makes no sense to have card/list
   show up at 768 anyways so lets use spectrum above 768". Tablet is now a
   FIRST-CLASS Spectrum surface, and 769 is exactly one pixel above the mobile
   ceiling so the bands are adjacent — no gap, no overlap, no width anywhere on
   the axis that lands on an unmaintained layout.

   Density in the tablet band is handled by the COLUMN LADDER below (2 columns
   at 769-1023, and the lead block stacks to one column), not by refusing to
   run. The earlier justification for excluding tablet — that a lead block plus
   a >=3-column grid cannot fit — was an argument against THREE COLUMNS, not
   against Spectrum: at 2 columns the card is ~360px at 769px wide, which is
   WIDER than the same card on a 1440px 4-up grid (~340px). The measurement is
   in the ladder comment below.

   NOTE ON VIEWPORT LOCK: the approved mockups use `body{height:100vh;
   overflow:hidden}` so each variant fits one screenshot. That is a MOCKUP
   device, not a product requirement — in production it would break "Load
   More", keyboard scrolling and zoom/a11y. The density (compact cards, lead +
   second, multi-column grid) is reproduced faithfully; the viewport lock is
   deliberately NOT reproduced. Everything below is normal document flow.
   ────────────────────────────────────────────────────────────────────────── */

@media (min-width: 769px) {

  /* ── top strip: page-level Listen module + the real UBH tagline ──────────
     Mirrors .topstrip in c-system.css. The audio module is the REAL
     #listen-controls node, relocated by desktop-spectrum.js — not a copy —
     so Daily Briefing / Full Headlines / category-channel handlers survive. */
  body.ubh-spectrum .ubh-topstrip {
    display: grid;
    grid-template-columns: minmax(0, 600px) 1fr;
    gap: 22px;
    align-items: center;
    padding-bottom: 8px;
    margin-bottom: 10px;
    border-bottom: 1px solid var(--border);
  }
  body.ubh-spectrum .ubh-topstrip > #listen-controls,
  body.ubh-spectrum .ubh-topstrip > .listen-controls { margin: 0; min-width: 0; }
  body.ubh-spectrum .ubh-topstrip .audio-module { margin-bottom: 0; }

  /* /saved owns its OWN page-level listen module (.saved-listen-module, built
     by loadSavedPage() in app.js) instead of the shared #listen-controls host,
     which does not exist on that route. desktop-spectrum.js relocates whichever
     one the surface has into this strip's left cell -- see LISTEN_SELECTORS.
     Its base style is `margin: 4px auto 8px; max-width: 680px`, and that `auto`
     would CENTRE it inside the 600px column rather than filling it, so both are
     neutralised here exactly as they are for #listen-controls above. */
  body.ubh-spectrum .ubh-topstrip > .saved-listen-module {
    margin: 0;
    min-width: 0;
    max-width: none;
  }

  /* No page-level listen module on this surface at all (e.g. /saved when no
     saved article has audio). Collapse to one column so the tagline keeps its
     right-aligned reading rather than sitting in a two-column grid with a dead
     left cell -- which is what the /saved defect looked like on screen. */
  body.ubh-spectrum .ubh-topstrip.ubh-topstrip--solo {
    grid-template-columns: minmax(0, 1fr);
  }
  body.ubh-spectrum .ubh-topstrip.ubh-topstrip--solo .ubh-tagline {
    border-left: 0;
    padding-left: 0;
  }

  /* Tagline — "Facts. Not Spin." is UBH production copy (site <title>,
     og:title, header logo alt, .hero-mini-text, .footer-tagline, RSS
     description, SHARE_TAGLINE). Not invented here. */
  body.ubh-spectrum .ubh-tagline {
    line-height: 1.5;
    text-align: right;
    border-left: 1px solid var(--border);
    padding-left: 20px;
    min-width: 0;
  }
  body.ubh-spectrum .ubh-tagline b {
    display: block;
    font-size: 1.06rem;
    font-weight: 800;
    letter-spacing: -0.015em;
    color: var(--text);
  }
  body.ubh-spectrum .ubh-tagline .ubh-tagline-sub {
    display: block;
    font-size: 0.67rem;
    color: var(--text-muted);
    margin-top: 4px;
    line-height: 1.45;
  }
  body.ubh-spectrum .ubh-tagline .ubh-tagline-sub code {
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    color: var(--blue);
    font-size: 0.94em;
  }

  /* ══ FEED-LEVEL CONTROL STRIP ═══════════════════════════════════════════
     Hosts page-level feed CONTROLS (currently the My News banner: the
     Tailored/Followed-Terms switch + Customize). Sits directly beneath the
     top strip at the SAME structural level -- never inside .articles-grid,
     because a grid cell would inflate row height.

     Styled to read as part of this layout rather than bolted on: it borrows
     the top strip's own surface, border, radius and rhythm so the Listen
     strip and the control strip form one continuous header stack. */
  body.ubh-spectrum .ubh-controlstrip {
    margin-bottom: 10px;
  }
  body.ubh-spectrum .ubh-controlstrip #mynews-header,
  body.ubh-spectrum .ubh-controlstrip .mynews-header {
    display: flex;
    align-items: center;
    gap: 16px;
    margin: 0;
    padding: 10px 14px;
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 9px;
  }
  /* Title + subline compress into a single tight lockup so the strip stays a
     thin control band, not a second hero. */
  body.ubh-spectrum .ubh-controlstrip .mynews-header-top {
    display: flex;
    align-items: center;
    gap: 16px;
    width: 100%;
    margin: 0;
  }
  body.ubh-spectrum .ubh-controlstrip .mynews-header-text { min-width: 0; }
  body.ubh-spectrum .ubh-controlstrip .mynews-header-title {
    font-size: 0.95rem;
    line-height: 1.2;
    margin: 0;
    letter-spacing: -0.01em;
  }
  body.ubh-spectrum .ubh-controlstrip .mynews-header-sub {
    font-size: 0.67rem;
    line-height: 1.35;
    color: var(--text-muted);
    margin: 2px 0 0;
  }
  /* The long-form subline is redundant at this density; keep the short one. */
  body.ubh-spectrum .ubh-controlstrip .mynews-header-sub--full { display: none; }
  body.ubh-spectrum .ubh-controlstrip .mynews-header-sub--short { display: block; }

  /* Tabs are the POINT of this strip (discoverability of the tailored vs
     followed-terms switch), so they keep full visual weight and sit inline
     with the title rather than being pushed to a second row. */
  body.ubh-spectrum .ubh-controlstrip .mynews-mode-tabs {
    margin: 0 0 0 auto;
    flex: 0 0 auto;
    order: 2;
  }
  body.ubh-spectrum .ubh-controlstrip .mynews-customize-btn {
    order: 3;
    flex: 0 0 auto;
    margin-left: 12px;
  }
  /* Term manager expands UNDER the strip when Followed Terms is active. */
  body.ubh-spectrum .ubh-controlstrip .mynews-term-manager-slot {
    order: 4;
    flex: 1 0 100%;
    margin-top: 10px;
  }

  /* ── group heading above the card grid ─────────────────────────────────── */
  body.ubh-spectrum .ubh-grouphead {
    display: flex;
    align-items: baseline;
    gap: 10px;
    margin-bottom: 8px;
  }
  body.ubh-spectrum .ubh-grouphead b {
    font-size: 0.78rem;
    font-weight: 800;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: var(--text);
  }
  body.ubh-spectrum .ubh-grouphead em {
    font-style: normal;
    font-size: 0.67rem;
    color: var(--text-muted);
  }
  body.ubh-spectrum .ubh-grouphead i {
    flex: 1;
    height: 1px;
    background: var(--border);
  }

  /* ══ SHARED GROUP 1 — lead + second ═════════════════════════════════════
     c-system.css `.tops` / `.big` / `.sec`. The children are the REAL
     .article-card nodes produced by createArticleCard(), relocated by
     desktop-spectrum.js. Because they are the same DOM nodes, every listener
     bound at creation (audio, full audio, save/toggleSave, share dropdown,
     sponsored impression tracking, navigate-to-article) is still attached.
     Only presentation is restyled below. */
  body.ubh-spectrum .ubh-spectrum-tops {
    display: grid;
    grid-template-columns: 1.62fr 1fr;
    gap: 20px;
    margin-bottom: 10px;
  }
  body.ubh-spectrum .ubh-spectrum-tops .article-card {
    background: none;
    border: 0;
    border-radius: 0;
    padding: 0;
    box-shadow: none;
    margin: 0;
    display: block;
  }

  /* ── the LEAD story: text left, 292px image right ─────────────────────── */
  body.ubh-spectrum .ubh-tops-lead .article-link {
    display: grid;
    grid-template-columns: minmax(0, 1fr) 292px;
    gap: 18px;
    align-items: start;
  }
  /* createArticleCard emits .article-image BEFORE .article-text-wrap; the C
     lead puts the image on the right. Reorder visually only — DOM order (and
     therefore reading/tab order, and the crawlable SSR source) is untouched. */
  body.ubh-spectrum .ubh-tops-lead .article-image { order: 2; margin: 0; }
  body.ubh-spectrum .ubh-tops-lead .article-text-wrap { order: 1; padding: 0; min-width: 0; }
  body.ubh-spectrum .ubh-tops-lead .article-image img {
    width: 292px;
    height: 122px;
    object-fit: cover;
    border-radius: 8px;
    display: block;
  }
  body.ubh-spectrum .ubh-tops-lead h2 {
    font-size: 1.3rem;
    line-height: 1.16;
    letter-spacing: -0.015em;
    margin: 6px 0 7px;
  }
  body.ubh-spectrum .ubh-tops-lead .summary {
    font-size: 0.775rem;
    line-height: 1.48;
    color: var(--text-secondary);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  body.ubh-spectrum .ubh-tops-lead .article-footer {
    margin-top: 8px;
    padding: 7px 0 0;
    border-top: 1px solid var(--border-light);
  }

  /* ── the SECOND story: image on top, divider rule on the left ─────────── */
  body.ubh-spectrum .ubh-tops-second {
    border-left: 1px solid var(--border);
    padding-left: 20px;
  }
  body.ubh-spectrum .ubh-tops-second .article-link { display: block; }
  body.ubh-spectrum .ubh-tops-second .article-image { margin: 0 0 7px; }
  body.ubh-spectrum .ubh-tops-second .article-image img {
    width: 100%;
    height: 72px;
    object-fit: cover;
    border-radius: 7px;
    display: block;
  }
  body.ubh-spectrum .ubh-tops-second .article-text-wrap { padding: 0; }
  body.ubh-spectrum .ubh-tops-second h2 {
    font-size: 0.98rem;
    line-height: 1.2;
    margin: 5px 0;
  }
  body.ubh-spectrum .ubh-tops-second .summary {
    font-size: 0.71rem;
    line-height: 1.42;
    color: var(--text-secondary);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
  }
  body.ubh-spectrum .ubh-tops-second .article-footer {
    margin-top: 8px;
    padding: 7px 0 0;
    border-top: 1px solid var(--border-light);
  }

  /* shared footer behaviour for both tops slots */
  body.ubh-spectrum .ubh-spectrum-tops .article-footer {
    display: flex;
    align-items: center;
    gap: 8px;
    flex-wrap: wrap;
  }
  body.ubh-spectrum .ubh-spectrum-tops .card-actions { margin-left: auto; }

  /* ══ SHARED CARD GRID — the component approved on the home page ═════════
     c-system.css `.grid` / `.card`. Category and topic pages get these EXACT
     rules (Nicolas v4: "why can't we use the same block style as the home
     page?" — they now literally share this one block). */
  .articles-grid.spectrum-view {
    display: grid;
    grid-template-columns: repeat(4, minmax(0, 1fr));
    gap: 12px;
    margin-bottom: 8px;
    /* ── row-height containment (Nicolas 2026-08-24) ──────────────────────
       The feed grid also receives INJECTED non-article cells: the inline
       "Create a free account" prompt (.acct-prompt-banner) and the
       account-capture surfaces. Those are much taller than a story card, and
       under CSS Grid's default `align-items: stretch` the tallest cell sets
       the whole ROW's height -- so every article card beside the signup card
       was stretched to ~400px, leaving a large dead gap under its bias line.

       `align-items: start` makes every cell size to its OWN content. This is
       fixed at the GRID level deliberately: it neutralises the entire class
       of bug for any future injected cell, not just the one card reported. */
    align-items: start;
  }
  /* ══ COLUMN LADDER ═══════════════════════════════════════════════
     Column count steps down with width so cards never become slivers.
     Top end is 4 (Nicolas: "5 per row is too many, do 4").

       >= 1440px      4 col
       1200-1439px    3 col
       1024-1199px    3 col   + lead block stacks
       769-1023px     2 col   + lead block stacks   <- NEW TIER (2026-08-25)

     WHY 2 AND NOT 3 IN THE TABLET TIER — measured, not guessed. Card width is
     (viewport - 2*container padding - (n-1)*12px gap) / n. The container pads
     16px a side in this band, so at the 769px floor:
         3 col -> (769 - 32 - 24) / 3 = 237.6px
         2 col -> (769 - 32 - 12) / 2 = 362.5px
     237.6px is below the width at which this card stays honest: the 16:9 image
     drops to 134px tall, and the headline (0.95rem/1.3) shreds to 4-5 lines for
     a typical UBH title. It also breaks the ACTION ROW — .card-actions carries
     four 30px-min buttons (summary audio, full audio, save, share) plus gaps,
     needing ~150px of the footer's usable width; at 237.6px the footer must
     also seat the timestamp and source count, so the buttons wrap to a second
     line. At 362.5px all four sit on one row with room to spare, the image is
     204px tall, and headlines settle at 2-3 lines. For reference the 4-up grid
     at 1440px gives ~340px per card, so the tablet card is WIDER than the
     widest-viewport card — this tier is comfortably inside proven territory.

     The lead-block stacking rules already existed for the 1024-1199 tier and
     apply verbatim here, so `max-width: 1199px` covers both bands. Those rules
     are the reason the tablet band does not need a Spectrum variant of its own:
     the lead goes full-width and single-column, exactly as at iPad landscape. */
  @media (max-width: 1439px) {
    .articles-grid.spectrum-view {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
  }
  @media (max-width: 1199px) {
    .articles-grid.spectrum-view {
      grid-template-columns: repeat(3, minmax(0, 1fr));
    }
    /* At iPad-landscape width and below, the lead block needs the full row to
       stay legible, so tops stacks to a single column instead of squeezing. */
    body.ubh-spectrum .ubh-spectrum-tops { grid-template-columns: minmax(0, 1fr); }
    /* ── STACKED-LEAD IMAGE HEIGHT ───────────────────────────────────
       The base rule pins .ubh-tops-second's image to a fixed 72px. That is
       correct in the SIDE-BY-SIDE layout, where the card is ~432px wide and
       72px reads as a 6:1 banner. Once the block STACKS, the same card spans
       the whole content column and that fixed height stops being a banner and
       becomes a letterbox slit: measured 1148x72 at 1180px (16:1) and 737x72
       at 769px, in which the photo is unrecognisable.

       This is an interaction defect between two individually-correct rules --
       a fixed height paired with a width that changed underneath it -- not an
       approved design choice, so the height is scaled to the stacked width
       instead. 140px keeps a banner-like ratio across the whole stacked band
       (5.3:1 at 769px, 8:1 at 1180px) without letting a single card push the
       grid below the fold.

       Pre-existing at 1024-1199 since the original Option C v5 commit; it only
       became WIDELY visible when the tablet band joined Spectrum on 2026-08-25,
       which is exactly the kind of thing a first-class surface review is for. */
    body.ubh-spectrum .ubh-tops-second .article-image img { height: 140px; }
    body.ubh-spectrum .ubh-tops-second {
      border-left: 0;
      padding-left: 0;
      border-top: 1px solid var(--border);
      padding-top: 12px;
    }
    body.ubh-spectrum .ubh-topstrip { grid-template-columns: minmax(0, 1fr); }
    body.ubh-spectrum .ubh-tagline {
      text-align: left;
      border-left: 0;
      padding-left: 0;
      border-top: 1px solid var(--border);
      padding-top: 8px;
    }
  }
  /* TABLET TIER — 769-1023px. Listed LAST of the three so it wins the cascade
     over the `max-width: 1199px` block above (equal specificity, later rule).
     Only the column count differs; every stacking rule above is inherited. */
  @media (max-width: 1023px) {
    .articles-grid.spectrum-view {
      grid-template-columns: repeat(2, minmax(0, 1fr));
    }
  }

  /* ══ SELF-SUFFICIENCY CONTRACT ═══════════════════════════════════════════
     Every declaration Spectrum needs is stated HERE, so that the computed
     style of a Spectrum card is IDENTICAL whether or not `.list-view` is also
     present on the grid.

     WHY THIS EXISTS -- this is what kills the FOUC.
     The server has a User-Agent but NEVER a viewport, so it cannot know
     whether a desktop-UA request is a 1440px laptop or a 554px phone in
     "Request Desktop Site" mode. Emitting Spectrum-only markup for desktop UAs
     would therefore hand a phone the squeezed desktop grid.

     The fix is to stop making the SERVER choose. SSR now ships BOTH classes
     (`articles-grid list-view spectrum-view` + `body.ubh-spectrum`) and lets
     the MEDIA QUERY arbitrate -- media queries are evaluated against the real
     viewport, at first paint, before any JS runs:
         <= 768   mobile -> this file never evaluates -> `list-view` drives.
         >= 769   tablet + desktop -> these rules evaluate and win -> Spectrum.
     Because the correct layout is chosen by the CSS engine during the FIRST
     style resolution, there is no "legacy paint, then JS relocates" window.
     The FOUC is eliminated BY CONSTRUCTION -- not by hiding the feed behind an
     opacity/visibility/spinner trick, which would only trade a flash for a
     blank screen.

     THE CATCH, and why the declarations below look redundant: this only works
     if `list-view` is a NO-OP once Spectrum applies. It was not. A computed-
     style diff (spectrum vs spectrum+list-view, 1440 / 1024 / 769, every card
     sub-element) found 23 distinct properties that leaked, because Spectrum
     had been silently INHERITING from `.articles-grid:not(.list-view)` --
     notably `display:contents` on .article-text-wrap and the `order:1..5`
     re-sequencing that puts the headline above the photo. Adding `list-view`
     flipped that `:not()` off and list mode's horizontal row took over.

     So each declaration below is one of two kinds:
       (a) RECLAIMED -- previously inherited from `:not(.list-view)` and now
           owned outright (display:contents, order, h2/summary padding).
       (b) NEUTRALISING -- explicitly restates the Spectrum value for a
           property list mode would otherwise set (image width/min-width,
           border-radius, flex-direction, gap, aspect-ratio, margins).
     Neither kind changes how Spectrum renders today; both make it INDEPENDENT.

     ⛔ REGRESSION GUARD: dualclass-probe.js re-measures this diff and must
     report 0. If you add a rule here that relies on `:not(.list-view)`, that
     probe fails and the FOUC comes back. Do not "fix" it by removing the
     probe. ────────────────────────────────────────────────────────────────── */
  .articles-grid.spectrum-view .article-card {
    background: var(--surface);
    border: 1px solid var(--border);
    border-radius: 9px;
    overflow: hidden;
    padding: 0;
    margin: 0;
    display: flex;
    flex-direction: column;
    /* EQUAL-HEIGHT CARDS. Found by bisection, not by reading: this was being
       inherited from `.articles-grid:not(.list-view) .article-card`, which
       carries `height:100%`. It is what makes every card in a row share the
       row's height instead of shrink-wrapping its own text, and it was the
       LAST remaining leak -- adding `list-view` flipped the `:not()` off and
       cards collapsed by ~23px (measured 229.66 -> 206.78, and only on the
       cards whose content was shorter than the row, which is why it looked
       intermittent and content-dependent). Owned explicitly now.

       This does NOT re-introduce the injected-banner stretch bug: that was
       caused by the GRID stretching cells, and is fixed at grid level by
       `align-items: start` above, which remains in force. Here the card opts
       INTO the row height for itself only. Verified by re-measuring a feed
       containing .acct-prompt-banner. */
    height: 100%;
  }
  .articles-grid.spectrum-view .article-link {
    display: block;
    flex: 1;
    min-height: 0;
    /* SELF-SUFFICIENCY (see the block comment below): neutralise list mode's
       horizontal flex row. Spectrum's .article-link is `display:block`, so
       these are inert here -- they exist purely so the declaration cannot be
       inherited from `.articles-grid.list-view .article-link`. */
    flex-direction: column;
    gap: normal;
    align-items: normal;
  }
  .articles-grid.spectrum-view .article-image {
    margin: 0;
    /* list mode pins the image to a 180px-wide left rail; Spectrum's is a
       full-width banner at the top of the card. */
    width: auto;
    min-width: 0;
    height: auto;
    flex: 0 1 auto;
    flex-shrink: 1;
    border-radius: 0;
    order: 3;
  }
  .articles-grid.spectrum-view .article-image img {
    width: 100%;
    height: 56px;
    object-fit: cover;
    display: block;
    border-radius: 0;
    aspect-ratio: auto;
    min-width: 0;
  }
  .articles-grid.spectrum-view .article-text-wrap {
    padding: 7px 9px 0;
    /* Card mode collapses this box with `display:contents` so the meta/h2/
       image/summary become direct flex children and can be re-ORDERED.
       Spectrum depends on that ordering, so it must declare it itself rather
       than inherit it from `.articles-grid:not(.list-view)`. */
    display: contents;
    flex: 0 1 auto;
    min-width: 0;
  }
  .articles-grid.spectrum-view .article-meta {
    display: flex;
    align-items: center;
    gap: 6px;
    flex-wrap: wrap;
    padding: 0;
    margin: 0 0 8px;
    order: 1;
  }
  .articles-grid.spectrum-view .article-card h2 {
    font-size: 0.775rem;
    line-height: 1.23;
    margin: 5px 0 4px;
    font-weight: 600;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    padding: 0 14px;
    order: 2;
  }
  .articles-grid.spectrum-view .article-card .summary {
    font-size: 0.66rem;
    line-height: 1.34;
    color: var(--text-muted);
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
    padding: 0 14px;
    margin: 0 0 10px;
    flex: 1 0 auto;
    flex-shrink: 0;
    order: 4;
  }
  .articles-grid.spectrum-view .article-card .matched-terms { order: 5; }
  .articles-grid.spectrum-view .category-tag {
    font-size: 0.68rem;
    line-height: 1.6;
    padding: 0;
  }
  .articles-grid.spectrum-view .article-footer {
    display: flex;
    align-items: center;
    gap: 7px;
    flex-wrap: wrap;
    margin: 7px 9px 0;
    padding: 6px 0 8px;
    border-top: 1px solid var(--border-light);
  }
  .articles-grid.spectrum-view .card-actions { margin-left: auto; }

  /* ── slim inline bias, straight off the single bias_score float ─────────
     No L/C/R source splits, no blindspot, no per-source lean list — none of
     those fields exist in the data model. This is the SAME .bias-meter DOM
     production already emits; only the scale changes. */
  .articles-grid.spectrum-view .bias-meter { gap: 3px; }
  .articles-grid.spectrum-view .bias-meter > span:not(.bias-label) { font-size: 0.6rem; }
  .articles-grid.spectrum-view .bias-bar { width: 34px; }
  .articles-grid.spectrum-view .bias-label { font-size: 0.6rem !important; }
  .articles-grid.spectrum-view .source-count { font-size: 0.62rem; }
  .articles-grid.spectrum-view .article-time { font-size: 0.62rem; }
  /* Reading time is a client-side estimate, not a stored field. The approved
     C card does not show it, so it is hidden in spectrum only (it remains in
     card/list/scroll/video exactly as today). */
  .articles-grid.spectrum-view .reading-time { display: none; }

  /* denser action buttons so a 5-up card can still carry all four */
  .articles-grid.spectrum-view .card-actions .action-btn {
    width: 24px;
    height: 24px;
    padding: 4px;
  }
  /* larger action row on the lead, per the approved `.acts-lg` treatment */
  body.ubh-spectrum .ubh-tops-lead .card-actions .action-btn {
    width: 30px;
    height: 30px;
  }

  /* ── injected signup / account-capture cells ───────────────────────────
     Paired with `align-items: start` above. The prompt legitimately needs
     more height than one story card, so it is given TWO grid rows to occupy
     instead of stretching its neighbours to match. Content is untouched --
     nothing is truncated and there is no internal scrolling; the card keeps
     full prominence, it just earns its own vertical space. */
  .articles-grid.spectrum-view .acct-prompt,
  .articles-grid.spectrum-view .acct-prompt-banner {
    grid-row: span 2;
    align-self: stretch;
    height: auto;
    overflow: visible;
  }

  /* Empty states / loading rows are FULL-WIDTH messages, not grid cells. Left
     as ordinary children they inherit a single narrow column and render as a
     cramped ~280px strip in the first slot. Span the whole row instead. */
  .articles-grid.spectrum-view > .empty-state,
  .articles-grid.spectrum-view > .loading {
    grid-column: 1 / -1;
  }

  /* Share dropdowns must escape the card's overflow:hidden clip. */
  .articles-grid.spectrum-view .article-card:has(.share-dropdown.open),
  .articles-grid.spectrum-view .article-card:has(.scroll-share-menu.open) {
    overflow: visible;
  }

  /* ── page context strip for category / topic pages ─────────────────────
     A thin band, never a hero. Carries no article layout of its own — the
     cards below it are the same shared component as the home page. */
  body.ubh-spectrum .ubh-pagehead {
    display: flex;
    align-items: flex-end;
    gap: 12px;
    margin-bottom: 8px;
  }
  body.ubh-spectrum .ubh-pagehead h1 {
    font-size: 1.32rem;
    letter-spacing: -0.015em;
    line-height: 1.1;
    margin: 0;
  }
  body.ubh-spectrum .ubh-pagehead .ubh-ph-meta {
    font-size: 0.68rem;
    color: var(--text-muted);
    padding-bottom: 3px;
  }

  /* The marketing hero is redundant once the tagline strip is present; the
     dense C layout puts stories above the fold instead. Spectrum only. */
  body.ubh-spectrum .hero,
  body.ubh-spectrum .hero-mini { display: none !important; }

  /* Top Story keeps its own slot in card/list mode. In spectrum the lead
     slot IS the top story, so the now-empty section must not leave a gap. */
  body.ubh-spectrum #top-story.ubh-spectrum-emptied { display: none !important; }

  /* ── DESKTOP CARD/LIST TOGGLE — hidden while Spectrum is the desktop mode ──
     Nicolas 2026-08-24: "I still see those buttons card/list switch in desktop
     mode in categories and my news and /all page".

     Spectrum IS the desktop layout. Its grid is driven by
     `.articles-grid.spectrum-view`, and activate() explicitly strips
     `.list-view`, so the card/list control is inert on this surface: pressing
     List would write a pref and add a class that Spectrum removes again on the
     next pass. An inert control is worse than no control.

     ⛔ HIDDEN, NOT DELETED — and hidden HERE, not anywhere else. This file is
     double-gated (`@media (min-width:769px)` + `body.ubh-spectrum`), so the
     control is untouched everywhere it is still real:
       <=768px  phones  -> already hidden by style.css; the FAB is the control
       >=769px  tablet + desktop -> hidden ONLY while body.ubh-spectrum is
                            present; if Spectrum tears down (resize,
                            unsupported surface, JS failure) the class goes
                            with it and the toggle returns on the same frame.
     ⚠️ The tablet band (769-1023px) MOVED into the hidden set on 2026-08-25,
     when Spectrum's floor dropped from 1024 to 769. That is correct and
     required, not collateral: the toggle is inert wherever Spectrum owns the
     grid, so leaving it visible on tablet would have shipped exactly the
     dead control this rule exists to prevent.
     The markup and every listener in app.js (initViewToggle,
     hydrateTopicViewToggle, refreshTopicView) stay exactly as they are.

     ⛔ DELIBERATELY NOT LISTED: `.view-toggle-fab` / `#view-toggle-fab` and
     `.btn-view-mobile`. Those are the MOBILE controls (scroll/list/card/video)
     and must keep working. They are already unreachable from this file (mobile
     never gets body.ubh-spectrum and never evaluates a min-width:769px block),
     and naming them here at all would be a trap for the next editor.

     THREE selectors because the toggle ships from THREE emitters, all of which
     must be covered or it survives on one surface:
       .view-toggle-row  public/index.html:515 (SSR home/category//all/my-news)
                         + server.js:3339 (SSR topic hub)
                         + app.js restoreHome() (SPA re-render)
       .th-controls      lib/topic-hubs.js:421 (standalone hub renderer)
       .view-toggle      the control itself — the backstop, so a FUTURE emitter
                         that forgets the wrapper is still covered. */
  body.ubh-spectrum .view-toggle,
  body.ubh-spectrum .view-toggle-row,
  body.ubh-spectrum .th-controls { display: none !important; }

  /* ══ /saved UNDER SPECTRUM ════════════════════════════════════════════════
     Saved joins the Spectrum surfaces (Nicolas 2026-08-24: "You forgot to
     redesign the 'saved' (bookmarks) page with the same layout as the topics
     and categories page layout"). It reuses the shared grid above verbatim --
     that IS the requested change -- so there are no layout rules here. The two
     rules below exist only to protect controls the shared card style would
     otherwise break, because Saved cards carry chrome no other feed card has.

     1. REORDER CONTROLS MUST STAY VISIBLE.
        `.articles-grid.spectrum-view .article-card` sets `overflow: hidden`
        (it clips the card image to the rounded corner). The Saved reorder
        cluster (buildSavedDragHandle: the ▲ / drag grip / ▼ column) is
        `position:absolute; top:8px; left:8px`, i.e. INSIDE the card box, so it
        is not clipped away -- but any future nudge of that offset would be, and
        the drag grip is the primary reorder affordance on touch. `overflow:
        visible` on Saved cards only removes that whole failure mode. The image
        keeps its own radius via .article-image, so nothing visually leaks.

     2. THE FOLDER CONTROL MUST NOT BE CLIPPED.
        buildSavedMoveControl appends a pop-open folder menu to the card. Under
        `overflow:hidden` in a dense 4-up grid that menu would be cut off at the
        card edge, which is a silent loss of function, not a cosmetic issue.

     Scoped with `body.saved-view` so these apply ONLY on Saved and cannot
     loosen clipping on the home/category/topic feeds. */
  body.ubh-spectrum.saved-view .articles-grid.spectrum-view .article-card {
    overflow: visible;
  }
  body.ubh-spectrum.saved-view .articles-grid.spectrum-view .saved-move-wrap {
    position: relative;
    z-index: 6;
  }
}
