/* ==========================================================================
   F3.3 — `ai.summary.short` in-card reveal.

   Editorial pull-quote aesthetic (per F3-PLAN §5.1 + user-approved design
   direction 2026-04-24). The summary sits *inside* the card body column
   after the tag row, so the tag metadata keeps its prominence and the
   summary reads as a follow-on paragraph-quote, not a separate module.

   Design principles locked here:
   * Serif body copy (`--font-serif`) to echo `.article__title` — reads
     as an extension of editorial voice, not a chat bubble.
   * Each sentence prefixed with ¶ in the accent colour. This is both
     the F3-D7 visual motif AND a quiet signal that three sentences are
     an AI output (numbered would read too much like console output).
   * 2px accent-coloured border-left rule for the "pull-quote" silhouette.
   * Minimal disclosure footer — per user choice: source link only,
     no "AI-generated" chrome. The pilcrow motif does the heavy lifting.

   All colours route through `--accent` / `--accent-soft` so dark-mode is
   automatic (those vars get redefined under [data-theme="dark"]).
   ========================================================================== */

/* Inline button row that sits after .article__tags. The row is flex so a
 * future "bullets summary" / "translate" button can slot in without
 * re-laying-out. Top border nibble is a very faint hairline to create a
 * gentle "tools" shelf feel without a hard divider.
 */
.article__ai-row {
  margin-top: var(--sp-3);
  display: flex;
  align-items: center;
  gap: var(--sp-3);
  flex-wrap: wrap;
  /* B5 (2026-07-07): the AI button is revealed on hover/focus, not always
     on — it was competing with the story pulls at rest. opacity (not display)
     so it can fade and stay in the tab order for focus-within to catch. */
  opacity: 0;
  transition: opacity var(--dur-fast) var(--ease);
}
.article:hover .article__ai-row,
.article:focus-within .article__ai-row,
/* Always visible when the summary panel is already open (button expanded). */
.article:has(.article__ai-btn[aria-expanded="true"]) .article__ai-row {
  opacity: 1;
}
/* Touch devices have no hover: keep the button always visible there. */
@media (pointer: coarse) {
  .article__ai-row { opacity: 1; }
}
/* Safety net for engines without :has() — reduced-motion users and older
   browsers still reach the button via keyboard focus (focus-within above);
   this keeps it visible whenever any descendant is focused. */
@media (prefers-reduced-motion: reduce) {
  .article__ai-row { transition: none; }
}

/* The summary button. Promoted from text-only to an outlined chip per
 * design-critique 2026-04-24 §2.3 / §6(5): the old "quiet link" style
 * under-sold a primary CTA. The new treatment:
 *   - Always-on hairline border tinted with --accent so the button reads
 *     as a distinct affordance at rest (not only on hover).
 *   - Accent-soft fill on hover/focus; colour continues to promote toward
 *     --ink to signal "active" without relying on underline alone
 *     (WCAG 1.4.1 still covered — border is the non-colour affordance).
 *   - Compact padding so it still sits naturally in .article__ai-row
 *     next to tags and meta. 32px min-height retained for touch. */
.article__ai-btn {
  appearance: none;
  background: var(--bg-elev-1);
  border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border));
  border-radius: var(--r-sm);
  padding: 4px 10px;
  font-family: var(--font-serif);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: -0.005em;
  color: var(--accent);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  line-height: 1.2;
  /* Large enough hit area on touch without visible padding bloating the
   * card. 32px matches the WCAG 2.5.5 AAA target-size floor. */
  min-height: 32px;
  white-space: nowrap;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease),
              color var(--dur-fast) var(--ease);
}
.article__ai-btn:hover,
.article__ai-btn:focus-visible {
  background: var(--accent-soft);
  border-color: var(--accent);
  color: color-mix(in srgb, var(--accent) 80%, var(--ink));
  outline: none;
}
.article__ai-btn:disabled {
  cursor: not-allowed;
  color: var(--ink-dim);
  background: var(--bg-elev-1);
  border-color: var(--border);
}

