    /* ── Status/announcement banner ────────────────────────── */
    #statusBanner {
      position: fixed;
      top: 0; left: 0; right: 0;
      z-index: 1000;
      align-items: center;
      justify-content: center;
      gap: 12px;
      /* M2: env() for the notch — top inset is 0 in browser tabs, 47px+
         in iOS PWA mode. Without this the banner sits behind the notch
         on installed iPhones and only the bottom half is readable. */
      padding: calc(10px + env(safe-area-inset-top)) 16px 10px;
      background: rgba(234,179,8,0.15);
      border-bottom: 1px solid rgba(234,179,8,0.35);
      backdrop-filter: blur(8px);
      font-family: var(--font-sans);
      font-size: var(--t-secondary);
      color: #fbbf24;
      text-align: center;
    }
    #statusBanner button {
      background: none; border: none; color: rgba(251,191,36,0.5);
      font-size: 16px; cursor: pointer; padding: 0 2px; line-height: 1;
      /* A6 — 44×44 hit area is now provided by .hit-44::before
         (see utility rule near top of file). The visible glyph stays
         at its natural size; the layout box no longer balloons to
         44px wide, which kept the × awkwardly far from the banner
         text on narrow viewports. v130 V23. */
    }
    #statusBanner button:hover { color: #fbbf24; }

    /* ── V23 hit-area expander ─────────────────────────────────
       Transparent ::before extends an invisible 44×44 touch target
       outward from a button without inflating its visible layout
       box. Replaces the old `min-width/min-height: 44px + inline-flex`
       pattern, which forced × glyphs to render in a 44px-wide cell
       even when the surrounding banner was tight on space.
       Applied via class="hit-44" on banner-dismiss buttons and the
       in-card timer Pause/Resume pill. */
    .hit-44 { position: relative; }
    .hit-44::before {
      content: '';
      position: absolute;
      /* Negative inset expands the click target outward. -14px on a
         ~16px glyph yields ~44px total; on the timer pill (~32px tall)
         it yields ~60px which is fine — generous, never less than 44. */
      inset: -14px;
    }

    /* ── Toast notification ────────────────────────────────── */
    #appToast {
      position: fixed;
      bottom: 120px; left: 50%; transform: translateX(-50%) translateY(20px);
      z-index: 1001;
      max-width: 340px;
      padding: 14px 20px;
      background: var(--surface, #231f1a);
      border: 1px solid color-mix(in srgb, var(--pink) 30%, transparent);
      border-radius: 12px;
      color: var(--text, #e8e8f0);
      font-family: var(--font-sans);
      font-size: var(--t-meta);
      line-height: 1.5;
      text-align: center;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.3s, transform 0.3s;
    }
    #appToast.visible {
      opacity: 1;
      transform: translateX(-50%) translateY(0);
      pointer-events: auto;
      cursor: pointer;
    }

    /* Callout tooltips (first-run, first-question) were removed — both
       patterns collided with the pink CTA at the bottom of the screen.
       Onboarding now relies on the card's own inline affordances. */

    /* ── Question slide animations ───────────────────────────── */
    @keyframes slideOutLeft {
      from { transform: translateX(0);     opacity: 1; }
      to   { transform: translateX(-108%); opacity: 0; }
    }
    @keyframes slideOutRight {
      from { transform: translateX(0);    opacity: 1; }
      to   { transform: translateX(108%); opacity: 0; }
    }
    @keyframes slideInFromRight {
      from { transform: translateX(108%); opacity: 0; }
      to   { transform: translateX(0);    opacity: 1; }
    }
    @keyframes slideInFromLeft {
      from { transform: translateX(-108%); opacity: 0; }
      to   { transform: translateX(0);     opacity: 1; }
    }
    .slide-out-left  { animation: slideOutLeft     0.22s cubic-bezier(0.4,0,1,1)       forwards; }
    .slide-out-right { animation: slideOutRight    0.22s cubic-bezier(0.4,0,1,1)       forwards; }
    .slide-in-right  { animation: slideInFromRight 0.3s  cubic-bezier(0.25,0.1,0.25,1) forwards; }
    .slide-in-left   { animation: slideInFromLeft  0.3s  cubic-bezier(0.25,0.1,0.25,1) forwards; }



    #updateBanner {
      position: fixed;
      top: 0; left: 0; right: 0;
      z-index: 999;
      display: none; /* shown by JS when SW signals update */
      align-items: center;
      justify-content: center;
      gap: 12px;
      /* M2 — see #statusBanner. */
      padding: calc(10px + env(safe-area-inset-top)) 16px 10px;
      /* v130: background neutralized to white@6% so a successful update
         reads as informational chrome, not the same kind of attention
         signal as #loadErrorBanner. Border + text stay pink to keep the
         "fresh release" association. */
      background: color-mix(in srgb, var(--chip-tint) 6%, transparent);
      border-bottom: 1px solid color-mix(in srgb, var(--pink) 35%, transparent);
      backdrop-filter: blur(8px);
      font-family: var(--font-sans);
      font-size: var(--t-body);
      color: var(--pink);
    }
    #updateBanner span { flex: 0 0 auto; }
    /* Banner primary action — outlined-pink family. Banner ✕ is the
       paired option, so per the button-family decision tree this is
       "primary path forward when there are 2+ options." Background is
       fully transparent so the family read isn't blurred by a soft
       fill; hover tints lightly without graduating to the full-fill
       hover used by the dialog primaries (#mainBtn / #consentAcceptBtn) —
       the small banner pill would shout. */
    #updateBannerBtn {
      background: transparent;
      border: 1px solid var(--pink);
      border-radius: 999px;
      color: var(--pink);
      font-family: var(--font-sans);
      font-size: var(--t-body);
      padding: 4px 14px;
      cursor: pointer;
      transition: background 0.2s;
    }
    #updateBannerBtn:hover { background: color-mix(in srgb, var(--pink) 18%, transparent); }
    /* Banner dismiss — ghost-gray (muted) per the button-family
       decision tree. Was pink-at-0.5-alpha so the dismiss matched the
       banner palette; that read as a second pink action and competed
       with the Update button. Muted gray puts the dismiss in the
       cancel/no/dismiss family with .btn-secondary-muted. */
    #updateBannerDismiss {
      background: none;
      border: none;
      color: var(--muted);
      font-size: var(--t-body);
      cursor: pointer;
      padding: 0 2px;
      line-height: 1;
      transition: color 0.2s;
      /* A6 — 44×44 hit area now provided by .hit-44::before. v130 V23. */
    }
    #updateBannerDismiss:hover { color: var(--text); }

    /* ── Question-bank load-failure banner (U3) ────────────────
       Sits at the top of the viewport when loadQuestions had to fall
       through to the hard-coded sample set. Uses the shared --red-*
       token palette (the same one #clearDataBtn pulls from) so the
       page only has one "this is a problem" color, not two. v130
       moved off the previous amber palette per visual critique V7.
       z-index between updateBanner (999) and statusBanner (1000) so
       the fresher announcement wins if both appear. */
    #loadErrorBanner {
      position: fixed;
      top: 0; left: 0; right: 0;
      z-index: 999;
      align-items: center;
      justify-content: center;
      gap: 12px;
      /* M2 — see #statusBanner. */
      padding: calc(10px + env(safe-area-inset-top)) 16px 10px;
      background: var(--red-bg);
      border-bottom: 1px solid var(--red-border);
      backdrop-filter: blur(8px);
      font-family: var(--font-sans);
      font-size: var(--t-secondary);
      color: var(--red-light);
      text-align: center;
    }
    #loadErrorBanner[style*="flex"],
    #loadErrorBanner[style*="block"] { display: flex !important; }
    .load-error-banner-text { flex: 1 1 auto; }
    #loadErrorRetryBtn {
      background: var(--red-bg);
      border: 1px solid var(--red-border);
      border-radius: 999px;
      color: var(--red-light);
      font-family: var(--font-sans);
      font-size: var(--t-secondary);
      padding: 4px 14px;
      cursor: pointer;
      transition: background 0.2s;
      flex: 0 0 auto;
    }
    #loadErrorRetryBtn:hover { background: rgba(239,68,68,0.28); }
    #loadErrorDismissBtn {
      background: none;
      border: none;
      color: rgba(252,165,165,0.6);
      font-size: 16px;
      cursor: pointer;
      padding: 0 2px;
      line-height: 1;
      flex: 0 0 auto;
      transition: color 0.2s;
      /* A6 — 44×44 hit area now provided by .hit-44::before. v130 V23. */
    }
    #loadErrorDismissBtn:hover { color: var(--red-light); }

    /* ── Swipe hint ──────────────────────────────────────────── */
    #swipeHintBar {
      display: flex;
      position: fixed;
      left: 0;
      right: 0;
      z-index: 11;
      align-items: center;
      justify-content: center;
      gap: 8px;
      font-family: var(--font-sans);
      font-size: var(--t-body);
      font-weight: 300;
      letter-spacing: var(--tr-caps-lg);
      text-transform: uppercase;
      color: var(--muted);
      opacity: 0;
      transition: opacity 0.6s ease;
      pointer-events: none;
      user-select: none;
      -webkit-user-select: none;
      /* bottom set by positionTalkLink() */
    }
    #swipeHintBar.visible { opacity: 1; }
    #swipeHintBar.fading  { opacity: 0; }
    .swipe-arrow {
      display: inline-flex;
      align-items: center;
      animation: swipeNudge 2s ease-in-out infinite;
    }
    .swipe-arrow svg {
      width: 18px; height: 18px;
      stroke: var(--pink);
      fill: none;
      stroke-width: 2;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    @keyframes swipeNudge {
      0%   { transform: translateX(0); }
      30%  { transform: translateX(-7px); }
      55%  { transform: translateX(0); }
      100% { transform: translateX(0); }
    }
    /* Welcome swipe animation */
    .swipe-track {
      position: relative;
      width: 80px;
      height: 32px;
      display: flex;
      align-items: center;
    }
    .swipe-dot {
      width: 26px; height: 26px;
      border-radius: 50%;
      background: color-mix(in srgb, var(--pink) 15%, transparent);
      border: 1px solid color-mix(in srgb, var(--pink) 40%, transparent);
      display: flex; align-items: center; justify-content: center;
      position: absolute;
      animation: dotSwipe 2.4s ease-in-out infinite;
    }
    .swipe-trail {
      position: absolute;
      top: 50%; height: 1px;
      background: linear-gradient(to left, transparent, color-mix(in srgb, var(--pink) 45%, transparent));
      transform: translateY(-50%);
      animation: trailAnim 2.4s ease-in-out infinite;
    }
    @keyframes dotSwipe {
      0%   { left: 65%; opacity: 0; }
      12%  { left: 65%; opacity: 1; }
      72%  { left: 12%; opacity: 1; }
      88%  { left: 12%; opacity: 0; }
      100% { left: 65%; opacity: 0; }
    }
    @keyframes trailAnim {
      0%,12% { left: 65%; width: 0; opacity: 0; }
      15%    { opacity: 0.7; }
      72%    { left: 12%; width: 53%; opacity: 0.7; }
      88%    { opacity: 0; }
      100%   { opacity: 0; }
    }

    :root {
      /* Warm dark palette — replaces the cool indigo/violet system.
         Surfaces sit on top of --bg. --card is the single paper-stock
         color shared by every card in the deck (top card face and
         both back layers) — a real deck's cards don't change color
         as you go down the stack; depth is carried by the offset,
         rotation, and border of each layer, not by tonal shifts.
         --surface-elevated is for non-card panels that need a step
         of elevation (dialogs, chat input, how-to-play sub-panels).
         --border-strong is for editorial dividers that carry
         hierarchy (panel titles, list rules). */
      --bg: #1a1612;
      --surface: #231f1a;
      --surface-elevated: #332e27;
      --card: #332e27;
      --border: #3d362e;
      --border-strong: #4d4640;
      /* Card outline — lighter than --border-strong so the card chrome
         reads clearly against --surface-elevated. Used by the welcome
         card-fan SVG, .card-back layers, and .quote-text. Kept separate
         from --border-strong so panel/dialog dividers stay quieter. */
      --card-border: #7a7164;
      --text: #f5ede0;
      --muted: #8a8377;       /* WCAG AA on --bg (5.8:1) */
      --pink: #d4a0a8;        /* dusty rose — primary accent */
      /* Foreground for text/icons sitting on a pink background (CTA pill,
         selected topic pill, #chatSend). Pairs with `background: var(--pink)`
         — never hand-pick a hex at the call site. Flips per mode so pink-
         as-bg always carries readable text. Dark: near-black on dusty rose
         ≈ 9:1 (AAA). Light: cream on deep rose ≈ 5:1 (AA). */
      --on-pink: #2b231a;
      /* Tint for "chip"-style subtle backgrounds & hairlines (panel-close,
         drag handle, header pill, timer pill, hairline rules). Used inside
         color-mix(in srgb, var(--chip-tint) X%, transparent) at each call
         site so the alpha is per-rule but the hue flips per mode. White on
         dark, warm near-black on cream — same intent ("a faint wash that
         carries an edge"), readable in either palette. */
      --chip-tint: #ffffff;
      --blue: #7dd3fc;        /* retained for link fallbacks */
      --red: #ef4444;
      --red-light: #fca5a5;
      --red-bg: rgba(239,68,68,0.15);
      --red-border: rgba(239,68,68,0.5);

      /* Typography tokens. Editorial surfaces (question card, panel
         titles, AI chat, dialog headlines) use --font-serif. UI chrome
         (buttons, menu labels, counters, form controls) uses
         --font-sans. Native stacks only — no web fonts. */
      --font-serif: Georgia, 'Times New Roman', 'Times', serif;
      --font-sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;

      /* ── Type scale ──────────────────────────────────────────
         Eight tiers covering every non-card typographic role in the
         app. Every new font-size should point at one of these. The
         scale was set in the typography audit (Phase 4k); outlier
         sizes (14, 16, 18, 20, 24, 26) were migrated to the nearest
         neighbor to eliminate sub-pixel drift.

           --t-caption     11px   rare meta / fine-print
           --t-meta        13px   caps headings, card-topic, counters
           --t-secondary   15px   topic pills, secondary controls
           --t-body        17px   all panel prose, CTAs, body copy
           --t-lede        19px   menu items, emphasised body
           --t-heading     22px   section headings (reserved)
           --t-title       28px   panel-title
           --t-display     clamp(48px, 12vw, 88px)   overlays, welcome

         The card question itself (#cardQuestion) is sized by
         fitQuoteText() in js/app.js — it uses a dynamic binary search
         against the available card body, not a scale tier. Time's Up
         hint and first-visit welcome marketing each keep their own
         clamp() because their sizing is viewport-driven. */
      --t-caption: 11px;
      --t-meta: 13px;
      --t-secondary: 15px;
      --t-body: 17px;
      --t-lede: 19px;
      --t-heading: 22px;
      --t-title: 28px;
      --t-display: clamp(48px, 12vw, 88px);

      /* ── Tracking tokens (letter-spacing) ────────────────────
         Collapsed from 11 values down to two plus default. Caps
         need measurable tracking to read; non-caps rarely benefit
         from sub-0.1em adjustments on screen. If a new class wants
         a tracking value outside these, add a ticket — don't slip
         in another 0.02em.

           --tr-caps-sm    0.12em   for 13px uppercase
           --tr-caps-lg    0.14em   for 17px+ uppercase

         The --tr-title token (0.01em) was retired in v130: at native
         Georgia italic the value was visually a no-op, and four use
         sites (.panel-title, .consent-title, .modal-title, #mainBtn,
         #appLogo) now rely on default tracking. If you ever need a
         measurable tune on italic serif, raise the value to 0.02em+
         and reintroduce as a named token.
       */
      --tr-caps-sm: 0.12em;
      --tr-caps-lg: 0.14em;

      /* ── Spacing scale ────────────────────────────────────────
         Five-step spacing token system, introduced in v130 (see
         critique-2026-04-24.md V12). Use these for new layout work
         in preference to literal pixel values. Existing one-offs
         (28px card gap, 22px card padding, 48/24/40 panel-sheet
         padding, etc.) were not migrated wholesale because the
         risk of visual regression outweighed the consistency win;
         migrate opportunistically as those areas are touched.

           --sp-1   4px    fine spacing (pill internal, badge)
           --sp-2   8px    tight stack (caption + meta)
           --sp-3   12px   default control padding
           --sp-4   20px   panel/section side padding
           --sp-5   28px   between-section, card padding, card gap
       */
      --sp-1: 4px;
      --sp-2: 8px;
      --sp-3: 12px;
      --sp-4: 20px;
      --sp-5: 28px;
    }

    /* ── Light mode (warm parchment palette) ───────────────────────────────
       See docs/light_mode_plan.md for rationale + palette decisions. The
       OS preference (`prefers-color-scheme: light`) is the default signal;
       a Light/Dark/Auto control in the Help panel footer flips
       `data-color-scheme` on <html> to override. The override selectors
       (specificity 0,1,1) beat the @media-wrapped :root rule (0,0,1) so a
       manual choice always wins regardless of OS state.

       Phase 2 deltas (shadows, scrims, white-on-dark "chip" backgrounds)
       live alongside the token block — they don't auto-flip via tokens
       because they reference rgba(0,0,0,*) or rgba(255,255,255,*) literals.
       Pink-with-alpha sites (e.g. `rgba(212,160,168,0.3)`) were refactored
       to `color-mix(in srgb, var(--pink) X%, transparent)` so they auto-
       flip; no override needed for those. */

    @media (prefers-color-scheme: light) {
      /* :not([data-color-scheme="dark"]) so an explicit user-picked Dark
         in light-OS still gets the dark tokens from :root. */
      html:not([data-color-scheme="dark"]) {
        --bg: #ece0c8;
        --surface: #e5d7ba;
        --surface-elevated: #f5ead3;
        --card: #fcf5e3;
        --border: #d6c6a6;
        /* --border-strong / --card-border tuned darker than the plan's
           initial #b8a680 so they hit ≥3:1 against bg/card (WCAG 1.4.11
           non-text UI). card-border > border-strong in visibility, mirroring
           the dark-mode hierarchy (card-border is the more-prominent edge). */
        --border-strong: #8a7656;  /* ≈ 3.3:1 on bg, 4.0:1 on card */
        --card-border: #7a6646;    /* ≈ 4.2:1 on bg, 5.1:1 on card */
        --text: #2b231a;
        /* --muted tuned to #635747 so it hits AA (≥4.5:1) on every
           surface it appears on, including --surface (panel-sheet body). */
        --muted: #635747;          /* ≈ 5.4:1 on bg, 5.0:1 on surface */
        --pink: #993556;           /* deep rose — ~5.4:1 on cream */
        --on-pink: #fcf5e3;        /* cream on deep rose — ~6.4:1 (AA) */
        --chip-tint: #2b231a;      /* warm near-black for subtle washes on cream */
        --blue: #0369a1;           /* darker blue passes AA on cream */
      }

      /* Phase 2 deltas — shadows, scrims, and chip backgrounds that
         used black/white literals. Lighter alpha on cream prevents
         the "muddy" look when dark-mode shadows are reused unchanged. */
      html:not([data-color-scheme="dark"]) .card-back {
        box-shadow: 0 2px 4px rgba(43, 35, 26, 0.15);
      }
      html:not([data-color-scheme="dark"]) .quote-text {
        box-shadow:
          0 12px 20px rgba(43, 35, 26, 0.16),
          0 3px 6px rgba(43, 35, 26, 0.10),
          inset 0 1px 0 rgba(255, 255, 255, 0.5),
          inset 0 -1px 0 rgba(43, 35, 26, 0.08);
      }
      html:not([data-color-scheme="dark"]) .chat-area {
        box-shadow: 0 12px 28px rgba(43, 35, 26, 0.18);
      }
      html:not([data-color-scheme="dark"]) .chat-message {
        filter: drop-shadow(0 4px 10px rgba(43, 35, 26, 0.14));
      }

      /* Modal scrims — keep darkening behavior in light mode (modals
         should still feel like a layer above) but warm the tone and
         drop the alpha so it's not pitch-black-on-cream. */
      html:not([data-color-scheme="dark"]) .secondary-panel,
      html:not([data-color-scheme="dark"]) #aiConsentOverlay,
      html:not([data-color-scheme="dark"]) #clearDataOverlay,
      html:not([data-color-scheme="dark"]) #clearDataConfirmOverlay {
        background: rgba(43, 35, 26, 0.4);
      }
      html:not([data-color-scheme="dark"]) #chatOverlay {
        background: rgba(43, 35, 26, 0.3);
      }

      /* Chip-style backgrounds (subtle white-on-dark in dark mode).
         Flip to subtle dark-on-cream so the visual boundary survives. */
      html:not([data-color-scheme="dark"]) #headerDeckPill {
        background: rgba(43, 35, 26, 0.04);
        border-color: rgba(43, 35, 26, 0.10);
      }
      html:not([data-color-scheme="dark"]) #headerDeckPill:hover {
        background: rgba(43, 35, 26, 0.07);
        border-color: rgba(43, 35, 26, 0.16);
      }
      html:not([data-color-scheme="dark"]) #updateBanner {
        background: rgba(43, 35, 26, 0.05);
      }
      html:not([data-color-scheme="dark"]) .panel-drag-handle {
        background: rgba(43, 35, 26, 0.18);
      }
      html:not([data-color-scheme="dark"]) .panel-close {
        background: rgba(43, 35, 26, 0.05);
        border-color: rgba(43, 35, 26, 0.10);
      }
      html:not([data-color-scheme="dark"]) .panel-close:hover {
        background: rgba(43, 35, 26, 0.10);
      }
      html:not([data-color-scheme="dark"]) #chatInput {
        background: rgba(43, 35, 26, 0.04);
      }
      html:not([data-color-scheme="dark"]) #ipadDeclutterBtn {
        background: rgba(43, 35, 26, 0.08);
        border-color: rgba(43, 35, 26, 0.22);
      }
      html:not([data-color-scheme="dark"]) body.ipad-declutter #ipadDeclutterBtn {
        border-color: rgba(43, 35, 26, 0.28);
      }
    }

    /* Manual override: explicit Light. Specificity 0,1,1 beats the
       @media-wrapped :root rule (0,0,1) so this wins regardless of OS. */
    html[data-color-scheme="light"] {
      --bg: #ece0c8;
      --surface: #e5d7ba;
      --surface-elevated: #f5ead3;
      --card: #fcf5e3;
      --border: #d6c6a6;
      --border-strong: #8a7656;
      --card-border: #7a6646;
      --text: #2b231a;
      --muted: #635747;
      --pink: #993556;
      --on-pink: #fcf5e3;
      --chip-tint: #2b231a;
      --blue: #0369a1;
    }
    html[data-color-scheme="light"] .card-back {
      box-shadow: 0 2px 4px rgba(43, 35, 26, 0.15);
    }
    html[data-color-scheme="light"] .quote-text {
      box-shadow:
        0 12px 20px rgba(43, 35, 26, 0.16),
        0 3px 6px rgba(43, 35, 26, 0.10),
        inset 0 1px 0 rgba(255, 255, 255, 0.5),
        inset 0 -1px 0 rgba(43, 35, 26, 0.08);
    }
    html[data-color-scheme="light"] .chat-area {
      box-shadow: 0 12px 28px rgba(43, 35, 26, 0.18);
    }
    html[data-color-scheme="light"] .chat-message {
      filter: drop-shadow(0 4px 10px rgba(43, 35, 26, 0.14));
    }
    html[data-color-scheme="light"] .secondary-panel,
    html[data-color-scheme="light"] #aiConsentOverlay,
    html[data-color-scheme="light"] #clearDataOverlay,
    html[data-color-scheme="light"] #clearDataConfirmOverlay {
      background: rgba(43, 35, 26, 0.4);
    }
    html[data-color-scheme="light"] #chatOverlay {
      background: rgba(43, 35, 26, 0.3);
    }
    html[data-color-scheme="light"] #headerDeckPill {
      background: rgba(43, 35, 26, 0.04);
      border-color: rgba(43, 35, 26, 0.10);
    }
    html[data-color-scheme="light"] #headerDeckPill:hover {
      background: rgba(43, 35, 26, 0.07);
      border-color: rgba(43, 35, 26, 0.16);
    }
    html[data-color-scheme="light"] #updateBanner {
      background: rgba(43, 35, 26, 0.05);
    }
    html[data-color-scheme="light"] .panel-drag-handle {
      background: rgba(43, 35, 26, 0.18);
    }
    html[data-color-scheme="light"] .panel-close {
      background: rgba(43, 35, 26, 0.05);
      border-color: rgba(43, 35, 26, 0.10);
    }
    html[data-color-scheme="light"] .panel-close:hover {
      background: rgba(43, 35, 26, 0.10);
    }
    html[data-color-scheme="light"] #chatInput {
      background: rgba(43, 35, 26, 0.04);
    }
    html[data-color-scheme="light"] #ipadDeclutterBtn {
      background: rgba(43, 35, 26, 0.08);
      border-color: rgba(43, 35, 26, 0.22);
    }
    html[data-color-scheme="light"] body.ipad-declutter #ipadDeclutterBtn {
      border-color: rgba(43, 35, 26, 0.28);
    }

    /* ── Focus indicators (a11y) ──────────────────────────── */
    /* Visible ring for keyboard users; hidden for mouse/touch */
    :focus-visible {
      outline: 2px solid var(--pink);
      outline-offset: 2px;
    }
    :focus:not(:focus-visible) {
      outline: none;
    }

    /* Screen-reader-only utility (A5). Content stays in the DOM and
       accessibility tree so AT can read it, but it occupies no visual
       space. Used for <label> elements attached to #chatInput and
       #timerCustomInput where the visible design has no room for a
       label above the field. Not a "display: none" — that would hide
       the label from AT too and defeat the purpose. */
    .sr-only {
      position: absolute;
      width: 1px;
      height: 1px;
      padding: 0;
      margin: -1px;
      overflow: hidden;
      clip: rect(0, 0, 0, 0);
      white-space: nowrap;
      border: 0;
    }

    html, body {
      height: 100%;
      min-height: 100dvh;
      background: var(--bg);
      color: var(--text);
      font-family: var(--font-sans);
      font-weight: 300;
      overscroll-behavior: none;
      /* Beta M4/M5: kill iOS double-tap-to-zoom. A double tap on the card
         zoomed the layout, made the page scrollable, and pushed chrome
         offscreen ("minimalist mode" in tester words) — and degraded the
         swipe gesture in landscape. `manipulation` = pan + pinch-zoom
         only, so accessibility pinch-zoom is preserved; only the
         double-tap gesture (and its 350ms tap delay) is removed. */
      touch-action: manipulation;
      /* M6: suppress the iOS Safari gray flash on tap. Replaced per
         element with meaningful :active states (.card-solo button,
         .sec-nav-btn, #mainBtn, etc.) so users still get touch
         feedback in the brand vocabulary instead of OS-default gray. */
      -webkit-tap-highlight-color: transparent;
    }

    /* ── Header ─────────────────────────────────────────────── */
    #appHeader {
      position: fixed;
      top: 0; left: 0; right: 0;
      z-index: 10;
      display: flex;
      align-items: center;
      justify-content: space-between;
      padding: calc(16px + env(safe-area-inset-top)) 20px 12px;
      background: linear-gradient(to bottom, var(--bg) 70%, transparent);
    }

    #appLogo {
      /* Sans (system-ui) for the logo, not the serif used on editorial
         surfaces. The brand mark wants a stable, fixed identity — clean
         geometry reads as signage, not voice. Italic is also disallowed
         here for the same reason (see font-style below).

         v130: bumped from 20px → --t-heading (22px) so the in-app brand
         doesn't read as a footnote next to the card-counter. The 8-tier
         scale's nearest neighbor to 24px, kept inside the system. */
      font-family: var(--font-sans);
      font-weight: 600;
      font-style: normal;
      font-size: var(--t-heading);
      color: var(--pink);
      text-decoration: none;
      user-select: none;
      -webkit-user-select: none;
      /* Kill iOS double-tap-to-zoom on the wordmark — touch-action isn't
         inherited, so the body-level rule (css ~648) doesn't cover this
         span; manipulation preserves pinch-zoom, drops only double-tap. */
      touch-action: manipulation;
    }

    /* Top-right card counter (phase 4i). Muted sans with tabular
       digits so the "Card 7 of 247" readout doesn't shimmy as N ticks
       up. Hidden before the first draw — we let the welcome screen
       breathe. */
    #cardCounter {
      font-family: var(--font-sans);
      font-size: var(--t-meta);
      font-weight: 500;
      color: var(--muted);
      font-variant-numeric: tabular-nums;
      white-space: nowrap;
      user-select: none;
      -webkit-user-select: none;
      margin-left: 10px;
    }

    /* Header deck pill (Phase 5 mock) — top-level "what am I playing"
       readout. Sits to the left of the card counter; tap opens the
       merged Deck sheet. margin-left: auto in a 3-child header pushes
       this and the counter to the right edge while the logo holds the
       left. The empty-pool state (.is-empty) bumps color to warning so
       the pill itself signals the user can't draw. */
    #headerDeckPill {
      margin-left: auto;
      font-family: var(--font-sans);
      font-size: var(--t-meta);
      font-weight: 500;
      color: var(--muted);
      background: color-mix(in srgb, var(--chip-tint) 4%, transparent);
      border: 1px solid color-mix(in srgb, var(--chip-tint) 8%, transparent);
      border-radius: 999px;
      padding: 5px 12px;
      cursor: pointer;
      user-select: none;
      -webkit-user-select: none;
      white-space: nowrap;
      transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
      position: relative;
      /* Same double-tap-zoom suppression as #appLogo — header controls aren't
         covered by the non-inherited body-level touch-action. */
      touch-action: manipulation;
    }
    /* A6 — invisible 44px hit-target expander (matches .panel-close pattern). */
    #headerDeckPill::before {
      content: '';
      position: absolute;
      inset: -8px;
    }
    #headerDeckPill:hover {
      background: color-mix(in srgb, var(--chip-tint) 8%, transparent);
      border-color: color-mix(in srgb, var(--chip-tint) 14%, transparent);
      color: var(--text);
    }
    #headerDeckPill:active { opacity: 0.7; }
    #headerDeckPill.is-empty { color: var(--warning, #e8a87c); }
    body.cinematic #headerDeckPill { display: none; }


    /* #headerLine was retired in v130 (V14) — the linear-gradient on
       #appHeader provides the visual separation, the hairline added
       a competing edge vocabulary. The element is also removed from
       index.html. */

    /* ── Main layout — collapsed, quoteArea is fixed ────────── */
    #mainWrap {
      min-height: 0;
      height: 0;
      padding: 0;
    }

    /* ── Quote area — fixed zone between header and controls ── */
    /* Acts as the "stage" holding the card deck. Contains two decorative
       .card-back siblings and a #quoteContent wrapper for dynamic HTML.
       overflow: hidden clips the off-screen slide animation and also the
       rotated card-backs so their corners don't poke past the stage edges. */
    #quoteArea {
      position: fixed;
      /* top/bottom/height set by JS on load + resize */
      left: 0;
      right: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      overflow: hidden;
      z-index: 5;
      padding: 0 28px;
      box-sizing: border-box;
      text-align: center;
      /* Force GPU compositing layer — prevents iOS fixed-element jitter */
      -webkit-transform: translateZ(0);
      transform: translateZ(0);
    }

    /* Card-back layers — decorative deck sitting behind the top card.
       Dimensions come from --card-w/--card-h (set by positionQuoteArea)
       so they match the top card exactly, and the "deck" illusion comes
       from rotation + translation, not size difference.

       Opaque by design: earlier versions used opacity: 0.75 / 0.55 to
       fake depth, but that let the top card's text bleed through mid-
       slide-animation. Real cards are solid.

       All three card surfaces (top + both backs) share --card — a real
       deck's cards are identical stock, so the "stacked" visual relies
       on each card's 1px border, shadow, rotation, and offset, not on
       tonal steps between layers. Earlier versions darkened each back
       progressively; that broke the playing-card metaphor. */
    .card-back {
      position: absolute;
      top: 50%;
      left: 50%;
      /* box-sizing must match .quote-text (border-box) so the 1px border
         doesn't make the backs 2px wider/taller than the top card. */
      box-sizing: border-box;
      /* Fallback dims match the 0.72 playing-card aspect used by the JS —
         only seen in the instant before positionQuoteArea() sets the
         vars, but a box-shaped fallback would flash a non-card. */
      width: var(--card-w, 300px);
      height: var(--card-h, 416px);
      border-radius: 22px;
      border: 1px solid var(--card-border);
      box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
      pointer-events: none;
    }
    .card-back-1 {
      transform: translate(-50%, -50%) translate(9px, 11px) rotate(2.4deg);
      background: var(--card);
      z-index: 1;
    }
    .card-back-2 {
      transform: translate(-50%, -50%) translate(-11px, 18px) rotate(-3deg);
      background: var(--card);
      z-index: 0;
    }
    /* v130 V16 — third card-back layer. Two-card stack read as "a couple
       of cards", not "a deck"; adding a third (smallest rotation, deepest
       offset) tips the visual into deck-territory. Sits behind the other
       two (z-index: -1) and uses a slightly darker background so it
       recedes optically rather than competing with .card-back-1's frame
       and corner labels. */
    .card-back-3 {
      transform: translate(-50%, -50%) translate(-3px, 25px) rotate(4.5deg);
      background: var(--card);
      filter: brightness(0.92);
      z-index: -1;
    }
    /* Back-of-deck brand mark — same three-icon set as the welcome-fan
       center card, painted onto .card-back-1 so it reads as the mark
       on the back of every card in the deck. vector-effect keeps stroke
       width visually consistent at the card-back's ~300px rendered size
       (viewBox 84×120 would otherwise scale strokes ~3.57×).
       Visible from the first paint. The earlier design kept this hidden
       until body.has-drawn-card was set (so the welcome screen read as
       a plain deck), but the 2026-04-24 intro redesign moved the brand
       onto the TOP card as well — the stacked deck should look like
       matching branded backs front-to-back from load. */
    .card-back-logo {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
      opacity: 1;
    }
    /* Stroke width is in user units with non-scaling-stroke, so the
       value here renders as that many px regardless of card-back size.
       Much heavier than the welcome fan's 1.5 because the card back
       renders ~5× larger and a thin line reads as faint pencil at
       that scale — we want a confident, inked brand mark. */
    .card-back-logo path,
    .card-back-logo ellipse {
      vector-effect: non-scaling-stroke;
      stroke-width: 4;
    }

    /* Inset frame on .card-back-1 — a thin rule sitting just inside
       the card border. Border color is --text (the same variable the
       question text uses) so the frame and the front-of-card type read
       as the same ink. Its TL and BR corners fall directly behind the
       GET REAL labels; the labels carry a solid card-colored background
       (see .card-back-label below), which masks the frame line exactly
       where the text crosses, so the rule appears to interrupt cleanly
       at both corners. Hidden on the welcome deck with the rest of the
       back-of-deck branding. */
    .card-back-frame {
      position: absolute;
      top: 22px;
      left: 22px;
      right: 22px;
      bottom: 22px;
      /* Same ink as --text but at 50% alpha. A 1px hairline in the full
         --text color reads visually brighter than the larger serif
         question text on the front of the card, so we dim the frame to
         match the perceived weight of the type. color-mix() flips the
         tint per mode (warm cream in dark, warm near-black in light) so
         the frame stays visible on either card stock. */
      border: 1px solid color-mix(in srgb, var(--text) 50%, transparent);
      border-radius: 8px;
      pointer-events: none;
      opacity: 1;
    }

    /* Playing-card corner labels on .card-back-1 — "GET REAL" in the
       top-left and the same rotated 180° in the bottom-right, the way
       rank/suit marks sit on a real playing card so the deck reads
       right-side-up from either orientation. Typography matches
       .card-topic (sans 600, --t-meta, --tr-caps-lg, --pink) so the
       back-of-deck brand and the front-of-card topic stamp feel like
       the same typographic system. Visible from first paint — see the
       note on .card-back-logo for the 2026-04-24 intro redesign that
       retired the has-drawn-card gate.

       The background matches --card (the shared card stock) so the
       label reads as a solid card-colored block that punches the frame
       rule behind it — the frame appears to break exactly where the
       text crosses it. Using the token directly means the mask always
       follows the card surface if --card ever changes. */
    .card-back-label {
      position: absolute;
      font-family: var(--font-sans);
      font-weight: 600;
      font-size: var(--t-meta);
      letter-spacing: var(--tr-caps-lg);
      color: var(--muted);
      line-height: 1;
      text-transform: uppercase;
      white-space: nowrap;
      pointer-events: none;
      background: var(--card);
      padding: 2px 5px;
      opacity: 1;
    }
    .card-back-label-tl {
      top: 15px;
      left: 15px;
    }
    .card-back-label-br {
      bottom: 15px;
      right: 15px;
      transform: rotate(180deg);
    }

    /* Dynamic-content wrapper. Everything that used to be written directly
       into #quoteArea now goes here so the card-backs above survive
       innerHTML replacement. Fills the stage and centers its content so
       the card (set to --card-w/--card-h) sits exactly where the backs'
       centered transform expects. */
    #quoteContent {
      position: relative;
      z-index: 3;
      width: 100%;
      display: flex;
      align-items: center;
      justify-content: center;
    }

    /* Top card of the deck. This element is what the draw animation
       translates (translateX in getReal / showPrevious), so giving it
       card chrome means the whole card flicks off — not just the text.
       Dimensions come from --card-w/--card-h (set by positionQuoteArea)
       so the card is a fixed size regardless of text length: short and
       long quotes both render in the same-shaped card. fitQuoteText
       scales the font to fit; text is centered via flex. Outer box-shadow
       is clipped by #quoteArea's overflow:hidden (needed for the slide
       animation), so depth is carried by strong border + inset highlight
       and an outer shadow that fits inside the qa's vertical padding. */
    .quote-text {
      position: relative;
      color: var(--text);
      /* -webkit-text-fill-color: currentColor (not var(--text)) so the
         keyword resolves at used-value time against each descendant's
         own color. This replaces the prior cascade, which fixed
         text-fill-color to --text and forced 20+ defensive overrides
         on children (.card-topic, .card-solo, AI chat bubbles, etc.).
         See the Phase 4k typography audit in README.md. */
      -webkit-text-fill-color: currentColor;
      opacity: 0;
      transform: translateY(12px);
      transition: opacity 0.35s ease, transform 0.35s ease;
      will-change: transform, opacity;
      /* ── Top-card chrome ────────────────────────────────────── */
      /* NOTE: no max-width. The old `calc(100% - 24px)` resolved against
         #quoteContent (viewport − 56) while .card-back's resolved against
         #quoteArea's padding-box (viewport), making the top card narrower
         than the backs. JS caps --card-w so this element always fits. */
      box-sizing: border-box;
      width: var(--card-w, 300px);
      height: var(--card-h, 416px);
      padding: 22px 22px;
      background: var(--card);
      border: 1px solid var(--card-border);
      border-radius: 22px;
      box-shadow:
        0 12px 20px rgba(0, 0, 0, 0.45),
        0 3px 6px rgba(0, 0, 0, 0.3),
        inset 0 1px 0 rgba(255, 255, 255, 0.05),
        inset 0 -1px 0 rgba(0, 0, 0, 0.2);
      /* Three-zone layout: topic (top), body (flex:1), solo (bottom).
         Gap sets the minimum breathing room between topic↔question and
         question↔solo — the body zone is flex:1 so any slack beyond the
         gap goes into the body and keeps the question visually centered. */
      display: flex;
      flex-direction: column;
      gap: 28px;
      text-align: left;
    }
    .quote-text.visible {
      opacity: 1;
      transform: translateY(0);
    }
    .quote-text.exit {
      opacity: 0;
      transform: translateY(-10px);
      transition: opacity 0.2s ease, transform 0.2s ease;
    }

    /* ── In-card zones ──────────────────────────────────────────
       Topic label — small uppercase pink type at top-left. Matches the
       mockup's editorial stamp ("REGRET", "RELATIONSHIPS", etc.). Empty
       when a question has no topics array — collapses via :empty. */
    .card-topic {
      flex: 0 0 auto;
      font-family: var(--font-sans);
      font-weight: 600;
      font-size: var(--t-meta);
      /* Was 0.2em — tightened to the canonical caps-lg token in Phase 4k.
         Still reads as an editorial stamp at 0.14em while conforming to
         the four-value tracking system (0, title, caps-sm, caps-lg). */
      letter-spacing: var(--tr-caps-lg);
      color: var(--pink);
      line-height: 1;
      min-height: 13px; /* reserve space even when empty so body doesn't jump */
    }
    /* "Played" checkmark — top-right corner badge shown only on a card the
       user has swiped back to (see .is-reviewing toggle in setCardContent).
       Absolute so it rides the slide animation as a child of .quote-text and
       doesn't disturb the three-zone flex layout. Tokens via color-mix so the
       fill/border flip correctly in light mode (never bake palette RGB). */
    .played-badge {
      display: none;
      position: absolute;
      top: 16px; right: 16px;
      align-items: center;
      justify-content: center;
      width: 24px; height: 24px;
      border-radius: 999px;
      background: color-mix(in srgb, var(--chip-tint) 6%, transparent);
      border: 1px solid color-mix(in srgb, var(--pink) 45%, transparent);
      pointer-events: none;
      z-index: 2;
    }
    .quote-text.is-reviewing .played-badge { display: inline-flex; }
    .played-badge svg {
      width: 13px; height: 13px;
      fill: none;
      stroke: var(--pink);
      stroke-width: 2;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    /* Body — the middle slice that holds the question. Flex:1 grows to
       absorb all leftover vertical space; its clientHeight is what
       fitQuoteText() measures against. min-height:0 lets it shrink
       below its content's intrinsic size (standard flex gotcha).
       overflow:hidden guards against a measurement edge case where the
       binary search fails and a 20px minimum still overflows. */
    .card-body {
      flex: 1 1 auto;
      min-height: 0;
      display: flex;
      align-items: center;
      justify-content: center;
      overflow: hidden;
    }
    .card-question {
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: normal;
      font-size: 32px; /* fallback — overridden by fitQuoteText() */
      line-height: 1.3;
      color: var(--text);
      margin: 0;
      text-align: center;
      word-break: normal;
      overflow-wrap: break-word;
      /* Without max-width, a flex item's main-axis size can stretch to
         max-content — long one-liners would blow past the card edge
         before wrapping. 100% clamps to the flex-row container's width
         (which equals body zone width), letting overflow-wrap engage. */
      max-width: 100%;
    }
    /* Solo CTA — bottom-left pink italic link. "Sit with this →" by
       default (U7 removed "alone"); swaps to "Back tomorrow →" when the
       daily AI budget is spent. Tapping opens the one-on-one reflection
       chat; the consent dialog explains the solo-AI flow. */
    .card-solo {
      flex: 0 0 auto;
    }
    .card-solo button {
      background: none;
      border: none;
      /* C5: was padding:0 — link looked like prose and the row was 21px tall.
         Pad vertically so the row is 44+ to meet the touch-target floor;
         negative inline margin keeps the visual baseline anchored where it
         was so the rest of the card composition doesn't shift. */
      padding: 12px 8px;
      margin: 0 -8px;
      color: var(--pink);
      font-family: var(--font-serif);
      /* Italic demoted in Phase 4k — this is a link/button action, not a
         voice. Italic is now reserved for panel titles, times-up copy,
         first-visit welcome, and the AI chat voice. */
      font-weight: 400;
      font-size: 18px;
      text-align: left;
      cursor: pointer;
      transition: opacity 0.2s, color 0.2s;
    }
    .card-solo button:hover,
    .card-solo button:active { color: var(--text); }
    body.offline .card-solo button,
    body.solo-capped .card-solo button {
      opacity: 0.45;
    }
    body.offline .card-solo button {
      pointer-events: none;
      cursor: default;
    }

    /* Thumbs feedback — anonymous per-question vote, bottom-right corner.
       Absolute (like .played-badge) so it stays out of the three-zone flex
       flow and doesn't affect fitQuoteText()'s body measurement; rides the
       slide animation as a child of .quote-text. .card-solo is bottom-LEFT,
       so the corners don't collide. Tokens only (color-mix) so it flips in
       light mode — never bake palette RGB. */
    .card-feedback {
      position: absolute;
      bottom: 12px;
      right: 12px;
      display: flex;
      gap: 2px;
    }
    .thumb-btn {
      display: inline-flex;
      align-items: center;
      justify-content: center;
      width: 36px; height: 36px;
      padding: 0;
      background: none;
      border: none;
      border-radius: 999px;
      color: var(--muted);
      cursor: pointer;
      -webkit-tap-highlight-color: transparent;
      transition: color 0.15s ease, background 0.15s ease;
    }
    .thumb-btn svg {
      width: 19px; height: 19px;
      fill: none;
      stroke: currentColor;
      stroke-width: 1.6;
      stroke-linecap: round;
      stroke-linejoin: round;
    }
    .thumb-btn:active { background: color-mix(in srgb, var(--chip-tint) 8%, transparent); }
    /* Active (the user's current vote): up reads as positive (pink), down as
       muted-but-filled so the state is unmistakable without shouting. */
    .thumb-btn.active { color: var(--pink); }
    .thumb-btn.active svg { fill: color-mix(in srgb, var(--pink) 22%, transparent); }

    /* ── Intro cards (pre-first-draw) ───────────────────────────
       Two "decorative" top cards that sit in #quoteContent before the
       deck is actually played. Both are rendered as .quote-text elements
       so the existing slide-in/slide-out animation pipeline can drive the
       transitions between them without bespoke logic. Their children
       override the default three-zone (topic/body/solo) layout.

       Flow:
         boot → .intro-logo-card (nudges left-then-back until swiped)
         swipe left → .intro-ready-card ("Real conversation — whenever
                      you're ready.")  — no nudge, calm
         swipe left → first real question via getReal()

       On PWA backgrounding the state is reset so the next open restarts
       at .intro-logo-card. See advanceIntro() + visibilitychange handler
       in js/app.js. */

    /* Common: cancel the three-zone flex layout so absolute-positioned
       children (frame, corner labels, center logo) stack cleanly. The
       card chrome — size, background, border, radius, shadow — stays
       inherited from .quote-text. */
    .quote-text.intro-logo-card,
    .quote-text.intro-ready-card {
      display: block;
      position: relative;
      padding: 0;
      overflow: hidden;
    }

    /* Logo card. Mirrors .card-back-1: inset frame + two GET REAL corner
       labels + stacked lightbulb/heart/firecracker SVG centered. The
       SVG's stroke, positioning, and typography match the back-of-deck
       brand mark so the intro card reads as "lifted off the deck." Keep
       this structure in sync with .card-back-1 in index.html. */
    .intro-logo-frame {
      position: absolute;
      top: 22px;
      left: 22px;
      right: 22px;
      bottom: 22px;
      /* See .card-back-frame — color-mix() so the hairline flips per mode. */
      border: 1px solid color-mix(in srgb, var(--text) 50%, transparent);
      border-radius: 8px;
      pointer-events: none;
    }
    .intro-logo-label {
      position: absolute;
      font-family: var(--font-sans);
      font-weight: 600;
      font-size: var(--t-meta);
      letter-spacing: var(--tr-caps-lg);
      color: var(--pink);
      line-height: 1;
      text-transform: uppercase;
      white-space: nowrap;
      pointer-events: none;
      background: var(--card);
      padding: 2px 5px;
    }
    .intro-logo-label-tl {
      top: 15px;
      left: 15px;
    }
    .intro-logo-label-br {
      bottom: 15px;
      right: 15px;
      transform: rotate(180deg);
    }
    .intro-logo-svg {
      position: absolute;
      inset: 0;
      width: 100%;
      height: 100%;
      pointer-events: none;
    }
    .intro-logo-svg path,
    .intro-logo-svg ellipse {
      vector-effect: non-scaling-stroke;
      stroke-width: 4;
    }

    /* Ready card. Serif italic welcome line, a spacer's worth of breathing
       room, then three short beats ("OPEN UP · DISCUSS · CONNECT")
       centered below it. flex-direction:column stacks the two paragraphs;
       .intro-ready-msg keeps the warm italic; .intro-ready-steps shifts
       to sans 600 caps with --tr-caps-lg tracking so the triad reads as
       a discrete label-style stepper (matching the .card-topic recipe),
       not as a continuation of the serif welcome sentence. v130 V5
       moved this off serif italic — too many serif italic elements on
       the same card was reading as one undifferentiated voice. */
    /* justify-content:flex-start + auto margins on the first/last children
       (see .intro-ready-msg margin-top:auto and .intro-ready-steps
       margin-bottom:auto) centers the block when the card has spare height
       but collapses to top-aligned when it doesn't — so a long rotating hint
       on a short screen (iPhone mini, older iOS with taller text metrics)
       overflows off the BOTTOM (the low-priority stepper) instead of clipping
       the welcome headline off the top. Auto margins are universally
       supported, unlike `justify-content: safe center`, which an older
       WebKit would drop. */
    .intro-ready-card {
      display: flex !important;
      flex-direction: column;
      align-items: center;
      justify-content: flex-start;
      padding: 28px !important;
    }
    .intro-ready-msg {
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: italic;
      /* Sized off the CARD HEIGHT (--card-h), not width or viewport. Height is
         the binding constraint for fitting headline + hint together: on a short
         card (iPhone mini, or any small viewport) the headline must shrink so
         it stops wrapping to 4 lines and crowding the hint out the bottom. The
         clamp floor/ceiling keeps it readable on tiny cards and from ballooning
         on desktop. (Previously sized off --card-w, which let a short-but-not-
         narrow card keep a big 4-line headline.) */
      font-size: clamp(16px, calc(var(--card-h, 416px) * 0.06), 27px);
      line-height: 1.3;
      color: var(--text);
      text-align: center;
      margin: auto 0 0;
      letter-spacing: 0.005em;
    }
    .intro-ready-steps {
      font-family: var(--font-sans);
      font-weight: 600;
      /* Smaller than the serif welcome line above — these are
         caption-scale labels, not body type. Caps + wide tracking
         carry the visual weight that 24px italic was carrying before. */
      font-size: var(--t-secondary);
      line-height: 1.5;
      color: var(--text);
      text-align: center;
      margin: 1.2em 0 auto;
      letter-spacing: var(--tr-caps-lg);
      text-transform: uppercase;
    }
    /* Rotating tip — the actionable copy on this card. Sits between the
       serif welcome line and the OPEN UP · DISCUSS · CONNECT stepper.
       Body type (17px) so it's the most prominent sans element on the
       card; the stepper underneath becomes the quieter recurring anchor. */
    .intro-ready-hint {
      font-family: var(--font-sans);
      /* Also height-scaled (capped at the 17px body size) so a long hint on a
         short card shrinks to fit rather than clipping off the bottom. */
      font-size: clamp(13px, calc(var(--card-h, 416px) * 0.046), 17px);
      line-height: 1.42;
      /* color-mix() so the tint flips per mode (warm cream in dark,
         warm near-black in light) — a hardcoded rgba bakes in the dark
         token and disappears against the cream card surface in light. */
      color: color-mix(in srgb, var(--text) 85%, transparent);
      text-align: center;
      margin: 1.2em auto 0;
      max-width: 28ch;
      text-wrap: balance;
    }
    .intro-ready-hint-label {
      font-weight: 600;
      letter-spacing: var(--tr-caps-lg);
      text-transform: uppercase;
      color: var(--pink);
      margin-right: 0.5em;
    }
    .intro-ready-hint-label::after {
      content: ":";
      margin-left: 0.05em;
    }

    /* Nudge hint — the logo card sways left then back on a loop to hint
       at swipe-left. Starts only after the slide-in completes (the .nudging
       class is toggled in JS once inline transform is cleared), and stops
       the moment the user commits a swipe. The keyframes use translateX
       only so they don't fight the base .visible transform (translateY 0). */
    /* v130 V19 — slowed the nudge cycle from 2.8s to 4s and tightened
       the hint to the first ~22% of the loop (was 30%). The previous
       cadence was insistent enough that it read as "broken" rather
       than "you can swipe"; the longer rest between nudges lets the
       intro card breathe and matches the unhurried tone of the deck. */
    @keyframes intro-nudge {
      0%   { transform: translateX(0); }
      12%  { transform: translateX(-16px); }
      22%  { transform: translateX(0); }
      100% { transform: translateX(0); }
    }
    .quote-text.intro-logo-card.nudging {
      animation: intro-nudge 4s ease-in-out infinite;
    }

    /* Reduced-motion handling for the nudge (and everything else)
       lives in the consolidated @media (prefers-reduced-motion: reduce)
       block at the bottom of this file. */

    /* ── Deck shuffle animation ──────────────────────────────────
       Replaces the "Shuffling deck — you'll see some questions again"
       toast that fired when a user drew through the full active pool.
       The animation: eject the spent question card upward, then 10
       card-back floaters streak in from off-screen, alternating CW/CCW
       spin, staggered ~110ms apart, each landing on the deck's rest
       position. End state matches the intro-logo-card with the nudge
       hint, so the user is back at "tap to draw" — a clean cycle
       boundary instead of a silent reshuffle. Total ≈1.9s.

       Floater cards are cloned from .card-back-1 in JS, so they share
       its visual treatment (frame, corner labels, brand SVG). The
       rules below add the in-flight z-index, lift shadow, and the
       transition spec the per-card flight uses. The flight transform
       itself is set inline on each element by playShuffleAnimation(). */
    .shuffle-fly {
      /* Ride above .card-back-1 (z-index 1) and #quoteContent (z-index 3)
         while the floaters are in flight. Sit below the header/nav
         (z-index 10/11) so an arriving card never overlaps chrome. */
      z-index: 8;
      /* Lifted shadow during flight; the static .card-back-1 underneath
         has only a 2px subtle shadow, so floaters read as "above" the
         deck before they settle. */
      box-shadow: 0 12px 28px rgba(0, 0, 0, 0.45);
      pointer-events: none;
      /* Flight tween: transform handles the position+rotation+scale
         travel; opacity fades the card in over the first half of the
         flight so it doesn't pop into view at full strength. JS sets
         this transition inline alongside the destination transform; the
         declaration here is a fallback in case JS leaves the floater
         orphaned (e.g., topic change mid-flight) — the same easing
         keeps any cleanup motion consistent. */
      transition: transform 0.6s cubic-bezier(0.2, 0.7, 0.2, 1),
                  opacity 0.3s ease-out;
      will-change: transform, opacity;
    }

    /* Eject keyframe for the previous question card. Slides up + scales
       down + fades out — quick (180ms) so it doesn't delay the storm. */
    @keyframes shuffleEject {
      0%   { transform: translateX(0) translateY(0) scale(1);     opacity: 1; }
      100% { transform: translateX(0) translateY(-56px) scale(0.92); opacity: 0; }
    }
    .quote-text.shuffle-eject {
      animation: shuffleEject 0.18s cubic-bezier(0.4, 0, 1, 1) forwards;
    }

    /* ── Cinematic mode ──────────────────────────────────────────
       Shared treatment for landscape phone + iPad declutter. The
       question becomes the only element on screen; topic label, solo
       CTA, and back cards are suppressed so the card reads as a single
       focused surface with serif text front-and-center. .card-body
       (flex: 1 1 auto) absorbs all freed vertical space, so fitQuoteText
       can scale the question larger to fill the card. */
    /* Cinematic / landscape / iPad-declutter modes hide the topic stamp
       and solo CTA so the question fills the card. (Used to be scoped to
       .card-meta-row, which wrapped the topic + the now-retired pack
       badge — the wrapper went with the badge.) */
    body.cinematic .card-topic,
    body.cinematic .card-solo,
    body.landscape .card-topic,
    body.ipad-declutter .card-topic {
      display: none;
    }
    body.cinematic .card-back {
      display: none;
    }

    /* ── Full-screen: drop the card entirely ─────────────────────
       Applies to two modes:
         • body.landscape — phone in landscape (group-game surface,
           phone on a table, heads leaning in)
         • body.ipad-declutter — iPad "full screen" toggle
       Both strip the card chrome so the question is pure serif text on
       --bg, sized to fill the viewport by fitQuoteText's widened
       full-screen cap (180px). Previously iPad declutter kept the card
       frame, but users expected parity with phone landscape. */
    body.landscape .quote-text,
    body.ipad-declutter .quote-text {
      background: none;
      border: none;
      border-radius: 0;
      box-shadow: none;
      padding: 0;
      /* Width/height come from --card-w/--card-h set by positionQuoteArea,
         which uses the full viewport minus padding in these modes. */
    }
    body.landscape .card-question,
    body.ipad-declutter .card-question {
      line-height: 1.25;
    }

    .status-msg {
      font-family: var(--font-sans);
      font-weight: 300;
      font-size: clamp(17px, 3.5vw, 20px);
      color: var(--muted);
      text-align: center;
    }

    /* ── Loading state ──────────────────────────────────────── */
    /* While questions load, the .card-back-1 brand mark IS the loader — a
       gentle breathe — and the caption is pinned to the bottom of the card so
       it never collides with the centered heart icon. Scoped to
       #quoteArea.is-loading (toggled in showLoading/showWelcome, js/app.js). */
    #quoteArea.is-loading .card-back-logo {
      animation: brandBreathe 1.6s ease-in-out infinite;
      transform-origin: center;
    }
    @keyframes brandBreathe {
      0%, 100% { opacity: 0.45; transform: scale(0.97); }
      50%      { opacity: 1;    transform: scale(1); }
    }
    /* Size #quoteContent to the card so the caption can bottom-anchor to the
       card edge; --card-h is kept in sync by positionQuoteArea(). */
    #quoteArea.is-loading #quoteContent { height: var(--card-h, 416px); }
    .loading-state {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 14px;
    }
    #quoteArea.is-loading .loading-state {
      height: 100%;
      justify-content: flex-end;
      padding-bottom: 26px;
      box-sizing: border-box;
    }
    .loading-text {
      font-family: var(--font-sans);
      font-size: var(--t-meta);
      color: var(--muted);
    }

    /* ── Bottom controls (fixed) ────────────────────────────── */
    #bottomControls {
      position: fixed;
      bottom: 0; left: 0; right: 0;
      z-index: 10;
      display: flex;
      flex-direction: column;
      align-items: center;
      padding: 0 16px max(28px, env(safe-area-inset-bottom));
      background: linear-gradient(to top, var(--bg) 60%, transparent);
      gap: 10px;
    }

    /* ── Play Solo link ───────────────────────────────────────
       Legacy off-card button. As of the 2026-04 redesign the solo CTA
       lives INSIDE the card (.card-solo button) so this element is
       force-hidden. Kept in DOM because ~20 call sites still toggle
       its display style — making those no-op visually via !important
       was cheaper than refactoring each one. */
    #talkLink {
      display: none !important;
      position: fixed;
      left: 0;
      right: 0;
      text-align: center;
      z-index: 11;
      /* bottom is set by positionTalkLink() */
    }
    #talkLink button {
      background: none;
      border: none;
      border-radius: 0;
      color: var(--pink);
      font-family: var(--font-serif);
      /* Italic demoted — Phase 4k. This is a link action, not a voice. */
      font-size: var(--t-body);
      font-weight: 400;
      padding: 7px 4px;
      cursor: pointer;
      text-decoration: underline;
      text-underline-offset: 5px;
      text-decoration-thickness: 1px;
      transition: opacity 0.2s, color 0.2s;
    }
    body.offline #talkLink button {
      opacity: 0.4;
      cursor: default;
      pointer-events: none;
    }
    body.solo-capped #talkLink button {
      opacity: 0.4;
      cursor: default;
      /* pointer-events stay enabled so tap shows the explainer toast */
    }
    #talkLink button:hover {
      color: var(--text);
    }

    /* ── Secondary nav ──────────────────────────────────────────── */
    /* Phase 4i: three plain-text slots (Topics | Timer | Menu).
       v130 (V11): pulled into the editorial system — bumped from
       --t-meta (13px) to --t-secondary (15px), added a hairline
       divider above the nav so the primary CTA reads as the hero,
       and added a 2px under-mark on whichever panel is open
       (.is-open class is toggled in openPanel/closePanel in app.js).
       Each slot is still a <button> for a11y and hit-target size.
       Min-height 44 keeps touch targets iOS-safe. */
    #secondaryNav {
      display: flex;
      justify-content: space-between;
      align-items: center;
      width: 100%;
      max-width: 420px;
      padding: 10px 4px 0;
      /* Hairline that separates the primary CTA from the secondary nav.
         Quieter than a solid var(--border) line — uses the same low-alpha
         white as .panel-drag-handle for a single edge vocabulary. */
      border-top: 1px solid color-mix(in srgb, var(--chip-tint) 8%, transparent);
    }

    .sec-nav-btn {
      flex: 1 1 0;
      min-height: 44px;
      padding: 10px 8px;
      border-radius: 0;
      border: none;
      background: transparent;
      color: var(--muted);
      font-family: var(--font-sans);
      font-size: var(--t-secondary);
      font-weight: 500;
      text-transform: none;
      cursor: pointer;
      transition: color 0.2s, opacity 0.2s;
      display: inline-flex;
      align-items: center;
      justify-content: center;
      position: relative;
    }
    .sec-nav-btn:first-child { justify-content: flex-start; }
    .sec-nav-btn:last-child  { justify-content: flex-end; }
    .sec-nav-btn:hover {
      color: var(--text);
    }
    .sec-nav-btn:active { opacity: 0.7; }
    /* Open-panel under-mark — 2px pink rule under the active slot.
       Wired by openPanel/closePanel in js/app.js, which toggles
       .is-open on the slot whose data-panel matches the open dialog. */
    .sec-nav-btn.is-open {
      color: var(--text);
    }
    .sec-nav-btn.is-open::after {
      content: '';
      position: absolute;
      left: 8px;
      right: 8px;
      bottom: 4px;
      height: 2px;
      background: var(--pink);
      border-radius: 1px;
    }
    .sec-nav-btn:first-child.is-open::after { left: 0; right: 16px; }
    .sec-nav-btn:last-child.is-open::after  { left: 16px; right: 0; }

    /* .sec-nav-label / .sec-nav-count styles removed with U5 — the
       "Topics · N" count was dropped from the nav (see index.html). */

    /* ── Secondary panels ───────────────────────────────────────── */
    .secondary-panel {
      position: fixed;
      inset: 0;
      z-index: 100;
      background: rgba(0,0,0,0.7);
      backdrop-filter: blur(6px);
      display: flex;
      align-items: flex-end;
      justify-content: center;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.25s;
    }
    .secondary-panel.open { opacity: 1; pointer-events: all; }

    .panel-sheet {
      position: relative;
      width: 100%;
      max-width: 560px;
      background: var(--surface);
      border-top: 1px solid var(--border);
      border-radius: 20px 20px 0 0;
      /* v130: padding tightened from 48px 24px 40px → 48/20/28. The
         48px top is doing real work (hosts drag handle + close); the
         side and bottom were out-breathing the card (which uses 22px).
         The card is the hero; panels shouldn't outshine its rhythm. */
      padding: 48px var(--sp-4) var(--sp-5);
      max-height: 85vh;
      overflow-y: auto;
      transform: translateY(30px);
      transition: transform 0.3s ease;
      border-top: 1px solid var(--border);
    }
    .secondary-panel.open .panel-sheet { transform: translateY(0); }

    /* Drag handle — visual affordance for the swipe-down-to-close gesture
       (see initPanelSwipe in js/swipe.js). Purely decorative; no pointer
       events of its own since the whole sheet is the touch target. */
    .panel-drag-handle {
      position: absolute;
      top: 10px;
      left: 50%;
      transform: translateX(-50%);
      width: 40px;
      height: 4px;
      border-radius: 2px;
      background: color-mix(in srgb, var(--chip-tint) 14%, transparent);
      pointer-events: none;
    }

    .panel-close {
      position: absolute;
      top: 14px;
      right: 16px;
      background: color-mix(in srgb, var(--chip-tint) 6%, transparent);
      border: 1px solid color-mix(in srgb, var(--chip-tint) 10%, transparent);
      border-radius: 50%;
      width: 32px;
      height: 32px;
      display: flex;
      align-items: center;
      justify-content: center;
      color: var(--muted);
      font-size: var(--t-body);
      line-height: 1;
      cursor: pointer;
      transition: background 0.2s, color 0.2s;
    }
    /* A6 — invisible hit-area expander. The visible circle stays 32×32
       for visual density; clicks/taps land on the surrounding 44×44
       square so the target meets WCAG 2.5.5 AAA (44×44 AAA, 24×24 AA).
       pointer-events:auto isn't needed — pseudo-elements accept events
       by default and the parent button is the actual click target. */
    .panel-close::before {
      content: '';
      position: absolute;
      inset: -6px;
    }
    .panel-close:hover { background: color-mix(in srgb, var(--chip-tint) 12%, transparent); color: var(--text); }

    .panel-title {
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: italic;
      font-size: var(--t-title);
      color: var(--text);
      margin-bottom: 20px;
      /* --tr-title retired in v130 — default tracking on Georgia italic
         28px is the right choice. */
    }

    .panel-description {
      font-family: var(--font-serif);
      font-size: var(--t-body);
      font-weight: 400;
      color: var(--text);
      line-height: 1.6;
      margin-bottom: 22px;
    }

    /* .panel-section-label removed in Phase 4k typography audit —
       its last caller (Topics panel header) was replaced by the live
       deck counter; the Timer panel now uses .panel-heading instead.
       If a third "big caps label" role ever appears, add it back with
       a clearer name ("panel-label-lg") and point font-size at
       var(--t-body). Until then, prefer .panel-heading (13px caps). */

    #topicSelectAll {
      background: none;
      border: none;
      color: var(--pink);
      font-family: var(--font-serif);
      /* Italic demoted — Phase 4k. Toggle action, not a voice. */
      font-size: var(--t-secondary);
      font-weight: 400;
      cursor: pointer;
      /* C6: was padding:0 — row was 75×17 and easy to misfire next to a
         topic checkbox. Pad vertically + horizontally to a 44h hit area
         and offset the negative inline margin so the underlined glyph
         keeps its right-aligned position. */
      padding: 14px 8px;
      margin: 0 -8px;
      text-decoration: underline;
      text-underline-offset: 3px;
      text-decoration-thickness: 1px;
      white-space: nowrap;
    }
    #topicSelectAll:hover { opacity: 0.8; }
    /* C6: confirm-once state for the destructive "Deselect all" action.
       First tap when N>0 swaps the label and arms the button; second
       tap commits. The arming is reverted automatically after a few
       seconds so a forgotten tap can't wipe the selection later. */
    #topicSelectAll.armed {
      color: var(--warning, #e8a87c);
      text-decoration-thickness: 2px;
    }

    /* Live deck counter row — reactive readout of how many questions
       match the current topic selection. Sits between the panel title
       and the pill grid so the reader sees the impact of each toggle. */
    .topics-counter-row {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 16px;
      margin-bottom: 16px;
    }

    /* Deck panel sections (Phase 5 mock) — Circle and Topics share one
       sheet. Each section carries a small caps heading and a serif note
       above its picker. A hairline rule between sections (via
       padding-top + border-top on non-first sections) signals they're
       siblings, not a single list. */
    .deck-section {
      margin-top: 24px;
    }
    .deck-section + .deck-section {
      padding-top: 24px;
      border-top: 1px solid var(--border);
    }
    .deck-section-heading {
      margin-bottom: 6px;
    }
    .deck-section-head-row {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 16px;
      margin-bottom: 6px;
    }
    .deck-section-head-row .deck-section-heading {
      margin-bottom: 0;
    }
    .deck-section-note {
      font-family: var(--font-serif);
      font-size: var(--t-body);
      font-weight: 400;
      color: var(--muted);
      line-height: 1.5;
      margin: 0 0 14px;
    }
    .deck-counter {
      display: flex;
      align-items: baseline;
      gap: 10px;
      min-width: 0; /* allow label to wrap on narrow screens */
    }
    .deck-counter-num {
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: normal;
      font-size: 32px;
      line-height: 1;
      color: var(--text);
      font-variant-numeric: tabular-nums;
      transition: color 0.25s ease;
    }
    /* Zero-state — no questions match the current filter. Muted color
       tips the user that drawing will hit the empty-pool path. */
    .deck-counter-num.is-empty {
      color: var(--muted);
    }
    .deck-counter-label {
      font-family: var(--font-sans);
      font-size: var(--t-caption);
      font-weight: 600;
      /* 11px caps — same editorial-stamp treatment as .card-topic, so
         reuse --tr-caps-lg (0.14em). Was 0.18em before Phase 4k. */
      letter-spacing: var(--tr-caps-lg);
      text-transform: uppercase;
      color: var(--muted);
    }

    /* ── Topic list ────────────────────────────────────────────
       Vertical list of topics — one row per topic, with a selection dot
       (filled pink when active, outlined when not) on the left and a
       serif label on the right. Each row is a <button> so Enter/Space
       toggles natively; role=checkbox + aria-checked keep screen
       readers in sync. Bottom borders separate rows; last row is
       flat so the stack doesn't have a dangling rule. */
    #topicPills {
      display: flex;
      flex-direction: column;
      gap: 0;
      /* Edge-to-edge inside the panel sheet — full-width rows read as
         a proper list, not a stack of buttons. */
      margin: 0 -4px;
    }
    .topic-item {
      display: flex;
      align-items: center;
      gap: 14px;
      width: 100%;
      padding: 14px 4px;
      background: none;
      border: none;
      border-bottom: 1px solid var(--border);
      text-align: left;
      cursor: pointer;
      user-select: none;
      -webkit-user-select: none;
      transition: opacity 0.15s;
    }
    .topic-item:last-child { border-bottom: none; }
    .topic-item:active { opacity: 0.6; }

    .topic-item-dot {
      flex-shrink: 0;
      width: 14px;
      height: 14px;
      border-radius: 50%;
      border: 1.5px solid var(--muted);
      background: transparent;
      transition: background 0.15s ease, border-color 0.15s ease;
      display: flex;
      align-items: center;
      justify-content: center;
    }
    .topic-item.active .topic-item-dot {
      background: var(--pink);
      border-color: var(--pink);
    }
    /* Non-color cue for selection state (WCAG 1.4.1). The check glyph
       is rendered inside the dot, visible only when the row is .active.
       Stroke uses the dark background color so it reads against the
       pink fill with strong contrast. */
    .topic-item-check {
      width: 10px;
      height: 10px;
      opacity: 0;
      transform: scale(0.6);
      transition: opacity 0.12s ease, transform 0.12s ease;
      stroke: var(--bg);
      stroke-width: 2.5;
      stroke-linecap: round;
      stroke-linejoin: round;
      fill: none;
      pointer-events: none;
    }
    .topic-item.active .topic-item-check {
      opacity: 1;
      transform: scale(1);
    }

    .topic-item-label {
      flex: 1 1 auto;
      min-width: 0;
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: normal;
      font-size: var(--t-lede);
      color: var(--muted);
      transition: color 0.15s ease;
    }
    .topic-item.active .topic-item-label {
      color: var(--text);
    }

    /* Right-aligned question total per topic. Tabular nums keep the
       column rigid as counts update; muted color keeps it secondary
       to the label. Inactive rows dim the count to match the label. */
    .topic-item-count {
      flex: 0 0 auto;
      font-family: var(--font-sans);
      font-size: var(--t-meta);
      font-weight: 500;
      font-variant-numeric: tabular-nums;
      color: var(--muted);
      opacity: 0.7;
      transition: opacity 0.15s ease;
    }
    .topic-item.active .topic-item-count {
      opacity: 1;
    }

    /* ── History list ──────────────────────────────────────────
       Read-only log in the History panel, rendered by renderHistory().
       Shares the topic-list vocabulary (edge-to-edge rows, hairline
       separators, serif body, uppercase muted meta) so it reads as the
       same family. All colors via tokens — flips correctly in light mode. */
    #historyList { margin: 0 -4px; }
    .history-day-group { margin-top: 18px; }
    .history-day-group:first-child { margin-top: 4px; }
    /* Day header — uppercase muted stamp, matching .panel-heading's
       editorial treatment but scoped here so the two can't drift. */
    .history-day {
      font-family: var(--font-sans);
      font-weight: 600;
      font-size: var(--t-meta);
      letter-spacing: var(--tr-caps-sm);
      text-transform: uppercase;
      color: var(--muted);
      padding: 0 4px 6px;
      border-bottom: 1px solid var(--border-strong);
      margin-bottom: 2px;
    }
    .history-row {
      padding: 14px 4px;
      border-bottom: 1px solid var(--border);
    }
    .history-row:last-child { border-bottom: none; }
    .history-row-meta {
      display: flex;
      align-items: baseline;
      justify-content: space-between;
      gap: 12px;
      margin-bottom: 4px;
    }
    .history-topic {
      flex: 1 1 auto;
      min-width: 0;
      font-family: var(--font-sans);
      font-weight: 600;
      font-size: var(--t-caption);
      letter-spacing: var(--tr-caps-lg);
      text-transform: uppercase;
      color: var(--pink);
    }
    .history-time {
      flex: 0 0 auto;
      font-family: var(--font-sans);
      font-size: var(--t-meta);
      font-weight: 500;
      font-variant-numeric: tabular-nums;
      color: var(--muted);
    }
    .history-question {
      font-family: var(--font-serif);
      font-size: var(--t-body);
      line-height: 1.5;
      color: var(--text);
    }

    /* ── Pack picker ────────────────────────────────────────────
       Multi-select rows in the Deck panel. Visual vocabulary mirrors
       .topic-item (dot+check on the left, serif label on the right) so
       both filter sheets read as one family. The extra .pack-item-desc
       caption underneath each label is a one-line plain-language hint
       since pack names aren't self-explanatory the way topic names are.
       The Inclusive deck row lives alone in #inclusivePackRow above
       #expansionPacks; both containers share these row styles. */
    #inclusivePackRow,
    #expansionPacks {
      display: flex;
      flex-direction: column;
      gap: 0;
      margin: 0 -4px;
    }
    .pack-item {
      display: flex;
      align-items: flex-start;
      gap: 14px;
      width: 100%;
      padding: 14px 4px;
      background: none;
      border: none;
      border-bottom: 1px solid var(--border);
      text-align: left;
      cursor: pointer;
      user-select: none;
      -webkit-user-select: none;
      transition: opacity 0.15s;
    }
    /* The Inclusive row sits alone in its container, and the Expansion
       Packs section opens with a caps heading + note — so the divider
       below the Inclusive row would create a hairline against the
       section break above. Drop it. The last expansion-pack row is
       likewise flat so the stack doesn't leave a dangling rule. */
    #inclusivePackRow .pack-item,
    #expansionPacks .pack-item:last-child { border-bottom: none; }
    .pack-item:active { opacity: 0.6; }
    /* Reuse the dot+check from topic items — keeps the one selection
       cue across both sheets. Bump the dot down a hair so it aligns
       with the first line of text in the two-line layout. The .active
       rules duplicate .topic-item.active because the dot/check selectors
       are scoped to .topic-item — pack-item rows don't pick those up
       just from being structurally similar. */
    .pack-item .topic-item-dot {
      margin-top: 4px;
    }
    .pack-item.active .topic-item-dot {
      background: var(--pink);
      border-color: var(--pink);
    }
    .pack-item.active .topic-item-check {
      opacity: 1;
      transform: scale(1);
    }
    .pack-item-text {
      flex: 1 1 auto;
      min-width: 0;
      display: flex;
      flex-direction: column;
      gap: 4px;
    }
    .pack-item-label {
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: normal;
      font-size: var(--t-lede);
      color: var(--muted);
      transition: color 0.15s ease;
    }
    .pack-item.active .pack-item-label {
      color: var(--text);
    }
    .pack-item-desc {
      font-family: var(--font-sans);
      font-size: var(--t-meta);
      font-weight: 400;
      line-height: 1.4;
      color: var(--muted);
      opacity: 0.85;
    }
    /* Inclusive deck section sits at the very top of the panel —
       no caps heading, no section note above it. Its own row carries
       a label + one-line description, which is enough. We tighten
       the top margin (the .deck-section default 24px is calibrated
       for a heading + note above each section) so the row sits
       comfortably below the deck counter / empty hint. */
    .deck-section--inclusive { margin-top: 14px; }
    .deck-section--inclusive + .deck-section {
      padding-top: 18px;
    }
    /* Empty-deck hint — surfaces "Pick at least one pack to fill the
       deck" beneath the counter when no packs are selected. Reserves
       no vertical space when hidden so the counter row keeps its
       rhythm; opacity transition keeps the appearance gentle. The
       dusty-rose accent matches the deck-counter zero-state cue
       without competing with the active-row pink. */
    .deck-empty-hint {
      font-family: var(--font-serif);
      font-style: italic;
      font-size: var(--t-meta);
      color: var(--pink);
      line-height: 1.5;
      max-height: 0;
      opacity: 0;
      overflow: hidden;
      transition: max-height 0.18s ease, opacity 0.18s ease, margin-top 0.18s ease;
      margin-top: 0;
    }
    .deck-empty-hint.is-visible {
      max-height: 60px;
      opacity: 1;
      margin-top: 6px;
    }

    /* ── Card meta row (topic + pack badge) ───────────────────
       Top band of the card. Replaces the lone .card-topic block; both
       the topic stamp and the optional pack badge live here as flex
       children so they share horizontal space and can't overlap on
       narrow phones. Both inherit the --t-meta caps treatment so the
       line reads as a single editorial header. The row is the .card-
       topic's old slot in the .quote-text flex column — same vertical
       placement, same min-height behavior. */
    /* ── Toggle label (shared by timer toggle) ────────────────── */
    .toggle-label {
      font-family: var(--font-sans);
      font-size: var(--t-body);
      font-weight: 500;
      transition: color 0.2s, opacity 0.2s;
      white-space: nowrap;
    }

    /* ── Main button ─────────────────────────────────────────── */
    /* Option A prototype (v133): filled dusty-rose pill → ghost pill.
       The swipe hint + intro state machine now do the teaching the
       filled CTA used to carry; the button's remaining job is an
       accessibility floor (keyboard, motor-impaired, desktop trackpad,
       iPad) plus empty-pool routing. A loud filled pill fights the
       card for attention and jars against the reflective tone. Ghost
       keeps the affordance, lowers the visual weight, and lets the
       card sit as the hero.

       Size and position unchanged so muscle memory, focus ring, and
       hit-target geometry all carry over. Contrast: pink text
       (#d4a0a8) on --bg (#1a1612) ≈ 5.6:1 — same pair as the in-card
       "Sit with this →" link, passes WCAG AA for text. The 1.5px
       border against --bg is ≈ 3:1 non-text contrast — meets 1.4.11.

       If we decide the ghost is too quiet in testing, the revert is a
       one-block CSS swap (see git v132 and earlier). Prefer that over
       partial compromises. */
    /* Row that holds the primary CTA and the (conditionally shown) End
       Game button. Caps at the same 380px the lone #mainBtn used to use,
       so pre-draw the single button is visually identical to before. */
    #mainBtnRow {
      display: flex;
      align-items: stretch;
      justify-content: center;
      gap: 10px;
      width: 100%;
      max-width: 380px;
    }

    #mainBtn {
      /* flex:1 so the button fills the row. When #endGameBtn is hidden
         (no game in progress) #mainBtn spans the full 380px exactly as
         the old width:100% rule did; when End Game appears, #mainBtn
         takes the remaining space and stays the wider of the two. */
      flex: 1 1 auto;
      min-width: 0;
      padding: 15.5px 24px;  /* -1.5px top/bottom to offset the new border so total height stays identical */
      border-radius: 999px;
      border: 1.5px solid var(--pink);
      background: transparent;
      color: var(--pink);
      font-family: var(--font-sans);
      font-weight: 600;
      font-size: var(--t-body);
      /* --tr-title retired in v130 — default tracking. */
      text-transform: none;
      cursor: pointer;
      transition: background 0.2s, color 0.2s, border-color 0.2s, opacity 0.2s, transform 0.15s;
    }
    /* Hover scoped to actual hover-capable pointers (mouse, trackpad).
       iOS Safari fires :hover on tap and keeps it active until the user
       taps somewhere else, which left "Draw another" filled pink after
       every draw. @media (hover: hover) and (pointer: fine) restricts
       the rule to devices that actually have hover semantics. Touch
       devices fall through to :active for tap feedback. v130 follow-up
       to V2 (the consent button got the same treatment by inheriting
       the same recipe). */
    @media (hover: hover) and (pointer: fine) {
      #mainBtn:hover {
        /* Pink-tinted fill + on-pink foreground (flips per mode). */
        background: var(--pink);
        color: var(--on-pink);
      }
    }
    #mainBtn:active { transform: translateY(1px); opacity: 0.9; }
    /* Disabled state keeps the ghost look — the button is only disabled
       during the ~400ms card slide-in, and a dim/flash in that window
       reads as a glitch rather than useful feedback. pointer-events
       alone is enough to swallow the click. */
    #mainBtn:disabled { pointer-events: none; transform: none; }

    /* ── End Game button ──────────────────────────────────────────
       Secondary action that lives beside #mainBtn once a game is in
       progress. Hidden until then, gated purely on body.game-started
       (set the moment Start Game is tapped / the user leaves the logo
       card, cleared by renderIntroLogoCard), so no JS toggles its
       visibility. Styled as a quieter, muted ghost
       pill — narrower than the primary CTA (flex:0 0 auto sizes it to its
       label) and lower-contrast so it reads as the lesser action.
       Palette via tokens only (no RGB literals) so it flips in light mode. */
    #endGameBtn { display: none; }
    body.game-started #endGameBtn {
      display: block;
      flex: 0 0 auto;
      padding: 15.5px 18px;
      border-radius: 999px;
      border: 1.5px solid color-mix(in srgb, var(--muted) 55%, transparent);
      background: transparent;
      color: var(--muted);
      font-family: var(--font-sans);
      font-weight: 600;
      font-size: var(--t-body);
      text-transform: none;
      cursor: pointer;
      transition: background 0.2s, color 0.2s, border-color 0.2s, opacity 0.2s, transform 0.15s;
    }
    @media (hover: hover) and (pointer: fine) {
      body.game-started #endGameBtn:hover {
        border-color: var(--muted);
        color: var(--text);
      }
    }
    body.game-started #endGameBtn:active { transform: translateY(1px); opacity: 0.9; }

    /* ── Retry button ────────────────────────────────────────── */
    .retry-btn {
      margin-top: 12px;
      background: none;
      border: 1px solid var(--border);
      border-radius: 8px;
      color: var(--muted);
      font-family: var(--font-sans);
      font-size: var(--t-body);
      padding: 8px 20px;
      cursor: pointer;
      transition: border-color 0.2s, color 0.2s;
    }
    .retry-btn:hover { border-color: var(--pink); color: var(--text); }


    /* ── AI Consent dialog ───────────────────────────────────── */
    #aiConsentOverlay {
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,0.7);
      z-index: 300;
      display: flex;
      align-items: flex-end;
      justify-content: center;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.25s;
    }
    #aiConsentOverlay.open {
      opacity: 1;
      pointer-events: all;
    }
    #aiConsentDialog {
      background: var(--surface);
      border-radius: 20px 20px 0 0;
      border-top: 1px solid var(--border);
      width: 100%;
      max-width: 540px;
      padding: 28px 24px 36px;
      transform: translateY(100%);
      transition: transform 0.3s cubic-bezier(0.32,0.72,0,1);
    }
    #aiConsentOverlay.open #aiConsentDialog {
      transform: translateY(0);
    }
    .consent-title {
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: italic;
      font-size: var(--t-heading);
      color: var(--text);
      margin-bottom: 14px;
      /* --tr-title retired in v130 — default tracking. */
    }
    .consent-body {
      font-family: var(--font-serif);
      font-size: var(--t-body);
      color: var(--text);
      /* v130 V24 — body line-height standardized at 1.6 (was 1.65).
         Matches .panel-body / .panel-description / .panel-prose so
         every long-form serif block in the app uses the same vertical
         rhythm. */
      line-height: 1.6;
      margin-bottom: 10px;
    }
    .consent-privacy-link {
      font-size: var(--t-body);
      color: var(--pink);
      margin-bottom: 24px;
      cursor: pointer;
      text-decoration: underline;
      text-underline-offset: 2px;
    }
    .privacy-consent-toggle {
      display: flex;
      align-items: flex-start;
      gap: 10px;
      margin-top: 14px;
      cursor: pointer;
      font-size: var(--t-body);
      color: var(--text-muted);
      line-height: 1.4;
    }
    .privacy-consent-toggle input[type="checkbox"] {
      flex-shrink: 0;
      margin-top: 2px;
      width: 16px;
      height: 16px;
      accent-color: var(--pink);
      cursor: pointer;
    }
    .consent-actions {
      display: flex;
      gap: 10px;
    }
    /* v130 V2 — restyled from filled-pink uppercase to outlined-pink
       mixed-case to match the #mainBtn recipe. The consent dialog is
       a meaningful but reversible commitment ("I understand"); a
       loud filled pill was overstating it relative to the calm tone
       of the rest of the app, and uppercase made it shout next to
       the serif italic title. Now reads as the dialog's primary
       action without screaming. Border + transition kept identical
       to #mainBtn for visual rhyme. */
    #consentAcceptBtn {
      flex: 1;
      padding: 13px 14px;  /* -1px to offset the 1.5px border (vs 1px before) */
      border-radius: 999px;
      border: 1.5px solid var(--pink);
      background: transparent;
      color: var(--pink);
      font-family: var(--font-sans);
      font-weight: 600;
      font-size: var(--t-body);
      text-transform: none;
      cursor: pointer;
      transition: background 0.2s, color 0.2s, border-color 0.2s, opacity 0.2s, transform 0.15s;
    }
    /* Same hover scoping as #mainBtn — see the explanation above its
       @media (hover: hover) block. iOS Safari otherwise leaves this
       button filled-pink after the user taps "I understand". */
    @media (hover: hover) and (pointer: fine) {
      #consentAcceptBtn:hover {
        background: var(--pink);
        color: var(--on-pink);
      }
    }
    #consentAcceptBtn:active { transform: translateY(1px); opacity: 0.9; }
    /* Shared secondary "decline / cancel" button — outlined, muted, caps.
       Used by #consentDeclineBtn and #clearDataCancelBtn. v130 V22 pulled
       both ID rules into one class so future tweaks happen in one place
       and the pair stay visually identical by construction. */
    .btn-secondary-muted {
      flex: 1;
      padding: 14px;
      border-radius: 999px;
      border: 1px solid var(--border-strong);
      background: transparent;
      color: var(--muted);
      font-family: var(--font-sans);
      font-weight: 500;
      font-size: var(--t-body);
      /* V3: was caps + tracking — paired with sentence-case primaries
         ("I understand", "Clear data") it read as accidental. The hierarchy
         that mattered (decline vs commit) was already carried by color
         (--muted vs --pink/--red). Sentence case throughout keeps the
         dialog row in one voice. */
      cursor: pointer;
      transition: border-color 0.2s, color 0.2s;
    }
    .btn-secondary-muted:hover { border-color: var(--pink); color: var(--text); }

    .modal-title {
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: italic;
      font-size: var(--t-heading);
      color: var(--text);
      margin-bottom: 12px;
    }
    .privacy-website-link {
      font-family: var(--font-serif);
      font-size: var(--t-body);
      margin: 0 0 22px;
    }
    .privacy-website-link a {
      color: var(--pink);
      text-decoration: underline;
      text-underline-offset: 3px;
    }
    .privacy-section {
      margin-bottom: 26px;
    }
    /* .privacy-heading and .privacy-body were consolidated into
       .panel-heading / .panel-body in the Phase 4k audit — they
       were identical recipes to begin with. Kept as a reference in
       case the Privacy panel needs a divergent style in future. */
    /* Welcome overlay. Fades in/out around a shared display:flex layout —
       the old display:none → display:flex toggle made a fade-out transition
       impossible (you can't transition off display:none). The overlay stays
       in the flex box at all times; opacity + pointer-events + visibility
       are the three properties that gate whether it reads as present. */
    #firstVisitOverlay {
      position: fixed;
      inset: 0;
      background: var(--bg);
      z-index: 350;
      display: flex;
      align-items: center;
      justify-content: center;
      padding: 40px 28px;
      opacity: 0;
      visibility: hidden;
      pointer-events: none;
      transition: opacity 0.5s ease, visibility 0s linear 0.5s;
    }
    #firstVisitOverlay.open {
      opacity: 1;
      visibility: visible;
      pointer-events: auto;
      transition: opacity 0.5s ease, visibility 0s linear 0s;
    }
    /* When transitioning out, use a longer easing so the fade feels
       intentional rather than flash-dismissed. The .closing class is
       applied just before .open is removed in JS. */
    #firstVisitOverlay.closing {
      opacity: 0;
      transition: opacity 0.8s ease, visibility 0s linear 0.8s;
    }
    #firstVisitContent {
      display: flex;
      flex-direction: column;
      align-items: center;
      gap: 28px;
      max-width: 480px;
    }
    /* Welcome heart — dusty rose stroke, traced by hand (asymmetric weight).
       Chosen over the earlier em-dash so the welcome has a brand mark that
       echoes the app's topic. Stroke-only (fill:none) reads as a mark, not a
       sticker. Sized small enough that it sits quietly above the headline. */
    .welcome-heart {
      opacity: 0.9;
      margin-bottom: 4px;
    }
    .welcome-fan {
      /* v130 V15 — was a fixed 180×135 attribute pair on the SVG,
         which left the fan looking small on phablets and chunky on
         narrow phones. Now scales with viewport width so it always
         reads as a real card spread. The 4:3 aspect ratio matches
         the viewBox so the cards never distort. */
      width: clamp(200px, 56vw, 320px);
      height: auto;
      aspect-ratio: 4 / 3;
      opacity: 0.92;
      margin-bottom: 12px;
      filter: drop-shadow(0 4px 10px rgba(0, 0, 0, 0.35));
    }
    #firstVisitMsg, #firstVisitMsg2, #firstVisitMsg3 {
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: italic;
      font-size: clamp(22px, 5.5vw, 34px);
      line-height: 1.4;
      text-align: center;
      margin: 0;
      color: var(--text);
    }
    /* #firstVisitBtn was removed in 2026-04 — the welcome overlay now
       auto-advances into the intro-logo card after ~4s. See showWelcome()
       in js/app.js. */
    #clearDataOverlay {
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,0.7);
      z-index: 300;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.25s;
      display: flex;
      align-items: flex-end;
      justify-content: center;
    }
    #clearDataOverlay.open {
      opacity: 1;
      pointer-events: all;
    }
    #clearDataDialog {
      background: var(--surface);
      border-radius: 20px 20px 0 0;
      border-top: 1px solid var(--border);
      width: 100%;
      max-width: 540px;
      padding: 28px 24px 36px;
      transform: translateY(100%);
      transition: transform 0.3s cubic-bezier(0.32,0.72,0,1);
    }
    #clearDataOverlay.open #clearDataDialog {
      transform: translateY(0);
    }
    /* Outlined-pink mixed-case to match #consentAcceptBtn (the
       "I understand" on the AI consent dialog) and #mainBtn. The
       previous filled-pink uppercase shouted next to the serif
       italic title and was the only solid-pink dialog button left
       after the v130 V2 restyle. Border + transition kept identical
       so the button reads as part of the same family. */
    #clearDataOkBtn {
      flex: 1;
      padding: 13px 14px;  /* -1px to offset the 1.5px border (vs 1px before) */
      border-radius: 999px;
      border: 1.5px solid var(--pink);
      background: transparent;
      color: var(--pink);
      font-family: var(--font-sans);
      font-weight: 600;
      font-size: var(--t-body);
      text-transform: none;
      cursor: pointer;
      width: 100%;
      transition: background 0.2s, color 0.2s, border-color 0.2s, opacity 0.2s, transform 0.15s;
    }
    /* Same hover scoping as #mainBtn / #consentAcceptBtn — iOS Safari
       otherwise leaves this button filled-pink after the user taps OK
       and the dialog dismisses, because the :hover state sticks until
       the next interaction. */
    @media (hover: hover) and (pointer: fine) {
      #clearDataOkBtn:hover { background: var(--pink); color: var(--on-pink); }
    }
    #clearDataOkBtn:active { transform: translateY(1px); opacity: 0.9; }
    /* Ghost-pink "open the destructive flow" trigger, embedded in the
       privacy section body. Per the button-family decision tree this is
       "a quiet companion action that lives near content" — the section
       is mostly explanatory prose, and this button gives the reader a
       way to act without becoming the section's hero. The actual
       destructive commit lives behind a confirm dialog
       (#clearDataConfirmBtn.destructive); this entry point should not
       shout. Was an outlined caps muted-gray pill, which read as
       cancel/dismiss family. Now: pink underlined inline, sentence
       case (text-transform dropped — the HTML label still has its
       title-case-y "Local Data" but no longer SHOUTS). */
    .clear-local-data-btn {
      margin-top: 12px;
      padding: 10px 4px;
      background: transparent;
      border: none;
      border-radius: 0;
      color: var(--pink);
      font-family: var(--font-sans);
      font-size: var(--t-secondary);
      font-weight: 500;
      text-transform: none;
      letter-spacing: normal;
      text-decoration: underline;
      text-underline-offset: 3px;
      text-decoration-thickness: 1px;
      cursor: pointer;
      transition: color 0.2s, opacity 0.2s;
    }
    .clear-local-data-btn:hover { color: var(--text); }
    .clear-local-data-btn:active { opacity: 0.6; }

    /* Pre-confirm modal for destructive wipe (U2).
       Shares visual structure with #clearDataOverlay but adds a bulleted
       list of what's about to be cleared and a destructive-styled action.
       Z-index 301 so it sits above the privacy panel (z-index 100 for
       panels) and cooperates with the post-wipe confirmation at z-index 300. */
    #clearDataConfirmOverlay {
      position: fixed;
      inset: 0;
      background: rgba(0,0,0,0.7);
      z-index: 301;
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.25s;
      display: flex;
      align-items: flex-end;
      justify-content: center;
    }
    #clearDataConfirmOverlay.open {
      opacity: 1;
      pointer-events: all;
    }
    #clearDataConfirmDialog {
      background: var(--surface);
      border-radius: 20px 20px 0 0;
      border-top: 1px solid var(--border);
      width: 100%;
      max-width: 540px;
      padding: 28px 24px 36px;
      transform: translateY(100%);
      transition: transform 0.3s cubic-bezier(0.32,0.72,0,1);
    }
    #clearDataConfirmOverlay.open #clearDataConfirmDialog {
      transform: translateY(0);
    }
    .clear-data-list {
      margin: 6px 0 14px;
      padding-left: 20px;
      list-style: disc;
      font-family: var(--font-serif);
      font-size: var(--t-secondary);
      color: var(--text);
      line-height: 1.55;
    }
    .clear-data-list li { margin: 4px 0; }

    /* Destructive action style — matches the pill geometry of
       #consentAcceptBtn but swaps the pink fill for a warning color so
       it reads as consequential. Keeps dark text on the fill for WCAG
       contrast with the warning hue. */
    /* #clearDataCancelBtn now uses the shared .btn-secondary-muted class
       (see above). v130 V22 consolidated the previously duplicated ID rule. */
    #clearDataConfirmBtn.destructive {
      flex: 1;
      padding: 14px;
      border-radius: 999px;
      border: 1px solid #c85a4a;
      background: #c85a4a;
      color: #1a1612;
      font-family: var(--font-sans);
      font-weight: 600;
      font-size: var(--t-body);
      /* V3: matches .btn-secondary-muted — sentence case + body size so the
         destructive label reads in the same voice as its Cancel sibling.
         The "this is the dangerous one" signal stays in the red fill. */
      cursor: pointer;
      transition: opacity 0.2s, background 0.2s;
    }
    #clearDataConfirmBtn.destructive:hover { opacity: 0.88; }
    #clearDataConfirmBtn.destructive:focus-visible {
      outline: 2px solid var(--pink);
      outline-offset: 2px;
    }

    .topics-note {
      font-family: var(--font-serif);
      font-size: var(--t-secondary);
      color: var(--muted);
      margin: 22px 0 0;
      line-height: 1.55;
    }

    /* Explainer now lives BELOW the pills as a quiet footnote — the live
       counter at the top carries the primary "what's in the deck" signal. */
    .topics-explainer {
      margin-top: 22px;
      text-align: center;
    }


    /* ── Shared editorial panel sections ─────────────────────────
       Canonical pattern used by the Help panel (phase 4c): a caps
       sans-serif heading over a serif body paragraph. Mirrors the
       .privacy-heading / .privacy-body pair but with a generic name
       so other panels can adopt it without semantic drift. */
    .panel-section {
      margin-bottom: 22px;
    }
    .panel-heading {
      font-family: var(--font-sans);
      font-weight: 500;
      font-size: var(--t-meta);
      color: var(--muted);
      margin-bottom: 6px;
      letter-spacing: var(--tr-caps-sm);
      text-transform: uppercase;
    }
    .panel-body {
      font-family: var(--font-serif);
      font-size: var(--t-body);
      font-weight: 400;
      color: var(--text);
      /* v130 V24 — body line-height standardized at 1.6 (was 1.65). */
      line-height: 1.6;
      margin: 0;
    }
    /* Inline emphasis — semibold, same color. Kept subtle so the caps
       headings remain the primary hierarchy signal. */
    .panel-body strong {
      font-weight: 600;
      color: var(--text);
    }

    /* Generic serif prose block for About-style panels. Consolidated from
       the legacy .htp-body recipe in the Phase 4k audit — same typography
       (serif 17px), renamed so it reads as a canonical panel token rather
       than a Help-panel leftover. Unused .htp-step / .htp-icon rules were
       dropped at the same time. v130 V24 dropped line-height from 1.7 to
       1.6 to match the rest of the long-form serif blocks. */
    .panel-prose {
      font-family: var(--font-serif);
      font-size: var(--t-body);
      color: var(--text);
      line-height: 1.6;
    }
    .panel-prose p { margin: 0 0 18px 0; }
    .panel-prose p:last-child { margin-bottom: 0; }
    .panel-prose strong { font-weight: 600; color: var(--text); }

    /* Contact groups (About panel): a short serif lead, a pink link, and
       an optional meta note. Each group stacks tight; groups are separated
       by their own margin-bottom so the "Send feedback" and "Visit us"
       clusters read as sibling units. */
    .panel-contact { margin-bottom: 16px; }
    .panel-contact:last-child { margin-bottom: 0; }
    .panel-contact-lead {
      font-family: var(--font-serif);
      font-size: var(--t-body);
      color: var(--text);
      line-height: 1.5;
      margin: 0 0 6px 0;
    }
    .panel-contact-note {
      font-family: var(--font-serif);
      font-size: var(--t-meta);
      color: var(--muted);
      line-height: 1.5;
      margin: 4px 0 0 0;
    }

    /* Canonical inline-panel link. Replaces 22-line inline style blobs on
       the About panel's feedback/website anchors. Uses the pink accent
       with a 3px underline offset to match the brand tone. */
    .panel-link {
      color: var(--pink);
      text-decoration: underline;
      text-underline-offset: 3px;
      font-family: var(--font-serif);
      font-size: var(--t-body);
      font-weight: 400;
    }
    .htp-divider {
      height: 1px;
      background: color-mix(in srgb, var(--chip-tint) 12%, transparent);
      margin: 24px 0;
    }

    /* Help footer strip (Phase 5 mock) — replaces the prior Menu sheet.
       Three caps-stamp links at the bottom of the Help panel for the
       lower-traffic items (Privacy / About / Feedback). The same caps
       treatment as .card-topic to read as editorial signage; the dot
       separators carry their own muted color so the links pop without
       fighting each other. */
    /* Appearance row — Light/Dark/Auto segmented control. Sits above the
       help-footer-strip and shares its top-rule treatment so it reads as
       a paired settings/contact strip rather than a stand-alone control.
       The pills use the same caps-stamp typography as the footer links so
       they feel like one editorial cluster. Selected state is filled-pink
       with --on-pink foreground (mirrors #mainBtn:hover and selected
       topic pill — the "this is the active choice" treatment is consistent
       across the app). */
    .appearance-row {
      margin: 28px -4px 0;
      padding: 18px 0 4px;
      border-top: 1px solid var(--border);
      display: flex;
      align-items: center;
      justify-content: space-between;
      gap: 12px;
    }
    .appearance-label {
      font-family: var(--font-sans);
      font-size: var(--t-meta);
      font-weight: 600;
      letter-spacing: var(--tr-caps-lg);
      text-transform: uppercase;
      color: var(--muted);
    }
    .appearance-pills {
      display: inline-flex;
      gap: 6px;
    }
    .appearance-pill {
      background: none;
      border: 1px solid var(--border);
      color: var(--muted);
      cursor: pointer;
      padding: 8px 14px;
      min-height: 36px;
      border-radius: 999px;
      font-family: var(--font-sans);
      font-size: var(--t-meta);
      font-weight: 600;
      letter-spacing: var(--tr-caps-sm);
      text-transform: uppercase;
      transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
    }
    .appearance-pill[aria-checked="true"] {
      background: var(--pink);
      color: var(--on-pink);
      border-color: var(--pink);
    }
    .appearance-pill:not([aria-checked="true"]):hover {
      color: var(--text);
      border-color: var(--border-strong);
    }
    .appearance-pill:active {
      transform: translateY(1px);
    }
    /* Footer strip drops its top border when it sits below the appearance
       row — the appearance row's border-top already does that work. */
    .appearance-row + .help-footer-strip {
      margin-top: 0;
      border-top: none;
      padding-top: 4px;
    }

    .help-footer-strip {
      margin: 28px -4px 0;
      padding: 18px 0 4px;
      border-top: 1px solid var(--border);
      display: flex;
      align-items: center;
      justify-content: center;
      gap: 4px;
    }
    .help-footer-link {
      background: none;
      border: none;
      color: var(--pink);
      cursor: pointer;
      padding: 10px 12px;
      min-height: 44px;
      font-family: var(--font-sans);
      font-size: var(--t-meta);
      font-weight: 600;
      letter-spacing: var(--tr-caps-lg);
      text-transform: uppercase;
      transition: color 0.15s ease;
    }
    .help-footer-link:hover {
      color: var(--text);
    }
    .help-footer-link:active {
      opacity: 0.7;
    }
    .help-footer-sep {
      color: var(--muted);
      opacity: 0.4;
      font-size: var(--t-meta);
      user-select: none;
      -webkit-user-select: none;
    }
    /* Beta M2: dead-end swipe feedback. Animates `translate` (not
       `transform`) so it composes with the card's inline transform from the
       slide pipeline instead of clobbering it. */
    .quote-text.bump {
      animation: card-bump 0.3s ease;
    }
    @keyframes card-bump {
      0%, 100% { translate: 0; }
      35%      { translate: 16px 0; }
    }
    @media (prefers-reduced-motion: reduce) {
      .quote-text.bump { animation: none; }
    }

    .htp-pwa {
      background: var(--surface-elevated);
      border: 1px solid var(--border-strong);
      border-radius: 12px;
      padding: 18px;
      margin-bottom: 18px;
    }
    /* Native (Capacitor) build: Safari "Add to Home Screen" instructions
       are web-only chrome — hide them inside the App Store app.
       body.native-app is set at boot in app.js. */
    .native-app .htp-pwa {
      display: none;
    }
    .htp-pwa-title {
      font-family: var(--font-sans);
      font-weight: 500;
      font-size: var(--t-meta);
      color: var(--muted);
      letter-spacing: var(--tr-caps-sm);
      text-transform: uppercase;
      margin-bottom: 12px;
    }
    .htp-pwa-steps {
      display: flex;
      flex-direction: column;
      gap: 8px;
    }
    .htp-pwa-step {
      display: flex;
      gap: 10px;
      align-items: flex-start;
      font-size: var(--t-body);
      color: var(--text);
      line-height: 1.5;
    }
    .htp-pwa-num {
      flex-shrink: 0;
      width: 26px;
      height: 26px;
      border-radius: 50%;
      background: color-mix(in srgb, var(--pink) 25%, transparent);
      border: 1px solid color-mix(in srgb, var(--pink) 50%, transparent);
      color: var(--pink);
      font-size: var(--t-body);
      font-weight: 600;
      display: flex;
      align-items: center;
      justify-content: center;
      margin-top: 1px;
    }
    .htp-pwa-icon {
      display: inline-block;
      background: color-mix(in srgb, var(--chip-tint) 10%, transparent);
      border: 1px solid color-mix(in srgb, var(--chip-tint) 15%, transparent);
      border-radius: 4px;
      padding: 1px 5px;
      font-size: var(--t-body);
      vertical-align: middle;
      margin: 0 2px;
    }


    /* ── Chat screen ─────────────────────────────────────────── */
    #chatOverlay {
      position: fixed;
      inset: 0;
      z-index: 200;
      background: rgba(0,0,0,0.5);
      opacity: 0;
      pointer-events: none;
      transition: opacity 0.3s;
    }
    #chatOverlay.open { opacity: 1; pointer-events: all; }

    #chatScreen {
      position: fixed;
      inset: 0;
      z-index: 201;
      background: var(--bg);
      display: flex;
      flex-direction: column;
      transform: translateY(100%);
      transition: transform 0.35s cubic-bezier(0.32, 0.72, 0, 1);
      will-change: transform;
    }
    #chatScreen.open { transform: translate3d(0, 0, 0); }

    body.chat-open #quoteArea,
    body.chat-open #appHeader,
    body.chat-open #headerLine,
    body.chat-open #bottomControls,
    body.chat-open #talkLink,
    body.chat-open #swipeHintBar { visibility: hidden; }

    #chatHeader {
      display: flex;
      align-items: flex-start;
      gap: 14px;
      padding: calc(18px + env(safe-area-inset-top)) 18px 14px;
      border-bottom: 1px solid var(--border);
      flex-shrink: 0;
    }

    #chatCloseBtn {
      background: none;
      border: none;
      color: var(--muted);
      font-size: var(--t-heading);
      cursor: pointer;
      padding: 0;
      line-height: 1;
      flex-shrink: 0;
      margin-top: 2px;
      transition: color 0.2s;
      /* C4: 44×44 minimum hit area (WCAG 2.5.5). The visible glyph stays
         the same size; the inline-flex centering keeps the X visually
         where it was while the parent box meets the touch-target floor. */
      min-width: 44px;
      min-height: 44px;
      display: inline-flex;
      align-items: center;
      justify-content: center;
    }
    #chatCloseBtn:hover { color: var(--text); }

    #chatHeaderQ {
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: italic;
      font-size: var(--t-lede);
      color: var(--text);
      line-height: 1.35;
      flex: 1;
    }

    #chatMessages {
      flex: 1;
      overflow-y: auto;
      padding: 20px 16px;
      display: flex;
      flex-direction: column;
      gap: 12px;
      -webkit-overflow-scrolling: touch;
    }

    /* Chat bubble base — applied to both AI (.ep) and user (.user). The two
       variants diverge heavily below: AI turns are serif-no-bubble (the voice
       of a thoughtful friend writing to you), user turns are sans-bubble
       (what you said, enclosed in a quote). */
    .chat-bubble {
      max-width: 88%;
      line-height: 1.55;
      white-space: pre-line;
    }
    /* AI turn: serif italic, no background, no border. Reads as prose, not chat. */
    .chat-bubble.ep {
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: italic;
      font-size: 18px;
      line-height: 1.6;
      color: var(--text);
      align-self: stretch;
      padding: 4px 2px;
      max-width: 100%;
    }
    /* Opener question — a shade of pink, not bold, so it reads as a
       pulled quote rather than a shout. */
    .chat-bubble.ep strong {
      font-weight: 400;
      color: var(--pink);
      font-style: italic;
    }
    /* User turn: sans, rounded pill, dusty-rose tint so it reads as "you said this". */
    .chat-bubble.user {
      font-family: var(--font-sans);
      font-weight: 400;
      font-size: 16px;
      padding: 10px 16px;
      border-radius: 18px;
      background: var(--surface-elevated);
      border: 1px solid var(--border);
      color: var(--text);
      align-self: flex-end;
      border-bottom-right-radius: 4px;
      max-width: 80%;
    }

    /* Typing indicator */
    .chat-bubble.typing {
      display: inline-flex;
      gap: 5px;
      align-items: center;
      padding: 14px 2px;
      font-style: normal;
    }
    .chat-bubble.typing span {
      width: 6px; height: 6px;
      border-radius: 50%;
      background: var(--pink);
      animation: dotPulse 1.2s ease-in-out infinite;
    }
    .chat-bubble.typing span:nth-child(2) { animation-delay: 0.2s; }
    .chat-bubble.typing span:nth-child(3) { animation-delay: 0.4s; }
    @keyframes dotPulse {
      0%, 80%, 100% { opacity: 0.2; transform: scale(0.8); }
      40% { opacity: 1; transform: scale(1); }
    }

    #chatInputArea {
      display: flex;
      gap: 10px;
      align-items: flex-end;
      padding: 12px 14px max(16px, env(safe-area-inset-bottom));
      border-top: 1px solid var(--border);
      flex-shrink: 0;
      background: var(--bg);
    }

    #chatInput {
      flex: 1;
      background: color-mix(in srgb, var(--chip-tint) 5%, transparent);
      border: 1px solid var(--border);
      border-radius: 14px;
      color: var(--text);
      font-family: var(--font-sans);
      font-size: var(--t-body);
      font-weight: 300;
      padding: 11px 14px;
      resize: none;
      min-height: 44px;
      max-height: 120px;
      overflow-y: auto;
      line-height: 1.4;
      transition: border-color 0.2s;
    }
    /* A11y fix (A3) — only suppress the native outline for non-keyboard
       focus. Keyboard users fall through to the global :focus-visible
       rule (css/app.css ~345) and see the pink ring. Border-color
       darkening still runs on any focus so mouse/touch users get a
       subtle affordance too. */
    #chatInput:focus:not(:focus-visible) {
      outline: none;
    }
    #chatInput:focus {
      border-color: color-mix(in srgb, var(--pink) 50%, transparent);
    }
    #chatInput::placeholder { color: var(--muted); }
    #chatInput:disabled { opacity: 0.5; }

    #chatSend {
      width: 44px; height: 44px;
      border-radius: 50%;
      border: none;
      background: var(--pink);
      color: var(--on-pink);
      cursor: pointer;
      display: flex;
      align-items: center;
      justify-content: center;
      flex-shrink: 0;
      font-size: 18px;
      transition: opacity 0.2s, transform 0.15s;
    }
    #chatSend:disabled { opacity: 0.4; cursor: not-allowed; }
    #chatSend:hover:not(:disabled) { opacity: 0.85; transform: scale(1.05); }

    /* ── Menu bottom sheet (phase 4i) ───────────────────────────
       The top-right hamburger dropdown is gone — #menuPanel is now a
       standard .secondary-panel, inheriting sheet/close/drag-handle
       styling from that system. The only thing that lives here is the
       list of items inside it. */
    #menuPanelList {
      display: flex;
      flex-direction: column;
      gap: 2px;
      margin-top: 4px;
    }
    #menuPanelList .menu-item {
      display: block;
      width: 100%;
      padding: 16px 4px;
      background: none;
      border: none;
      border-bottom: 1px solid var(--border);
      color: var(--text);
      font-family: var(--font-serif);
      font-size: var(--t-lede);
      font-weight: 400;
      text-align: left;
      cursor: pointer;
      transition: opacity 0.15s, color 0.15s;
      white-space: nowrap;
      box-sizing: border-box;
    }
    #menuPanelList .menu-item:last-child { border-bottom: none; }
    #menuPanelList .menu-item:hover,
    #menuPanelList .menu-item:active {
      color: var(--pink);
    }

    /* ── Timers panel ────────────────────────────────────────────── */
    #timerToggle {
      display: flex;
      align-items: center;
      gap: 12px;
      cursor: pointer;
      user-select: none;
      -webkit-user-select: none;
    }
    #timerTrack {
      width: 44px;
      height: 26px;
      border-radius: 13px;
      background: color-mix(in srgb, var(--pink) 15%, transparent);
      border: 1.5px solid color-mix(in srgb, var(--pink) 40%, transparent);
      position: relative;
      flex-shrink: 0;
      transition: background 0.2s, border-color 0.2s;
    }
    #timerThumb {
      width: 18px;
      height: 18px;
      border-radius: 50%;
      background: color-mix(in srgb, var(--pink) 50%, transparent);
      position: absolute;
      top: 3px;
      left: 3px;
      transition: transform 0.2s, background 0.2s;
    }
    #timerToggle.active #timerTrack {
      background: color-mix(in srgb, var(--pink) 35%, transparent);
      border-color: color-mix(in srgb, var(--pink) 90%, transparent);
    }
    #timerToggle.active #timerThumb {
      transform: translateX(18px);
      background: var(--pink);
    }
    #timerToggle .toggle-label { transition: color 0.2s, opacity 0.2s; }
    #timerToggle.active #timerLabelOff  { color: var(--muted); opacity: 0.5; }
    #timerToggle.active #timerLabelOn   { color: var(--text); font-weight: 500; }
    #timerToggle:not(.active) #timerLabelOff { color: var(--text); font-weight: 500; }
    #timerToggle:not(.active) #timerLabelOn  { color: var(--muted); opacity: 0.5; }

    /* Phase 4j: options sit inside a .panel-section now, which carries its
       own heading + rhythm. Less top margin is needed here because the
       section heading ("Minutes per question") supplies the visual gap. */
    #timerOptions {
      display: none;
      margin-top: 28px;
    }
    #timerOptions.visible { display: block; }

    /* Flex row for the 3/5/10 preset pills. Used to be an inline style on
       the wrapper div; pulled into a class so the preset row can pick up
       consistent gap / wrap behavior and the HTML stays free of style
       attributes. */
    .timer-pills-row {
      display: flex;
      flex-wrap: wrap;
      gap: 10px;
    }

    .timer-pill {
      display: inline-flex;
      align-items: center;
      padding: 10px 18px;
      border-radius: 999px;
      border: 1px solid color-mix(in srgb, var(--pink) 50%, transparent);
      background: transparent;
      color: #ccc0e0;
      font-family: var(--font-sans);
      font-size: var(--t-body);
      font-weight: 400;
      cursor: pointer;
      user-select: none;
      -webkit-user-select: none;
      transition: background 0.15s, border-color 0.15s, color 0.15s;
    }
    .timer-pill.active {
      background: color-mix(in srgb, var(--pink) 35%, transparent);
      border-color: color-mix(in srgb, var(--pink) 100%, transparent);
      color: #ffffff;
    }

    /* Custom-minutes input. Editorial tune: serif placeholder, softer
       border, no filled background — reads as a quiet second option
       below the presets, not a primary form field. */
    #timerCustomInput {
      width: 100%;
      background: transparent;
      border: none;
      border-bottom: 1px solid var(--border);
      border-radius: 0;
      color: var(--text);
      font-family: var(--font-serif);
      font-size: var(--t-body);
      font-weight: 400;
      padding: 8px 2px;
      margin-top: 16px;
      box-sizing: border-box;
    }
    #timerCustomInput::placeholder {
      color: var(--muted);
      /* Italic demoted — Phase 4k. Placeholder is already differentiated
         by color; italic was redundant and conflicted with the reserved
         italic roles (panel-title, times-up, first-visit, AI voice). */
    }
    /* A11y fix (A3) — same pattern as #chatInput. Pink underline still
       fires on any focus; the native outline is only suppressed when
       focus is NOT keyboard-driven. */
    #timerCustomInput:focus:not(:focus-visible) {
      outline: none;
    }
    #timerCustomInput:focus {
      border-bottom-color: var(--pink);
    }

    /* Secondary instruction that appears only when the timer is on.
       Muted serif so it reads as a footnote to the control above it,
       not as a second paragraph of lede copy. Italic was demoted in
       Phase 4k — the muted color is sufficient to flag it as secondary,
       and italic is now reserved for voice/title roles. */
    .timer-post-note {
      font-family: var(--font-serif);
      font-size: var(--t-meta);
      color: var(--muted);
      line-height: 1.55;
      margin: 8px 0 0;
    }

    /* ── Timer pill (Phase 5 mock) ────────────────────────────────
       Persistent control above the primary CTA. Replaces both the
       prior corner #timerDisplay widget AND the "Timer" slot in the
       bottom nav. Two visual modes driven by class on #timerPill:
         .timer-off  — small clock icon only; tap opens the Timer panel
         .timer-on   — pill with label (Start/Pause/Resume) + countdown
                       + ✕ disable button to the right
       Sits at the top of #bottomControls so it inherits the existing
       cinematic / chat-open hide rules through the parent (no extra
       overrides needed). */
    #timerPill {
      display: flex;
      align-items: center;
      justify-content: flex-end;
      gap: 6px;
      margin-bottom: 14px;
      min-height: 36px; /* keep vertical rhythm stable across off ↔ on */
      /* Override #bottomControls' align-items: center so the pill spans
         full width and its justify-content: flex-end pushes the contents
         to the right edge of the controls strip. Without this stretch
         the pill collapses to its content width and centers. */
      align-self: stretch;
    }
    #timerPillBtn {
      display: inline-flex;
      align-items: center;
      gap: 8px;
      background: color-mix(in srgb, var(--chip-tint) 5%, transparent);
      border: 1px solid color-mix(in srgb, var(--chip-tint) 10%, transparent);
      border-radius: 999px;
      padding: 6px 14px;
      color: var(--muted);
      font-family: var(--font-sans);
      font-size: var(--t-secondary);
      font-weight: 500;
      cursor: pointer;
      user-select: none;
      -webkit-user-select: none;
      transition: background 0.15s ease, border-color 0.15s ease, color 0.15s ease;
      position: relative;
      min-height: 36px;
    }
    /* Invisible 44px hit-target expander (matches .panel-close + header
       deck pill). Pill stays visually compact; touch target meets WCAG. */
    #timerPillBtn::before {
      content: '';
      position: absolute;
      inset: -6px;
    }
    #timerPillBtn:hover {
      background: color-mix(in srgb, var(--chip-tint) 9%, transparent);
      border-color: color-mix(in srgb, var(--chip-tint) 16%, transparent);
      color: var(--text);
    }
    #timerPillBtn:active { opacity: 0.7; }

    /* Clock glyph — only renders in the off state. Sized to read at
       small px without competing with the primary CTA. currentColor so
       the muted/pink hover transitions carry through. */
    .timer-pill-clock {
      width: 18px;
      height: 18px;
      flex: 0 0 auto;
      color: var(--muted);
      transition: color 0.15s ease;
      display: none; /* shown only in .timer-off via the rule below */
    }
    #timerPill.timer-off .timer-pill-clock { display: block; }
    #timerPillBtn:hover .timer-pill-clock { color: var(--text); }

    /* Off state visual: collapse to icon-only — no label, no time, no ✕.
       Quieter than a labeled pill so an unused timer doesn't pull eye
       weight away from "Draw a card". The label/time spans are hidden
       (not just empty) so the flex gap doesn't reserve space for them
       and the clock glyph centers in the pill. */
    #timerPill.timer-off #timerPillBtn {
      padding: 6px 10px;
    }
    #timerPill.timer-off #timerPillLabel,
    #timerPill.timer-off #timerPillTime {
      display: none;
    }
    #timerPill.timer-off #timerPillDisable {
      display: none;
    }

    /* On state: label + countdown share the pill body. Tabular nums on
       the time so 5:00 ↔ 4:32 doesn't shimmy the layout. */
    #timerPillLabel {
      font-family: var(--font-sans);
      font-size: var(--t-secondary);
      font-weight: 500;
      letter-spacing: 0.01em;
    }
    #timerPillTime {
      font-family: var(--font-sans);
      font-weight: 500;
      font-variant-numeric: tabular-nums;
      color: var(--pink);
      letter-spacing: 0.01em;
    }
    #timerPill.timer-on #timerPillBtn {
      color: var(--text);
    }

    /* ✕ disable button — quiet outlined circle to the right of the pill.
       Matches the .panel-close visual idiom but smaller and inline. */
    #timerPillDisable {
      display: flex;
      align-items: center;
      justify-content: center;
      width: 28px;
      height: 28px;
      border-radius: 50%;
      background: color-mix(in srgb, var(--chip-tint) 4%, transparent);
      border: 1px solid color-mix(in srgb, var(--chip-tint) 10%, transparent);
      color: var(--muted);
      font-size: 14px;
      line-height: 1;
      cursor: pointer;
      transition: background 0.15s, color 0.15s;
      position: relative;
    }
    #timerPillDisable::before {
      content: '';
      position: absolute;
      inset: -8px;
    }
    #timerPillDisable:hover {
      background: color-mix(in srgb, var(--chip-tint) 10%, transparent);
      color: var(--text);
    }
    #timerPillDisable:active { opacity: 0.7; }

    body.chat-open #timerPill { display: none !important; }
    /* Hide Play Solo when timer is on — same intent as before, kept here
       so the body.timer-on contract still ties Play Solo visibility to
       timer state without a separate marker. */
    body.timer-on #talkLink { display: none !important; }

    /* ── Time's Up overlay — two layers ─────────────────────────── */
    /* Background: sits just above the question (z 5) but below header/nav (z 10) */
    #timesUpBg {
      display: none;
      position: fixed;
      inset: 0;
      z-index: 6;
      /* color-mix on var(--bg), NOT a baked literal (this was
         rgba(26,22,18,.78) — dark --bg frozen in both modes, which left
         light mode's dark-ink heading unreadable on a dark scrim).
         Unlike the modal scrims (which stay dark in light mode — their
         content rides on a sheet above), this overlay's text sits
         DIRECTLY on the scrim, so the scrim must track --bg polarity:
         dark frosting under light text in dark mode, parchment frosting
         under ink text in light mode. */
      background: color-mix(in srgb, var(--bg) 78%, transparent);
      backdrop-filter: blur(6px);
      -webkit-backdrop-filter: blur(6px);
      pointer-events: none;
    }
    #timesUpBg.show {
      display: block;
      animation: timesUpFadeIn 0.4s ease forwards;
    }
    @keyframes timesUpFadeIn {
      from { opacity: 0; }
      to   { opacity: 1; }
    }
    /* Foreground: text + button, above everything */
    #timesUpFg {
      display: none;
      position: fixed;
      inset: 0;
      z-index: 151;
      flex-direction: column;
      align-items: center;
      justify-content: center;
      gap: 20px;
      pointer-events: none;
    }
    /* v130 V21 — match the background's 0.4s fade-in. Previously the
       foreground (heading + button) snapped to opacity 1 while the
       background scrim eased in over 400ms, making the type pop in
       ahead of its frame. Both layers now arrive together. */
    #timesUpFg.show {
      display: flex;
      animation: timesUpFadeIn 0.4s ease forwards;
    }
    #timesUpHeading {
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: italic;
      font-size: var(--t-display);
      color: var(--text);
    }
    #timesUpHint {
      font-family: var(--font-serif);
      font-weight: 400;
      font-style: italic;
      font-size: clamp(20px, 5vw, 30px);
      color: var(--text);
      text-align: center;
      line-height: 1.5;
      opacity: 0.85;
    }
    /* Outlined-pink-family primary on the time's-up overlay. The
       overlay's other path forward is gestural (swipe / tap draw,
       carried by the hint copy), so by the button-family decision
       tree this is "primary path forward when there are 2+ options."
       Was caps + gray-bordered + body-color text, which read as a
       muted stamp; the gestural alternative is hint copy, not a
       second button, so the explicit "Adjust timer" path deserves
       the outlined-pink recipe (matches #mainBtn, #consentAcceptBtn,
       #clearDataOkBtn). Sentence case throughout. */
    #timesUpModifyBtn {
      pointer-events: auto;
      margin-top: 12px;
      background: transparent;
      border: 1.5px solid var(--pink);
      border-radius: 999px;
      color: var(--pink);
      font-family: var(--font-sans);
      font-weight: 600;
      font-size: var(--t-body);
      text-transform: none;
      padding: 12px 24px;
      cursor: pointer;
      transition: background 0.2s, color 0.2s, border-color 0.2s, opacity 0.2s, transform 0.15s;
    }
    /* Hover scoped per #mainBtn / #consentAcceptBtn — iOS Safari
       otherwise leaves the button filled-pink after a tap. */
    @media (hover: hover) and (pointer: fine) {
      #timesUpModifyBtn:hover { background: var(--pink); color: var(--on-pink); }
    }
    #timesUpModifyBtn:active { transform: translateY(1px); opacity: 0.9; }

    /* ── iPad declutter toggle ───────────────────────────────────── */
    #ipadDeclutterBtn {
      position: fixed;
      z-index: 6;
      width: 34px;
      height: 34px;
      border-radius: 9px;
      border: 1px solid color-mix(in srgb, var(--chip-tint) 28%, transparent);
      background: color-mix(in srgb, var(--chip-tint) 10%, transparent);
      color: var(--text);
      display: flex;
      align-items: center;
      justify-content: center;
      cursor: pointer;
      opacity: 0.6;
      transition: opacity 0.2s, background 0.2s, border-color 0.2s;
      padding: 0;
    }
    #ipadDeclutterBtn:hover,
    #ipadDeclutterBtn:active { opacity: 1; background: color-mix(in srgb, var(--pink) 20%, transparent); border-color: color-mix(in srgb, var(--pink) 50%, transparent); }
    #ipadDeclutterBtn svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 2.5; stroke-linecap: round; stroke-linejoin: round; }
    body.ipad-declutter #ipadDeclutterBtn { opacity: 0.65; border-color: color-mix(in srgb, var(--chip-tint) 30%, transparent); }
    body.chat-open #ipadDeclutterBtn { display: none !important; }

    /* ── Reduced motion ─────────────────────────────────────────
       WCAG 2.3.3. Users who opt in to "Reduce motion" at the OS
       level (macOS System Settings → Accessibility → Display, iOS
       Settings → Accessibility → Motion) are telling the browser
       to stop every non-essential animation. For most of this app
       that means collapsing transition/animation durations so the
       state change still happens (state without motion) but the
       movement is effectively instant.

       Decorative loops (intro-card nudge, swipe-hint sweep, loading
       dots) are killed outright inside the block instead of left at
       0.01ms — that flashes harder than the real animation on some
       machines.

       The card slide-in/out animations collapse to a fast
       crossfade: opacity transitions ~120ms, transforms disabled.
       Faster than the 320ms slide but still gives the eye a
       state-change cue so the new card registers. Per the
       critique (A4), "near-instant snap" reads as a jump cut;
       crossfade is kinder. */
    @media (prefers-reduced-motion: reduce) {
      /* Blanket near-instant fallback for any animation/transition
         not overridden below. The 0.01ms value keeps state changes
         in the right order without visible motion. */
      *, *::before, *::after {
        animation-duration: 0.01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: 0.01ms !important;
        scroll-behavior: auto !important;
      }

      /* Card slide-in / slide-out: preserve a short crossfade so
         the user still gets a state-change signal — just without
         the horizontal travel that causes vestibular discomfort. */
      @keyframes slideOutLeft-rm  { from { opacity: 1; } to { opacity: 0; } }
      @keyframes slideOutRight-rm { from { opacity: 1; } to { opacity: 0; } }
      @keyframes slideInFromRight-rm { from { opacity: 0; } to { opacity: 1; } }
      @keyframes slideInFromLeft-rm  { from { opacity: 0; } to { opacity: 1; } }
      .slide-out-left  { animation: slideOutLeft-rm     140ms ease forwards !important; }
      .slide-out-right { animation: slideOutRight-rm    140ms ease forwards !important; }
      .slide-in-right  { animation: slideInFromRight-rm 140ms ease forwards !important; }
      .slide-in-left   { animation: slideInFromLeft-rm  140ms ease forwards !important; }

      /* Purely decorative loops: the intro-card nudge, swipe-hint
         sweep, loading brand-mark breathe. Kill them outright so Reduce
         Motion users never see them flicker at 0.01ms. */
      .quote-text.intro-logo-card.nudging { animation: none !important; }
      .swipe-dot { animation: none !important; }
      #quoteArea.is-loading .card-back-logo { animation: none !important; }
    }

    /* ── Narrow phones (iPhone SE 1st gen / 320 wide) ───────────
       At 320×568 the playing-card aspect lock leaves the body zone
       too cramped for longer questions: ~117×71 with the default
       22px padding + 28px gap, which clips even at the lo=14
       fitQuoteText floor. Tighten internal chrome so the body zone
       gets ~25px more height without the card losing its frame. */
    @media (max-width: 359px) {
      .quote-text {
        padding: 14px 16px;
        gap: 16px;
      }
      /* Match the tighter card padding so the played badge stays in the corner. */
      .played-badge { top: 12px; right: 12px; }
    }