/* Free-tier "Plus" marker rendered *next to* the button (not on it) so
 * the button itself stays clean and the Plus gate is read as a separate
 * piece of metadata. Use the existing tag shape for visual coherence. */
.article__ai-plus {
  font-family: var(--font-sans);
  font-size: var(--text-3xs);
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  padding: 2px 6px;
  border-radius: var(--r-sm);
  color: var(--warm-text);   /* C6 — badge text on light */
  background: var(--warm-soft);
  border: 1px solid color-mix(in srgb, var(--warm) 30%, transparent);
}

/* When the summary has been loaded + revealed, the button flips to a
 * "hide summary" affordance. This stays on the same element so that
 * focus/tab order doesn't shift. The chip recedes (neutral border, flat
 * fill, muted text) so the open summary panel below is the new centre
 * of attention — the button reads as a secondary "collapse" control. */
.article__ai-btn[aria-expanded="true"] {
  color: var(--ink-muted);
  background: var(--bg-elev-2);
  border-color: var(--border);
}

/* The revealed summary panel. Grid-level layout uses the full body
 * column width. `article[hidden]` handles collapsed state so we can
 * toggle purely via the `hidden` attribute (no display:none class
 * needed). */
/* Calm accent (P2, 2026-04-24): we keep the accent `border-left` rail as
 * the sole chromatic signal and strip the 4% tint so the summary reads
 * as an editorial extension of the article card rather than a distinct
 * tinted UI module. The card shell inherits the transparent article
 * background and relies on the rail + serif body for hierarchy. */
.article__ai-summary {
  margin-top: var(--sp-3);
  padding: var(--sp-3) var(--sp-4);
  border-left: 2px solid var(--accent);
  background: transparent;
  border-radius: 0 var(--r-md) var(--r-md) 0;
  font-family: var(--font-serif);
  font-size: var(--text-base);
  line-height: 1.55;
  color: var(--ink);
}
.article__ai-summary[hidden] { display: none; }

/* Each sentence. Prefixed with a pilcrow in accent colour. The margin
 * between sentences gives the pull-quote a breathing feel without
 * pretending to be a numbered list. */
.article__ai-summary__sentences {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.article__ai-summary__sentences li {
  display: grid;
  grid-template-columns: 16px 1fr;
  gap: var(--sp-2);
  align-items: baseline;
}
.article__ai-summary__sentences li::before {
  content: "¶";
  color: var(--accent);
  font-weight: 600;
  /* Slight vertical lift so the glyph optically aligns with the
   * ascender of the first line instead of the baseline. */
  line-height: 1;
  transform: translateY(1px);
}

/* Disclosure footer — per user's "minimal disclosure" choice. Just a
 * small link to the original + relative timestamp. Keeps the AI
 * fingerprint so quiet that the summary reads as if it were produced
 * by a human editor, which is exactly what we want during the beta. */
.article__ai-summary__foot {
  margin-top: var(--sp-3);
  padding-top: var(--sp-2);
  border-top: 1px solid color-mix(in srgb, var(--accent) 12%, transparent);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  color: var(--ink-dim);
  display: flex;
  justify-content: space-between;
  gap: var(--sp-3);
}
.article__ai-summary__source {
  color: var(--ink-muted);
  text-decoration: none;
  font-weight: 500;
}
.article__ai-summary__source:hover,
.article__ai-summary__source:focus-visible {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  outline: none;
}

/* F3.4 translate toggle: left-hand slot in the disclosure footer.
 * The tools container holds the button (and, in future, any sibling
 * affordances like a "share summary" link). When no tools are active
 * it's empty-but-present so flexbox still pushes the source link to
 * the right. */
.article__ai-summary__tools {
  display: inline-flex;
  align-items: center;
  gap: var(--sp-2);
}

/* Visually matches .article__ai-summary__source on the right — a quiet
 * text-button that reveals its interactive affordance on hover. We use
 * a <button> (not an <a>) because the click has no URL target and needs
 * keyboard activation via Enter + Space, which buttons give us for
 * free. Same font/size as the source link so the footer reads as one
 * horizontal line of metadata rather than a competing UI strip. */
.article__ai-summary__toggle {
  appearance: none;
  background: transparent;
  border: 0;
  padding: 0;
  margin: 0;
  color: var(--ink-muted);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 500;
  line-height: inherit;
  cursor: pointer;
  text-decoration: none;
}
.article__ai-summary__toggle:hover,
.article__ai-summary__toggle:focus-visible {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-thickness: 1px;
  text-underline-offset: 3px;
  outline: none;
}
.article__ai-summary__toggle:disabled {
  opacity: 0.55;
  cursor: progress;
  text-decoration: none;
}

/* Loading state: three shimmering skeleton lines inside the same
 * panel shell so the transition in/out of "loaded" is structural
 * rather than a layout shift. Reduced-motion users get a static
 * swatch via the existing prefers-reduced-motion block. */
.article__ai-summary--loading .article__ai-summary__sentences {
  gap: 10px;
}
.article__ai-summary__skel {
  height: 14px;
  width: 100%;
  border-radius: var(--r-sm);
  /* perf (audit #26): same composited-shimmer conversion as .skeleton. */
  position: relative;
  overflow: hidden;
  background: color-mix(in srgb, var(--accent) 3%, var(--bg-elev-1));
}
.article__ai-summary__skel::after {
  content: "";
  position: absolute;
  inset: 0;
  background: linear-gradient(
    90deg,
    color-mix(in srgb, var(--accent) 3%, var(--bg-elev-1)) 0%,
    color-mix(in srgb, var(--accent) 10%, var(--bg-elev-1)) 50%,
    color-mix(in srgb, var(--accent) 3%, var(--bg-elev-1)) 100%
  );
  animation: skeleton-shimmer 1.4s linear infinite;
}
.article__ai-summary__skel:nth-child(2) { width: 92%; }
.article__ai-summary__skel:nth-child(3) { width: 78%; }

/* Error + quota-exhausted panel. Same shell but orange (--warm) accent
 * so users instantly register "something went sideways" vs "nothing here".
 * Per F3-PLAN §5.3 we differentiate the copy; the colour just flags the
 * whole panel as "not a normal summary". */
.article__ai-summary--error {
  /* Calm accent parity: the error state inherits the transparent shell
   * from the base rule and simply swaps the rail to --warm. The footer
   * hairline (below) keeps a warm tint as a subtle echo of the rail. */
  border-left-color: var(--warm);
}
.article__ai-summary--error .article__ai-summary__foot {
  border-top-color: color-mix(in srgb, var(--warm) 14%, transparent);
}
.article__ai-summary__msg {
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  color: var(--ink);
  line-height: 1.5;
  margin: 0;
}
.article__ai-summary__msg--muted {
  color: var(--ink-muted);
  margin-top: 4px;
}

/* Tight retry button inside the error panel. Uses the existing filled
 * button language (matches .modal__submit visual weight but sized
 * down). */
.article__ai-summary__retry {
  appearance: none;
  align-self: flex-start;
  margin-top: var(--sp-2);
  padding: 4px 12px;
  border: 1px solid var(--border-strong);
  background: var(--bg-elev-1);
  color: var(--ink);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 600;
  border-radius: var(--r-sm);
  cursor: pointer;
  /* (Glass Terminal, 2026-07-12) Settle the hover fill via the motion system
     instead of snapping (focus ring comes from the global :focus-visible
     baseline). transform/shadow are untouched — background only, so it stays a
     cheap composite and honours the reduced-motion gate. */
  transition: background var(--dur-fast) var(--ease-out);
}
.article__ai-summary__retry:hover {
  background: var(--bg-elev-2);
}

/* B3: intermediate topbar breakpoint. Between the desktop layout and the
 * 720px mobile reflow, the topbar-float control row gets crowded. Trim it:
 * hide the theme button's text label (the .icon-btn__glyph SVG remains, so
 * the button stays icon-only and usable — app.js keeps writing #theme-label
 * textContent invisibly, which is harmless) and hide the .stats readout. */
@media (min-width: 721px) and (max-width: 1100px) {
  #theme-label { display: none; }
  .topbar-float .stats { display: none; }
}

/* Mobile: the card already collapses to a single column via existing
 * rules. The summary panel follows along because it inherits the body
 * column's width. We only tighten the internal padding so narrow screens
 * don't feel squeezed. */
@media (max-width: 720px) {
  .article__ai-summary {
    padding: var(--sp-2) var(--sp-3);
    font-size: var(--text-sm);
  }
  .article__ai-summary__sentences li {
    grid-template-columns: 14px 1fr;
  }

  /* Phase A (2026-06-15): mobile topbar. The inline control row (brand +
   * search + lang + theme + stats + refresh + user) was wider than a phone
   * viewport and clipped off the right edge. Reflow to: brand + corner menu
   * button on row 1, full-width search on row 2, and the lang/theme/refresh/
   * account controls collapsed into a vertical menu revealed by the corner
   * button — reusing the existing [data-expanded] toggle, no JS change. */
  .topbar-float__head {
    flex-wrap: wrap;
    row-gap: var(--sp-2);
    padding-right: calc(var(--sp-4) + 40px);
  }
  .topbar-float__brand { flex: 1 1 auto; }
  .topbar-float__search { order: 3; flex: 1 0 100%; }
  .topbar-float__actions {
    order: 4;
    flex: 1 0 100%;
    display: none;
    flex-direction: column;
    align-items: stretch;
    gap: var(--sp-2);
    margin-top: var(--sp-2);
    padding-top: var(--sp-3);
    border-top: 1px solid var(--border);
  }
  .topbar-float[data-expanded="true"] .topbar-float__actions { display: flex; }
  .topbar-float__actions > * { width: 100%; }
  .topbar-float__actions .btn,
  .topbar-float__actions .icon-btn { min-height: 44px; justify-content: flex-start; }
  .topbar-float .stats { display: none; }
  /* Empty Step-1 body is unused on mobile — keep it hidden in both states. */
  .topbar-float__body { display: none; }
  /* Corner chevron becomes the menu trigger: bigger, top-right. Re-enabled
   * here (hidden on desktop, where it toggles only an empty placeholder). */
  .topbar-float__expand { display: inline-grid; width: 40px; height: 40px; top: 8px; right: 8px; bottom: auto; }
  .topbar-float__expand-glyph { font-size: 18px; }

  /* Slim globe topbar (page-split 2026-07-06): it has NO expand chevron and no
   * search, so the mobile chevron-collapse machinery above would hide its
   * controls with no way to reveal them. Keep its (few) actions inline and
   * wrapping instead, and reclaim the head padding the (absent) chevron
   * reserved. Overrides scoped to .topbar-float--slim only, so the feed page's
   * full topbar keeps its collapse behaviour. */
  .topbar-float--slim .topbar-float__head { padding-right: var(--sp-4); }
  .topbar-float--slim .topbar-float__actions {
    order: 4;
    flex: 1 0 100%;
    display: flex;
    flex-direction: row;
    flex-wrap: wrap;
    align-items: center;
    justify-content: flex-end;
    gap: var(--sp-2);
    margin-top: var(--sp-2);
    padding-top: 0;
    border-top: 0;
  }
  .topbar-float--slim .topbar-float__actions > * { width: auto; }
  .topbar-float--slim .topbar-float__actions .btn,
  .topbar-float--slim .topbar-float__actions .icon-btn { min-height: 40px; justify-content: center; }
  .topbar-float--slim .page-tabs { order: 2; }

  /* Feed page: the topbar reflows to ~2 rows on phones (search wraps below the
   * brand + page tabs), so reserve more clearance than the desktop 1-row value
   * to keep the geo-panel from sliding under the fixed bar. */
  body[data-page="feed"] .page { padding-top: calc(var(--sp-3, 12px) + 112px); }

  /* Phase B (2026-06-15): country panel → full-width bottom sheet on phones.
   * The desktop side panel (33% / min 320px, right-anchored) left an awkward
   * globe sliver on the left. On mobile the panel is a viewport-fixed sheet
   * that slides up from the bottom; the globe stays full-width behind it.
   * position:fixed (not the desktop absolute-in-hero) keeps the sheet pinned
   * to the screen bottom even after Phase C shrinks the hero. The WebGL
   * side-panel morph (globe-panel-mesh.js) is desktop-specific geometry, so
   * it's neutralised here — the sheet just slides in. (A JS guard to skip the
   * mesh work entirely on mobile is a perf follow-up.) */
  .globe-panel {
    position: fixed;
    top: auto;
    left: 0;
    right: 0;
    bottom: 0;
    width: auto;
    min-width: 0;
    max-height: 82vh;
    z-index: var(--z-sheet);
    border-radius: var(--r-lg) var(--r-lg) 0 0;
    box-shadow: 0 -8px 28px var(--scrim-18);
    transform: translateY(100%);
    transition: opacity var(--dur-base) var(--ease), transform var(--dur-slow) var(--ease-spring);
    /* A8: the sheet is pinned to the screen bottom, so its lower content would
     * sit under the phone home indicator. .globe-panel has no base padding
     * (inner sections handle their own), so add the inset directly — nothing
     * to compose. Mirrors the watchlist overlay's safe-area treatment (~2888). */
    padding-bottom: env(safe-area-inset-bottom, 0px);
  }
  .globe-hero[data-panel-state="open"] .globe-panel { transform: translateY(0); }
  /* B1b scaffold: the left profile panel is desktop-only. On mobile the profile
   * lives INSIDE the country sheet under the Profile tab (audit #41), so the
   * standalone aside stays hidden; globe-panel.js relocates its body into the
   * sheet's .globe-panel__mprofile host. */
  .globe-panel--profile { display: none; }
  /* Mobile Headlines | Profile tab bar — only exists on phones. Full-width so
   * the two tabs split the sheet header; reuses .view-tabs visuals. */
  .globe-panel__mtabs {
    display: flex;
    width: 100%;
    margin-top: var(--sp-2);
    margin-bottom: 0;
  }
  .globe-panel__mtabs .view-tabs__tab { flex: 1 1 0; text-align: center; }
  /* Profile host inside the sheet — its own scroll region so long profiles
   * scroll independently while the sheet stays pinned. Padding mirrors the
   * desktop .globe-panel__profile-body. */
  .globe-panel__mprofile {
    display: block;
    flex: 1 1 auto;
    min-height: 0;
    padding: var(--sp-3) var(--sp-4) var(--sp-4);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
  .globe-panel__mprofile[hidden] { display: none; }
  .globe-panel__head { padding-top: var(--sp-4); position: relative; }
  .globe-panel__head::before {
    content: "";
    position: absolute;
    top: 6px;
    left: 50%;
    transform: translateX(-50%);
    width: 36px;
    height: 4px;
    border-radius: 2px;
    background: var(--border);
  }

  /* Page-split (2026-07-06): the mobile hero shrink (was 46vh) + its paired
   * Layers-popover re-cap were DELETED. They existed only to push the feed
   * content up within the first screenful on the single-page layout. The feed
   * now lives on feed.html (which has no hero), so the globe page's hero is a
   * full 100dvh at ALL widths (base rule) and the base popover cap
   * `min(60vh, calc(100vh - 140px))` is correct again. */
}

/* ==========================================================================
   Plus upsell modal — reuses .modal / .modal__panel scaffold so all the
   positioning, z-index, backdrop and Escape behaviour comes free.

   Visual twist: centred newspaper-style composition with serif title, an
   italic dek, and a bulleted-but-unbulleted benefit list (again pilcrow
   markers, matching the rest of F3.3).
   ========================================================================== */
#ai-upsell-modal .modal__panel {
  max-width: 440px;
  /* Gives the editorial composition some vertical room; 40px x-padding
   * for a more magazine-like left/right margin. */
  padding: var(--sp-6) var(--sp-6) var(--sp-5);
  gap: var(--sp-4);
  text-align: center;
}
/* (Glass Terminal, 2026-07-12) Safe-area floor for the upsell modal. On a phone
   with viewport-fit=cover (added centrally) a near-full-height modal's CTA can
   land under the home indicator, so grow the bottom padding by the inset.
   @supports-gated per spec; env() = 0 on non-notch devices, so the desktop
   composition is unchanged. Only padding-bottom is overridden — the base
   shorthand's other sides stand. */
@supports (padding: env(safe-area-inset-bottom)) {
  #ai-upsell-modal .modal__panel {
    padding-bottom: calc(var(--sp-5) + env(safe-area-inset-bottom));
  }
}

.ai-upsell__title {
  font-family: var(--font-serif);
  font-size: var(--text-lg);
  font-weight: 600;
  letter-spacing: -0.015em;
  color: var(--ink);
  margin: 0;
  line-height: 1.2;
}

.ai-upsell__lede {
  font-family: var(--font-serif);
  font-style: italic;
  font-size: var(--text-base);
  color: var(--ink-muted);
  line-height: 1.5;
  margin: 0;
}

.ai-upsell__benefits {
  list-style: none;
  margin: var(--sp-2) 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--sp-3);
  text-align: left;
}
.ai-upsell__benefits li {
  display: grid;
  grid-template-columns: 16px 1fr;
  gap: var(--sp-2);
  align-items: start;
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  color: var(--ink);
  line-height: 1.45;
}
/* (Glass Terminal, 2026-07-12) Conversion-grade benefit markers: the F3.3
   pilcrow gives way to a check on a tinted accent tile — a "here's what you
   get" affordance that reads as premium value, not editorial voice. The tile
   is a 16px control square on the 4px grid (fills the first grid column); the
   1px optical lift aligns it to the cap-height of the first text line. Accent
   (the app's brand marker colour) keeps continuity with the pilcrow it
   replaces; the summary sentences elsewhere keep ¶. */
.ai-upsell__benefits li::before {
  content: "\2713";
  display: grid;
  place-items: center;
  width: 16px;
  height: 16px;
  margin-top: 1px;
  border-radius: var(--r-sm);
  background: var(--accent-soft);
  color: var(--accent);
  font-family: var(--font-sans);
  font-size: var(--text-2xs);
  font-weight: 700;
  line-height: 1;
}

.ai-upsell__form {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  text-align: left;
  margin-top: var(--sp-2);
}
.ai-upsell__label {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 600;
  color: var(--ink-muted);
  letter-spacing: 0.08em;
  text-transform: uppercase;
}
/* (Glass Terminal, 2026-07-12) Inset well — recessed onto the deepest surface
   (--bg) with a 1px inset top shadow, matching the topbar/masthead search
   fields. The recess reads correctly on the tier-4 glass modal panel: an
   opaque well sunk into frosted glass. Keyboard/focus surfaces an accent
   outline (consistent with topbar) + border promote. */
.ai-upsell__input {
  font-family: var(--font-sans);
  font-size: var(--text-base);
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid var(--border);
  border-radius: var(--r-md);
  background: var(--bg);
  color: var(--ink);
  width: 100%;
  box-shadow: inset 0 1px 2px var(--scrim-08);
  transition: border-color var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out);
}
.ai-upsell__input:focus {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
  border-color: var(--accent);
}
.ai-upsell__actions {
  display: flex;
  flex-direction: column;
  gap: var(--sp-2);
  margin-top: var(--sp-2);
}
/* (Glass Terminal, 2026-07-12) Primary CTA — mirrors the canonical .btn lift
   language (hover: shadow-2 + 1px up; active: 0.5px compress; transform/shadow
   are compositor-only) with the accent-fill / white-ink primary recipe. When a
   shared .btn--primary lands in 10-base, these declarations already match it
   token-for-token, so the HTML can adopt `btn btn--primary` with no visual
   drift. --ease-out for the settle. */
.ai-upsell__submit {
  appearance: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--sp-2);
  padding: var(--sp-3) var(--sp-5);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 600;
  color: #fff;
  background: var(--accent);
  border: 1px solid var(--accent);
  border-radius: var(--r-md);
  cursor: pointer;
  letter-spacing: 0.01em;
  transition: border-color var(--dur-fast) var(--ease-out),
              background var(--dur-fast) var(--ease-out),
              box-shadow var(--dur-fast) var(--ease-out),
              transform var(--dur-fast) var(--ease-out);
}
/* (Glass Terminal, W1) Dark AA grounding — the coral dark --accent (#e63946) is
   only ~4.17:1 with the #fff ink, so ground the FILL toward --bg to clear AA
   (~5.4:1) and hold >=4.5:1 even through the +5% hover brighten. Mirrors
   [data-theme="dark"] .btn--primary token-for-token, so the HTML can later adopt
   `btn btn--primary` with no visual drift. */
[data-theme="dark"] .ai-upsell__submit {
  background: color-mix(in srgb, var(--accent) 85%, var(--bg));
  border-color: color-mix(in srgb, var(--accent) 85%, var(--bg));
}
.ai-upsell__submit:hover:not(:disabled) {
  /* (Glass Terminal, W1) .btn--primary hover language: brighten the (dark-
     grounded) accent fill IN PLACE. A re-mix toward black here would replace the
     grounded background and defeat the dark AA fix above. Lift preserved. */
  filter: brightness(1.05);
  box-shadow: var(--shadow-2);
  transform: translateY(-1px);
}
.ai-upsell__submit:active:not(:disabled) {
  transform: translateY(0.5px);
  box-shadow: none;
}
.ai-upsell__submit:disabled {
  opacity: 0.6;
  cursor: not-allowed;
  transform: none;
  box-shadow: none;
}
/* Ghost secondary — recedes to a bare label at rest, lifts a whisper of
   neutral tint on hover so it stays clearly subordinate to the accent CTA.
   The 1px transparent border reserves the box so hover doesn't shift layout. */
.ai-upsell__cancel {
  appearance: none;
  padding: var(--sp-2) var(--sp-3);
  border: 1px solid transparent;
  background: transparent;
  color: var(--ink-muted);
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  font-weight: 500;
  border-radius: var(--r-md);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease-out),
              color var(--dur-fast) var(--ease-out);
}
.ai-upsell__cancel:hover {
  color: var(--ink);
  background: color-mix(in srgb, var(--ink) 8%, transparent);
  text-decoration: none;
}
/* (Glass Terminal, 2026-07-12) Coarse-pointer tap heights for the upsell modal
   CTAs. Neither button carries the .btn class, so the central 70-aux
   @media(pointer:coarse) rule doesn't reach them; in a centred conversion modal
   (no list density at stake) give both the 44px house floor. The submit is
   already inline-flex-centred; the cancel is a native button that centres its
   own label, so min-height is all that's needed. Coarse-only. */
@media (pointer: coarse) {
  .ai-upsell__submit,
  .ai-upsell__cancel { min-height: 44px; }
}

.ai-upsell__msg {
  font-family: var(--font-sans);
  font-size: var(--text-xs);
  line-height: 1.5;
  margin: 0;
  padding: var(--sp-2) var(--sp-3);
  border-radius: var(--r-sm);
  text-align: left;
}
.ai-upsell__msg--error {
  color: var(--accent);
  background: var(--accent-soft);
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
}
.ai-upsell__msg--ok {
  color: color-mix(in srgb, var(--warm) 60%, var(--ink));
  background: var(--warm-soft);
  border: 1px solid color-mix(in srgb, var(--warm) 30%, transparent);
}
.ai-upsell__msg.is-hidden { display: none; }

/* ---- Empty-state "Clear all filters" affordance -------------------------- */
.empty__clear {
  margin-top: var(--sp-4);
}
.btn--link {
  background: transparent;
  border: 1px solid var(--border);
  color: var(--accent);
  padding: 6px 14px;
  border-radius: var(--r-md);
  font-size: var(--text-sm);
  cursor: pointer;
  transition: background var(--dur-fast) var(--ease), border-color var(--dur-fast) var(--ease);
}
.btn--link:hover {
  background: var(--accent-soft);
  border-color: color-mix(in srgb, var(--accent) 40%, var(--border));
}

/* ---- Refresh button spinner ---------------------------------------------- */
/* The label grew from "Refresh" to "Reload headlines" (and localised
 * multi-word equivalents) per design-critique U8, so we lock the button
 * on one line to stop a narrow masthead from breaking "Reload /
 * headlines" across two rows. */
#refresh-btn {
  white-space: nowrap;
}
#refresh-btn.is-loading {
  position: relative;
  padding-right: 32px;
}
#refresh-btn.is-loading::after {
  content: "";
  position: absolute; top: 50%; right: 10px;
  width: 12px; height: 12px; margin-top: -6px;
  border: 2px solid currentColor;
  border-right-color: transparent;
  border-radius: 50%;
  animation: refresh-spin 0.7s linear infinite;
  opacity: 0.8;
}
@keyframes refresh-spin {
  to { transform: rotate(360deg); }
}

/* Post-success dwell state: briefly (2s) replaces the label with a
 * delta readout ("+N new" or "Up to date") so the user gets an explicit
 * "something happened" confirmation before the button reverts to idle.
 * Implements design-critique 2026-04-24 §2.2 U8 / §6(5). */
#refresh-btn.is-done {
  background: color-mix(in srgb, var(--accent) 8%, var(--bg-elev-1));
  border-color: color-mix(in srgb, var(--accent) 40%, var(--border-strong));
  color: var(--accent);
}

/* ---- Accessibility helpers ----------------------------------------------- */
.visually-hidden {
  position: absolute !important;
  width: 1px; height: 1px;
  padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0, 0, 0, 0);
  white-space: nowrap; border: 0;
}

/* WCAG 2.4.1 Bypass Blocks — sits offscreen until it receives keyboard
 * focus, then slides into the top-left corner. Clicking/activating it
 * jumps focus and scroll to the results section (#main-content).
 * Contrast: #fff on --accent ≈ 5:1 (passes AA for normal text).
 * Focus outline matches the global focus-visible style. */
.skip-link {
  position: absolute;
  top: 0;
  left: 0;
  transform: translateY(-150%);
  z-index: var(--z-skip-link);
  background: var(--accent);
  color: #fff;
  padding: 10px 16px;
  border-radius: 0 0 var(--r-md) var(--r-md);
  font-family: var(--font-sans);
  font-size: var(--text-sm);
  font-weight: 600;
  letter-spacing: 0.02em;
  text-decoration: none;
  box-shadow: var(--shadow-2);
  transition: transform var(--dur-fast) var(--ease);
}
.skip-link:focus {
  transform: translateY(0);
  outline: 3px solid var(--ink);
  outline-offset: 2px;
}
/* Reduced-motion users get the instant jump; transition is zeroed out
 * elsewhere by the global prefers-reduced-motion block. */

