/* Friendly, rounded self-hosted fonts (OFL, subset to latin). Nunito for body — high x-height,
   very legible for younger readers; Fredoka for display headings — rounded and playful. Both are
   VARIABLE (one file covers the whole weight range). Self-hosted so the strict CSP is happy and the
   app renders in-font offline (both files are precached in sw.js). font-display:swap so first paint
   never blocks on the download; the system-ui fallback tail below covers that moment. */
@font-face {
  font-family: "Nunito";
  font-style: normal;
  font-weight: 200 1000;
  font-display: swap;
  src: url("/fonts/nunito-var.woff2?v=1") format("woff2");
}
@font-face {
  font-family: "Fredoka";
  font-style: normal;
  font-weight: 300 700;
  font-display: swap;
  src: url("/fonts/fredoka-var.woff2?v=1") format("woff2");
}

:root {
  color-scheme: dark;
  --font-display: "Fredoka", "Nunito", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  --bg:        #0b0b0b;
  --surface:   #1a1a19;
  --surface-2: #242422;
  --border:    #2f2f2c;
  --text-1:    #ffffff;
  --text-2:    #b9b8b1;
  --accent:    #3987e5;
  --accent-2:  #1baf7a;
  --good:      #2ecc71;   /* success/mastered green — was used (learn plan) but never defined here */
  --track:     #33332f;
  --danger:    #d95926;
  /* Medium severity. Never the only carrier of that meaning — every severity chip also
     spells its level out in words (see .chip.sev), because colour alone excludes the
     ~1 in 12 men with a colour-vision deficiency on a screen that gates publication. */
  --warn:      #d9a12b;
  /* Height reserved at the bottom of the viewport for the fixed bottom bar (version
     label + create button — one transparent overlay now, no separate version strip).
     Every element that anchors near the bottom edge adds this to its own offset, so
     the bar overlays nothing. One number, one place to change it. Signed out the bar
     holds only the version line, and the override further down shrinks this to
     match. */
  --navbar-h:  56px;
}
:root[data-theme="light"] {
  color-scheme: light;
  --bg:        #f4f4f2;
  --surface:   #ffffff;
  --surface-2: #f0f0ed;
  --border:    #e2e2dd;
  --text-1:    #0b0b0b;
  --text-2:    #52514e;
  --accent:    #2a78d6;
  --accent-2:  #008300;
  --good:      #0a8a3f;
  --track:     #e6e6e2;
  /* Darker than the dark theme's: #d9a12b on white is under 3:1 and unreadable. */
  --warn:      #8a5a00;
  /* Same reasoning, and it had been missed. Measured in the browser, not eyeballed:
     the dark theme's #d95926 scored 3.53:1 on this background — under AA — and it is
     used BOTH as a text colour (rejection reasons, overdue ages, the quiz "Not quite")
     and as a fill behind WHITE text (.chip.sev-high, .chip.st-terminated), which scored
     3.88:1 the other way. Both readings were failing, so darkening fixes both at once:
     5.89:1 as text here, 6.49:1 for white on it. */
  --danger:    #a8380f;
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }
html, body { height: 100%; margin: 0; overscroll-behavior-y: none; }
/* The pager is 300vw wide (context + feed + stats side by side); its off-screen
   pages must be clipped at the ROOT, or the document itself scrolls sideways — clipping on <body>
   alone does not reduce documentElement.scrollWidth. `clip` (not `hidden`) because it
   clips without turning <html> into a scroll container, so the feed and the stats page
   keep owning their own vertical scrolling inside. */
/* overscroll-behavior-x: none disables the browser's swipe-from-edge history navigation
   (2026-07-25). The whole app is built on horizontal swipes — the feed pager, and now the
   profile's right-swipe to Settings — and a right-swipe starting near the left edge would
   otherwise be eaten by "go back" instead of moving the pager/panel. The feed pager already
   preventDefaults its gesture; this protects the rest, defence-in-depth. */
html { overflow-x: clip; overscroll-behavior-x: none; }
body {
  background: var(--bg); color: var(--text-1);
  font: 16px/1.5 "Nunito", system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
  overflow-x: clip; overscroll-behavior-x: none;
}

/* Headings carry the playful display face app-wide; the fallback tail keeps them sane pre-load. */
h1, h2, h3 { font-family: var(--font-display); }

/* ---- the feed: one poll per screen, snap on swipe ---------------------- */
/* ---- the horizontal pager: [ context | feed | results ] --------------------
   Three full-width pages in one 300vw row, the feed in the middle. data-view slides
   between them; the drag itself is tracked live by js/statspager.js, which sets an
   inline transform and the .dragging class (transition off) so the page follows the
   finger, then hands back to this transition to settle. The RESTING position is
   -100vw (the feed), which is why every offset below is one page further left than
   the two-page version of this pager. */
.pager {
  display: flex;
  width: 400vw;   /* [ context | feed | stats | comments ] — comments joined the strip 2026-07-24 */
  height: 100dvh;
  transform: translateX(-100vw);
  transition: transform .3s cubic-bezier(.22, .61, .36, 1);
  will-change: transform;
}
/* Tell the browser up front that only vertical panning is native here, so a
   horizontal touch stream is left to the pager's JS. This is defense-in-depth: what
   actually decides the gesture is statspager.js cancelling the FIRST touchmove (the
   browser grants exactly one cancelable move — see the JITTER_PX comment there), but
   pan-y is the declared contract real browsers use for arbitration, and it prevents
   the UA starting a scroll during any window JS has not yet claimed.

   It sits on the two PAGES (the scroll containers), not on .pager: arbitration walks
   the chain from the touched element up to the scroller, and .pager is above the
   scroller, so a value there never enters the decision — measured, not assumed
   (tests/e2e/swipe.spec.js failed with it on .pager). */
/* touch-action: pan-y must sit on the actual SCROLL CONTAINER, or the pager cannot claim a
   horizontal swipe from it (statspager.js preventDefaults the first cancelable touchmove;
   an `auto` scroller never yields one — measured). The stats/context pages ARE the
   scroller (.results-page), but the comments page delegates scrolling to .comments-body
   (its .comments-page is overflow:hidden), so that inner scroller needs pan-y too — without
   it, you could not swipe back OUT of the comments page on a real phone (2026-07-25). */
#feed, .results-page, .comments-body { touch-action: pan-y; }
.pager[data-view="context"] { transform: translateX(0); }
.pager[data-view="stats"] { transform: translateX(-200vw); }
.pager[data-view="comments"] { transform: translateX(-300vw); }
.pager.dragging { transition: none; }
/* A quiz or bracket has no stats page (the breakdown is poll-only). Collapse the empty stats
   page so its COMMENTS become the immediate left-swipe (statspager.js's `.no-stats` mode) —
   instead of the left-swipe opening a blank page, or, as before this, being refused entirely
   so nothing happened (owner, 2026-07-26). Comments then sits where stats did, at -200vw. */
.pager.no-stats #results { display: none; }
.pager.no-stats[data-view="comments"] { transform: translateX(-200vw); }

#feed {
  flex: 0 0 100vw; width: 100vw;
  height: 100dvh; overflow-y: auto;
  scroll-snap-type: y mandatory;
  scroll-behavior: smooth;
  scrollbar-width: none;
}
#feed::-webkit-scrollbar { display: none; }

.slide {
  height: 100dvh; scroll-snap-align: start; scroll-snap-stop: always;
  display: grid; grid-template-rows: auto 1fr auto;
  /* The topbar is fixed and overlays the slide, so the first row (the badges)
     has to start below it — otherwise a category badge sits under the wordmark.
     Only showed up once polls actually carried a category. */
  /* The bottom padding also clears the fixed version footer AND the tab bar beneath
     it — without both, the like/share rail and the last option row sit underneath
     them. */
  /* 8px side gutters, down from 18 (product owner, 2026-07-23): the post takes the
     full width of the screen. */
  padding: calc(max(10px, env(safe-area-inset-top)) + 58px) 8px
           calc(max(18px, env(safe-area-inset-bottom)) + var(--navbar-h));
  position: relative;
}
/* Desktop: don't stretch a phone layout across a monitor. */
@media (min-width: 760px) {
  .slide { max-width: 620px; margin: 0 auto; }
}

/* ---- the ambient backdrop (product owner, 2026-07-23) ----------------------
   A slide is no longer a dead black/white rectangle: js/render.js drops a decorative
   layer behind every post. It is absolutely positioned and clipped, so it changes not
   one thing about the grid the content sits in, and the content rows are lifted above
   it by z-index alone (below). pointer-events:none so nothing here can ever swallow a
   tap meant for an option or the rail. */
.slide-bg {
  position: absolute; inset: 0; z-index: 0;
  overflow: hidden; pointer-events: none;
}
.slide > .badge-row, .slide > .middle, .slide > .foot { position: relative; z-index: 1; }

/* The colour wash: two soft blobs of the poll's own hue, one warm-shifted, fading to
   nothing through the middle so the base background still carries the text's contrast.
   Painted UNDER the blurred image (a real child, so it stacks on top) — the image wins
   where there is one, the wash carries every text-only post. */
.slide-bg::before {
  content: ""; position: absolute; inset: -20%;
  /* A four-stop MESH rather than the two blobs this started as (2026-07-24): three hues
     stepped off the post's own, plus a deep anchor, so a text-only post reads as a
     considered gradient instead of a flat tint. inset:-20% gives the drift below room to
     move without ever exposing an edge. */
  /* Layer one is fine grain, over the mesh. A flat CSS gradient bands visibly across a
     phone-sized dark screen; a little noise breaks the banding up and reads as texture
     rather than as a defect. The SVG is inline (no request), tiles at its intrinsic
     140px, and the rect's own alpha keeps the effective strength around .06 — present
     when you look for it, invisible when you do not. */
  background:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='140' height='140'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='.85' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='140' height='140' filter='url(%23n)' opacity='.12'/%3E%3C/svg%3E"),
    radial-gradient(62% 48% at 14% 10%,
      hsl(var(--slide-hue, 220) 74% 54% / .42), transparent 62%),
    radial-gradient(58% 46% at 88% 22%,
      hsl(calc(var(--slide-hue, 220) + 38) 70% 50% / .34), transparent 64%),
    radial-gradient(64% 52% at 78% 88%,
      hsl(calc(var(--slide-hue, 220) + 72) 68% 52% / .36), transparent 66%),
    radial-gradient(70% 60% at 30% 76%,
      hsl(calc(var(--slide-hue, 220) - 28) 62% 42% / .28), transparent 70%);
  /* Very slow, very small: a 34s breathe that a reader notices only as the screen feeling
     alive, never as movement competing with a video post or the answer flourish. Paused
     outright under reduced motion (below). */
  animation: bg-drift 34s ease-in-out infinite alternate;
  /* Deliberately NO will-change: this pseudo-element exists on every slide in the feed,
     and a promoted layer each would cost real memory on a phone for a 34s drift the
     compositor promotes by itself while it runs. */
}
@keyframes bg-drift {
  from { transform: translate3d(0, 0, 0) scale(1); }
  to   { transform: translate3d(2.5%, -2%, 0) scale(1.06); }
}
/* The post's own picture, blurred to a colour field — the "now-playing" bloom. Scaled
   up so the blur has no hard edges, dimmed so the large bold text stays legible over
   the base background showing through. */
.slide-bg-img {
  position: absolute; inset: 0; width: 100%; height: 100%;
  object-fit: cover; transform: scale(1.35);
  filter: blur(60px) saturate(1.35); opacity: .40;
}
/* A gentle top-and-bottom scrim toward the base colour, so the badges and the footer
   row keep a clean ground while the colour blooms in the middle where the question is. */
.slide-bg::after {
  content: ""; position: absolute; inset: 0;
  background: linear-gradient(var(--bg), transparent 18% 76%, var(--bg));
  opacity: .64;
}
:root[data-theme="light"] .slide-bg-img { opacity: .30; }

.badge-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
.badge {
  font-size: 11px; font-weight: 700; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-2); background: var(--surface); border: 1px solid var(--border);
  padding: 5px 10px; border-radius: 999px;
}
.badge.ai { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
/* The Following feed's "{name} reposted" attribution (Phase 20). A link, not a label —
   it points at whoever surfaced this — so it drops the uppercase/letter-spacing shout of a
   category chip and reads as a sentence, tinted the same green the inbox uses for the
   `reposted` notification so the two obviously mean the same thing. */
.badge.repost {
  display: inline-flex; align-items: center; gap: 5px;
  text-transform: none; letter-spacing: 0; font-weight: 650;
  color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 40%, transparent);
  text-decoration: none; max-width: 100%;
}
.badge.repost span { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.badge.repost svg { flex: none; }

/* The feed-mode switch (Phase 20), centred in the topbar. Absolutely positioned so it
   sits dead-centre regardless of how wide the wordmark or the action cluster are, and
   pointer-events re-enabled because the topbar itself disables them (it is a gradient
   scrim that must not eat taps meant for the feed). Hidden until signed in. */
/* TikTok-style top tabs (owner, 2026-07-26): plain text, the active one lit and
   underlined, the rest dimmed — NOT a centred pill. It was an absolutely-centred pill
   before, which grew wide enough with a third tab to slide under the bell and dashboard
   icons. Now it is an in-flow, flex-shrinkable strip that lives in its OWN track between
   the wordmark and the icons, so it can never overlap them. When the tabs outgrow that
   track they scroll horizontally and fade out at the edges ("morph into invisible")
   rather than pushing anything. */
.feed-modes {
  display: none; flex: 0 1 auto; min-width: 0; gap: 2px; margin: 0 4px;
  overflow-x: auto; overflow-y: hidden; scrollbar-width: none;
  pointer-events: auto;
  -webkit-mask-image: linear-gradient(to right, transparent 0, #000 12px, #000 calc(100% - 12px), transparent 100%);
          mask-image: linear-gradient(to right, transparent 0, #000 12px, #000 calc(100% - 12px), transparent 100%);
}
.feed-modes::-webkit-scrollbar { display: none; }
html.signed-in .feed-modes { display: flex; }
.feed-mode {
  flex: 0 0 auto; position: relative;
  border: 0; background: transparent; color: var(--text-2);
  font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
  padding: 4px 6px; line-height: 1.15; white-space: nowrap; opacity: 0.65;
}
.feed-mode.is-active { color: var(--text-1); opacity: 1; }
/* The active underline, inset so it sits under the word, not the tap padding. */
.feed-mode.is-active::after {
  content: ''; position: absolute; left: 7px; right: 7px; bottom: -1px; height: 2.5px;
  border-radius: 2px; background: var(--accent);
}
.feed-mode:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }

/* Wheel selector (tabwheel.js adds .tw). The same tabs, laid out as a rolling picker: JS
   positions each tab every frame with an inline transform + opacity, so these rules only set
   the stage — a fixed-height, clipped, edge-faded strip whose children stack at its centre.
   touch-action:none hands every drag to the wheel (it would otherwise try to pan the page). */
/* Shown to EVERYONE (2026-08-09): a signed-out reader lands in Learn and needs the wheel to move
   between Learn and the feed. The per-account tabs (Following/Updates/Inbox/Dashboard) are hidden
   individually until auth reveals them, so an anonymous wheel carries just Latest + Learn. */
.feed-modes.tw { display: block; }
.feed-modes.tw {
  position: relative; height: 34px; overflow: hidden;
  flex: 1 1 auto; min-width: 0; margin: 0 6px;
  perspective: 340px;                 /* the dial's 3D depth — tabs angle away in it (tabwheel.js) */
  touch-action: none; user-select: none; -webkit-user-select: none;
  -webkit-mask-image: linear-gradient(to right, transparent, #000 15%, #000 85%, transparent);
          mask-image: linear-gradient(to right, transparent, #000 15%, #000 85%, transparent);
}
/* CYLINDRICAL SHADING — top and bottom edges fade to the background so the row reads as the
   curved face of a round dial; the tabs themselves are the ones that rotate away in 3D. Inert. */
.feed-modes.tw::after {
  content: ''; position: absolute; inset: 0; z-index: 101; pointer-events: none;
  background: linear-gradient(to bottom,
    color-mix(in srgb, var(--bg) 78%, transparent), transparent 32%,
    transparent 68%, color-mix(in srgb, var(--bg) 78%, transparent));
}
.feed-modes.tw .feed-mode {
  position: absolute; left: 50%; top: 50%;
  transform: translate(-50%, -50%);   /* JS appends the per-tab dial transform (rotateY + depth) */
  transform-origin: center center; backface-visibility: hidden;
  will-change: transform, opacity;
  padding: 3px 8px; font-size: 14px; opacity: 1;
  transition: color 0.15s ease;
}
/* POSITION DOTS — one per wheel tab, under the dial (the dial only shows ~3 at once). They tell
   the reader how many tabs there are and which one is selected. Built + tracked by tabwheel.js. */
.feed-dots {
  display: flex; position: absolute; left: 0; right: 0; bottom: 8px; z-index: 102;
  justify-content: center; align-items: center; gap: 4px; pointer-events: none;
}
.feed-dot {
  width: 3px; height: 3px; border-radius: 50%;
  background: color-mix(in srgb, var(--text-1) 26%, transparent);
  transition: width 0.2s ease, background-color 0.2s ease;
}
.feed-dot.on { width: 9px; border-radius: 2px; background: var(--accent); }

/* min-width: 0 for the same reason as min-height: 0 — a grid item's min size is
   auto, so wide content (the bracket tree's columns) would otherwise blow the track
   open past the slide's padding instead of shrinking to fit. Seen, not theorised:
   the champion view rendered with its title unwrapped and its button cut off at the
   viewport edge. */
/* `safe center` centres the content when it FITS, but falls back to top-alignment when
   it does not — without it, a tall post (image + four options + a source line) overflows
   a flex column equally top AND bottom, and the image rode up UNDER the category badges
   (owner's screenshot, 2026-07-23). `safe` keeps the image below the badges and lets the
   overflow fall off the bottom instead, where the slide's own padding already clears the
   chrome. */
.middle { display: flex; flex-direction: column; justify-content: safe center; min-height: 0; min-width: 0; }
.q {
  font-size: clamp(26px, 6.4vw, 40px); font-weight: 750; line-height: 1.18;
  letter-spacing: -0.02em; margin: 0 0 20px;
  text-wrap: balance;
}

/* ---- generated media and news attribution ------------------------------ */

/* The image sits ABOVE the question and is capped by viewport height, not by a fixed
   pixel size: one poll fills one screen here, so an image tall enough to push the
   options below the fold breaks the whole interaction on a short phone. */
.slide-media {
  /* Capped a little lower than before (was 38vh/320) so an image post with four
     options and a source line has room to fit one screen instead of overflowing — the
     other half of the badge-clip fix above. A small top margin keeps a hair of air
     between the image and the badges even when `safe center` does align to the top. */
  width: 100%; max-height: min(31vh, 270px); object-fit: cover;
  border-radius: 14px; border: 1px solid var(--border);
  margin: 6px 0 16px; background: var(--surface-2);
  /* A missing or slow image must not shift the question downward as it loads. */
  aspect-ratio: 16 / 9;
}
/* A quiz/bracket also carries a title, a progress line and up to four choices, so its
   banner is capped shorter than a poll's to keep the answers on one screen (owner,
   2026-07-26: the ambient bloom alone read as "no image"). */
.quiz-media { max-height: min(20vh, 180px); margin: 4px 0 12px; }
/* ---- the full-bleed video post (owner, 2026-07-23) ------------------------
   A video post fills the whole slide with the clip, TikTok-style, and the question and
   options float on top of it. The clip is a full-bleed element behind the content;
   object-fit: cover fills a portrait screen from a landscape clip by cropping the sides
   (the subject is centre-framed by the generation prompt). pointer-events: none so a tap
   always reaches the option under the finger, never the video. z-index sits it above the
   hue-wash backdrop but below the content rows (which are z-index 1). */
.slide-fill {
  position: absolute; inset: 0; z-index: 0;
  width: 100%; height: 100%; object-fit: cover;
  pointer-events: none;
}
/* A legibility scrim over the clip. The content (question + options) is one block anchored
   to the BOTTOM, so the scrim is clear across the top — where the clip is the hero — and
   darkens through the lower half where the text sits. A light touch at the very top keeps
   the badges legible. Painted after the fill, before the content. */
.slide.video-post::after {
  content: ""; position: absolute; inset: 0; z-index: 0; pointer-events: none;
  background: linear-gradient(to bottom,
    rgba(0,0,0,.42) 0%, rgba(0,0,0,.12) 15%, transparent 32%,
    transparent 44%, rgba(0,0,0,.45) 68%, rgba(0,0,0,.84) 100%);
}
/* Question and options are ONE cohesive block anchored to the bottom of the slide, with the
   clip as a full-bleed hero above (owner, 2026-07-28). The old layout split them — question
   pinned top, options pinned bottom via margin-bottom:auto — which left a dead strip of empty
   video between them. The clip is the full-bleed BACKGROUND, visible behind everything, so
   grouping the content hides none of it; it just stops wasting the middle of the screen. */
.slide.video-post .middle { justify-content: flex-end; }
.slide.video-post .q { text-shadow: 0 2px 14px rgba(0,0,0,.65); }
.slide.video-post .source { text-shadow: 0 1px 8px rgba(0,0,0,.7); }
.slide.video-post .count { color: #fff; text-shadow: 0 1px 8px rgba(0,0,0,.7); }
/* Semi-transparent option pills with a frosted blur, so the reader sees the video THROUGH
   them and can still read and tap each one. The fill bar and the voted/mine states are
   unchanged — they layer inside the same translucent pill. */
.slide.video-post .opt {
  background: color-mix(in srgb, var(--surface) 52%, transparent);
  border-color: color-mix(in srgb, #fff 22%, transparent);
  -webkit-backdrop-filter: blur(12px) saturate(1.2);
  backdrop-filter: blur(12px) saturate(1.2);
}
@media (hover: hover) { .slide.video-post .opt:hover { border-color: var(--accent); } }

/* The admin video panel's toggle and success line. */
.video-toggle { display: flex; align-items: center; gap: 8px; margin: 6px 0 16px; font-weight: 600; }
.video-ok { color: var(--accent-2); font-weight: 600; margin: 8px 0 0; }

/* The found-photo credit: legible but subordinate — it must not compete with the
   question. Sits tight under the image, eating into its bottom margin. */
.media-credit {
  display: block; margin: -14px 2px 14px; font-size: 11px;
  color: var(--text-2); text-decoration: none;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
}
a.media-credit:hover { text-decoration: underline; }
/* On a quiz/bracket card the credit follows the source line rather than an image, so the
   negative top margin (which tucks it under a poll's photo) would drag it up over the
   source. Stack them instead. */
.source + .media-credit { margin-top: 6px; }

.source {
  /* align-self, NOT display:inline-block. `.middle` is a flex column, so this anchor is a
     flex item and stretches to the full card width regardless of its display — which drew
     the underline right across the card as if it were a divider. */
  align-self: flex-start;
  margin-top: 16px;
  font-size: 13px; color: var(--text-2); text-decoration: none;
  border-bottom: 1px solid var(--border);
}
.source:hover, .source:focus-visible { color: var(--accent); border-bottom-color: var(--accent); }

/* Closed is stated in words in the badge and the footer; the colour only reinforces it,
   so nothing here is the sole carrier of the meaning. */
.badge.closed { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 40%, transparent); }

/* The AI marker beside a simulated user's name. Small, but never subtle enough to miss:
   disclosure that a reader has to hunt for is not disclosure. */
.badge.sm {
  font-size: 10px; padding: 1px 6px; letter-spacing: 0.06em; vertical-align: middle;
}

/* Right padding keeps every option's percentage text (right-aligned inside the pill)
   clear of the action rail, which floats over the lower-right corner of the slide —
   see .actions below. Shared by polls and quizzes; both render into .opts. */
.opts { display: flex; flex-direction: column; gap: 10px; padding-right: 62px; }
/* Two-per-row when the options are short (render.js's optionsLayout) — half the vertical
   space, so more of the card is usable (owner, 2026-07-26). Slightly tighter type/padding so
   a date or short phrase sits comfortably on half the width. */
.opts.two-col { display: grid; grid-template-columns: 1fr 1fr; align-content: start; }
.opts.two-col .opt { min-height: 50px; padding: 12px 14px; font-size: 15px; }
.opts.two-col .opt .label { gap: 8px; }

.opt {
  position: relative; width: 100%; text-align: left;
  font: inherit; font-weight: 600; color: var(--text-1);
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 16px; padding: 15px 18px; min-height: 56px;
  cursor: pointer; overflow: hidden;
  transition: transform .12s ease, border-color .15s ease, background .15s ease;
}
.opt:active { transform: scale(.985); }
.opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) { .opt:hover { border-color: var(--accent); } }

/* Result state: the bar is a background layer inside the same pill, so the
   option doesn't jump position when results appear. */
.opt .bar {
  position: absolute; inset: 0 auto 0 0; width: 0;
  background: color-mix(in srgb, var(--accent) 26%, transparent);
  /* No leading edge until results exist, or a 0%-wide bar shows a sliver. */
  border-right: 0 solid var(--accent);
  transition: width .5s cubic-bezier(.22,.8,.28,1);
}
.voted .opt .bar { border-right-width: 2px; }
.opt.mine .bar { background: color-mix(in srgb, var(--accent-2) 26%, transparent); border-right-color: var(--accent-2); }
.opt .label { position: relative; display: flex; justify-content: space-between; gap: 14px; align-items: center; }
.opt .pct { font-variant-numeric: tabular-nums; font-weight: 750; opacity: 0; transition: opacity .3s ease .15s; }
.voted .opt .pct { opacity: 1; }
.voted .opt { cursor: default; }
.tick { color: var(--accent-2); font-weight: 800; }

/* ---- the answer flourish (product owner, 2026-07-23) ----------------------
   Voting or answering should feel like something happened. Three light touches, none
   of which move layout:

   The bars sweep in as a CASCADE rather than all at once — each option's fill starts a
   beat after the one above, so the result "counts up the board" instead of snapping.
   (The delay only applies once .voted is on, so it never delays the empty resting bar.) */
.voted .opts .opt:nth-child(2) .bar { transition-delay: .05s; }
.voted .opts .opt:nth-child(3) .bar { transition-delay: .10s; }
.voted .opts .opt:nth-child(4) .bar { transition-delay: .15s; }
.voted .opts .opt:nth-child(5) .bar { transition-delay: .20s; }

/* The option the reader actually chose gives a single spring of acknowledgement.
   Driven by js/vote.js adding .just-voted on the optimistic paint ONLY — never on a
   scroll-back re-render of an already-voted poll, which would replay it uninvited. */
@keyframes opt-confirm {
  0%   { transform: scale(1); }
  35%  { transform: scale(1.03); }
  100% { transform: scale(1); }
}
/* A glowing ring that flares out of the chosen option and dissolves — the "something
   happened!" flash, paired with the confetti and the buzz js/vote.js fires (owner,
   2026-07-23: "make it exciting"). Two animations on the one element, comma-separated:
   the spring above and this glow. */
@keyframes confirm-glow {
  0%   { box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent-2) 60%, transparent); }
  100% { box-shadow: 0 0 0 16px transparent; }
}
.opt.just-voted {
  animation: opt-confirm .45s cubic-bezier(.34,1.56,.64,1),
             confirm-glow .6s ease-out;
}
/* A quiz reveals its answer through a full re-render, so a CSS animation on the freshly
   marked options fires on its own — no JS hook needed. */
.quiz-opt.right, .quiz-opt.wrong { animation: opt-confirm .45s cubic-bezier(.34,1.56,.64,1); }

/* The tick/cross pops in with a spring instead of just appearing. */
@keyframes tick-pop {
  0%   { transform: scale(0); opacity: 0; }
  55%  { transform: scale(1.35); }
  100% { transform: scale(1); opacity: 1; }
}
.tick { display: inline-block; }
.opt.mine .tick, .quiz-opt.right .tick, .quiz-opt.wrong .tick {
  animation: tick-pop .4s cubic-bezier(.34,1.56,.64,1) both;
}

/* ---- quizzes (Phase 8) ---------------------------------------------------
   A quiz reuses .opt so a question looks like a poll — same target size, same
   rhythm — and adds only what marking an answer right or wrong requires. */

.quiz-title { margin: 0 0 4px; font-size: 19px; font-weight: 750; color: var(--text-2); }
.quiz .q { margin: 0 0 14px; }
.quiz-progress { font-size: 13px; margin: 0 0 6px; }
/* .dim is defined once, further down, and already carries the colour and size this
   needs. A second copy here was redundant — the later rule won anyway. */

/* Correct and incorrect are carried by a tick/cross in the label as well as by
   colour, because colour alone is invisible to a reader who cannot tell red from
   green — the same rule .badge.closed follows above. */
.quiz-opt.right {
  border-color: var(--accent-2);
  background: color-mix(in srgb, var(--accent-2) 14%, var(--surface));
}
.quiz-opt.wrong {
  border-color: var(--danger);
  background: color-mix(in srgb, var(--danger) 14%, var(--surface));
}
.quiz-opt:disabled { cursor: default; opacity: 1; }
.quiz-opt.wrong .tick { color: var(--danger); }

.quiz-feedback { margin-top: 14px; display: flex; flex-direction: column; gap: 6px; }
.quiz-feedback .quiz-verdict { margin: 0; font-weight: 750; }
.quiz-feedback.right .quiz-verdict { color: var(--accent-2); }
.quiz-feedback.wrong .quiz-verdict { color: var(--danger); }
.quiz-feedback .explain { margin: 0; color: var(--text-2); font-size: 14px; line-height: 1.45; }
.quiz-feedback .btn { align-self: flex-start; margin-top: 6px; }
.quiz-error { color: var(--danger); font-size: 14px; margin: 10px 0 0; }
.quiz-retake { margin-top: 14px; }

/* The score comparison (owner, 2026-07-24): You / Average / Best side by side, so a
   finished quiz answers "how did I do against everyone else", not just "here is a
   number". Three equal columns; the reader's own cell is tinted with the accent so the
   eye lands on where THEY placed first, and the best cell is warmed so the target reads
   as the thing to chase. */
.quiz-beat { margin: 12px 0 0; font-size: 15px; font-weight: 650; color: var(--text-1); }
.quiz-beat strong { color: var(--accent); }
/* Tone-tuned finish headline: WOW a strong score, warmly ENCOURAGE a weak one. */
.quiz-beat.tone-wow { font-size: 17px; font-weight: 800; color: #ffd23f; }
.quiz-beat.tone-wow strong { color: #ffd23f; }
.quiz-beat.tone-encourage { color: var(--text-1); font-weight: 700; }
.quiz-compare {
  margin: 10px 0 2px; display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px;
  max-width: 340px;
}
.quiz-compare .cmp {
  display: flex; flex-direction: column; align-items: center; gap: 2px;
  padding: 10px 6px; border-radius: 12px;
  background: var(--surface-2); border: 1px solid var(--border);
}
.quiz-compare .cmp-n { font-size: 22px; font-weight: 800; line-height: 1; color: var(--text-1); }
.quiz-compare .cmp-l { font-size: 12px; font-weight: 600; color: var(--text-2); }
.quiz-compare .cmp-you { background: color-mix(in srgb, var(--accent) 20%, var(--surface-2)); border-color: var(--accent); }
.quiz-compare .cmp-you .cmp-n { color: var(--accent); }
.quiz-compare .cmp-best { background: color-mix(in srgb, var(--accent-2) 16%, var(--surface-2)); border-color: var(--accent-2); }
.quiz-compare-n { font-size: 12px; margin: 4px 0 0; }
.quiz-compare-few { font-size: 14px; margin: 12px 0 0; line-height: 1.45; max-width: 340px; }

/* ---- brackets (Phase 10) ------------------------------------------------- */

.bracket-progress { font-size: 13px; margin: 0 0 6px; }
.bracket-start { margin-top: 10px; }

/* The matchup: two big cards with a small "vs" between — one thumb-reach column,
   reusing .opt so the tap targets match a poll's. */
.bracket-vs { display: flex; flex-direction: column; gap: 6px; }
.bracket-vs .bracket-opt { min-height: 72px; }
.bracket-vs-label { align-self: center; font-size: 13px; text-transform: uppercase; letter-spacing: .08em; }

/* The personal tree (R4.3): one column per round, space-around so a later round's
   boxes sit between the pair that fed them. Wide brackets scroll inside their own
   box rather than the page — same rule as every wide element here. */
.tree {
  display: flex; gap: 10px; align-items: stretch;
  margin: 12px 0; overflow-x: auto; max-width: 100%;
}
.tree-col { display: flex; flex-direction: column; justify-content: space-around; gap: 4px; min-width: 0; flex: 1; }
.tn {
  border: 1px solid var(--border); border-radius: 8px;
  padding: 3px 8px; font-size: 12px; line-height: 1.3;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 15ch;
}
.tn.bye { border-style: dashed; opacity: .35; min-height: 1.6em; }
.tn.champ {
  border-color: var(--accent-2); font-weight: 750;
  background: color-mix(in srgb, var(--accent-2) 14%, var(--surface));
}
.bracket-crowd { margin: 4px 0 0; font-size: 14px; line-height: 1.45; }
.bracket-retake { margin-top: 12px; }

/* ---- composing a quiz --------------------------------------------------- */

.quiz-question {
  border: 1px solid var(--border); border-radius: 14px;
  padding: 12px 14px 14px; margin: 0 0 12px; min-width: 0;
}
.quiz-question legend { font-size: 13px; font-weight: 700; color: var(--text-2); padding: 0 6px; }

/* The radio and its text field on one row, so "which of these is right" reads as a
   single choice down the column rather than as eight unrelated controls. */
.quiz-choice-row { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.quiz-choice-row .quiz-choice { flex: 1; min-width: 0; margin: 0; }
/* A radio is a small target; the label around it gives it a comfortable one without
   changing how it looks. */
.quiz-correct { display: flex; align-items: center; padding: 8px; margin: -8px 0 -8px -8px; cursor: pointer; }

/* A button that reads as a link. Used for "Making a quiz instead?" — it is a control,
   not navigation, so it stays a <button> for keyboard and screen-reader behaviour. */
.linkish {
  font: inherit; color: var(--accent); background: none; border: 0; padding: 0;
  cursor: pointer; text-decoration: underline;
}
.linkish:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* position:relative is now just the anchor for the transient heart-burst/toolbar-
   bounce feedback (below) — the action row itself no longer lives here at all, see
   /* min-width:0 so the label yields before the actions do — "Vote to see results" can
   wrap, unlike a button. */
.count { color: var(--text-2); font-size: 13px; min-width: 0; }

/* The count and the context cue sit on one row; the rail is absolutely positioned out
   of this flow, so .foot only lays out these two. */
.foot { display: flex; align-items: center; gap: 12px; }
/* The context-page cue: a quiet pill that says the swipe-right page exists and is worth
   opening, and is itself the tap target for readers with no touchscreen. Muted so it
   never competes with the question or the vote, but legible over any slide. Only
   rendered when the poll has a context page (js/render.js), so its mere presence is the
   signal. */
.context-cue {
  display: inline-flex; align-items: center; gap: 5px;
  font: inherit; font-size: 12px; font-weight: 700; cursor: pointer;
  color: var(--text-2); background: color-mix(in srgb, var(--surface) 70%, transparent);
  border: 1px solid var(--border); border-radius: 999px; padding: 5px 11px 5px 9px;
  white-space: nowrap; flex: 0 0 auto;
}
.context-cue svg { opacity: .8; }
.context-cue:active { transform: scale(.96); }
@media (hover: hover) { .context-cue:hover { color: var(--text-1); border-color: var(--accent); } }
.context-cue:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }

/* The TikTok-style rail (product owner, 2026-07-23, from a reference screenshot):
   a TRANSPARENT vertical overlay hugging the right edge of every slide — it consumes
   no layout space at all. Absolutely positioned INSIDE its slide, which is the
   trap-free geometry: it scrolls with the post, and no transformed-ancestor
   fixed-position or stacking-context surprise can reach it (three of those bit the
   fixed footer-strip design this replaces, in one evening). Always visible — no
   docking, no chevron, nothing to discover. */
.rail {
  position: absolute; right: 6px;
  bottom: calc(max(18px, env(safe-area-inset-bottom)) + var(--navbar-h) + 8px);
  z-index: 3;
  /* 8px between now-uniform buttons: with every .act the same height (see below), a
     tight, even gap reads as one organised cluster rather than four scattered icons
     (owner, 2026-07-23). Down from 14 originally, when the rail "stretched too far up". */
  display: flex; flex-direction: column; align-items: center; gap: 8px;
  background: transparent; padding: 0;
}

/* The poster: a tappable avatar with the follow badge overlapping its bottom edge,
   exactly the reference's arrangement. */
.rail-avatar {
  position: relative; display: block; width: 46px; height: 46px;
  margin-bottom: 8px;   /* room for the badge's overhang */
  border-radius: 50%; border: 2px solid #fff;
  background: var(--surface-2);
  box-shadow: 0 1px 6px rgba(0,0,0,.45);
}
.rail-avatar img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; display: block; }
.rail-initial {
  display: flex; align-items: center; justify-content: center;
  width: 100%; height: 100%; border-radius: 50%;
  font-weight: 800; font-size: 20px; color: var(--text-1);
}
.rail-follow {
  position: absolute; left: 50%; bottom: -9px; transform: translateX(-50%);
  width: 20px; height: 20px; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  background: var(--danger); color: #fff; border: 0;
  font-size: 15px; font-weight: 800; line-height: 1; cursor: pointer;
  padding: 0 0 1px;
}
.rail-follow.on { background: var(--accent-2); font-size: 12px; cursor: default; }

/* A rail button: just the icon, centred, transparent, white, drop-shadowed so it reads
   over any post background. Its count is no longer stacked beneath it — it is a corner
   badge on the icon (see .act-count, owner 2026-07-24) — so every button is now simply an
   icon of the same fixed height, which keeps them on one even vertical rhythm with nothing
   to reserve or forget. */
.act {
  position: relative;
  display: flex; align-items: center; justify-content: center;
  background: transparent; border: 0; color: #fff;
  min-width: 46px; height: 44px; padding: 0;
  font: inherit; font-size: 12px; font-weight: 700; cursor: pointer;
  filter: drop-shadow(0 1px 3px rgba(0,0,0,.55));
  transition: transform .15s ease;
}
/* The icon wrapper is the anchor the count badge hangs off — the icon itself, not the
   whole 46px button, so the badge sits on the glyph's corner rather than out in the gap. */
.act-ico { position: relative; display: inline-flex; align-items: center; justify-content: center; }
.act svg, .act [data-heart] { display: block; }
/* The label would fight the icon for width; the icon IS the label. */
.act .act-label { display: none; }

/* The count as a COUNTER, not part of the button: a small pill on the icon's bottom-right
   corner, in the brand accent so it is plainly a badge and not a control (owner,
   2026-07-24). The halo is the base background, so the pill separates cleanly whether it
   sits over the white icon or the post behind it. Hidden at zero via .empty. */
.act-count {
  /* Anchored at the icon's bottom-right and growing DOWN-AND-RIGHT from there (left:54%,
     not right:), so a wide number (128, 999+) extends into open space rather than back
     across the glyph — the icon stays recognisable under its own counter. */
  position: absolute; bottom: -7px; left: 54%;
  min-width: 15px; height: 15px; padding: 0 4px;
  display: inline-flex; align-items: center; justify-content: center;
  border-radius: 999px;
  background: var(--accent); color: #fff;
  font-size: 9px; font-weight: 800; line-height: 1;
  box-shadow: 0 0 0 2px var(--bg);
}
.act-count.empty { display: none; }

.act:active { transform: scale(.9); }
.act[disabled] { opacity: .5; cursor: default; }
/* Liked/active state: the ICON warms up; no chip background — the rail has none. */
.act.on { color: var(--accent); }

/* Long-press label, driven by js/actionbubble.js toggling .show-tip — attr(data-tip)
   as the ::after content means no extra DOM per button. ABOVE the button now: the
   row hugs the screen's bottom edge, so below is off-screen and sideways would land
   on a neighbouring button. A real tooltip element (not [title]) so it can fade
   instead of snapping in on the OS's own delay. */
.act[data-tip]::after {
  content: attr(data-tip);
  position: absolute; bottom: calc(100% + 8px); left: 50%;
  transform: translateX(-50%) translateY(4px);
  background: var(--surface-2); color: var(--text-1); border: 1px solid var(--border);
  padding: 4px 9px; border-radius: 8px;
  font-size: 12px; font-weight: 700; white-space: nowrap;
  opacity: 0; pointer-events: none;
  box-shadow: 0 4px 14px rgba(0,0,0,.3);
  transition: opacity .15s ease, transform .15s ease;
}
.act[data-tip].show-tip::after {
  opacity: 1; transform: translateX(-50%) translateY(0);
}


/* ---- micro-interactions: each fires via js/pulse.js re-adding this one .pop class,
   scoped per button so the same class name plays a different keyframe in each context. */
/* Fires on POINTERDOWN (js/feed.js), not on the click that actually commits the like —
   like comment/share/results below, tapping like closes this slide's action row
   (visibility: hidden, no transition) in that same synchronous click, so a pulse
   added after the fact would never get painted. Starting it on the press means it is
   already mid-animation, and actually visible, by the time the release closes the
   row around it. */
@keyframes heart-pop { 0% { transform: scale(1); } 35% { transform: scale(1.35); } 60% { transform: scale(.92); } 100% { transform: scale(1); } }
.act.like [data-heart].pop { animation: heart-pop .32s cubic-bezier(.34,1.56,.64,1); }

@keyframes comment-pop { 0% { transform: scale(1); } 50% { transform: scale(1.22); } 100% { transform: scale(1); } }
.act.comment svg.pop { animation: comment-pop .28s cubic-bezier(.34,1.56,.64,1); }

@keyframes share-rotate { 0% { transform: rotate(0); } 40% { transform: rotate(-16deg); } 70% { transform: rotate(6deg); } 100% { transform: rotate(0); } }
.act.share svg.pop { animation: share-rotate .3s cubic-bezier(.34,1.56,.64,1); }

/* .foot, after a vote lands or a like is spawned from it — a small nudge, not a
   shake. Both fire ON THE SLIDE now (not a per-card floating wrapper, which no
   longer exists): .foot is the one element that is always present and always
   visible regardless of whether that slide's own action row is open or closed. */
@keyframes toolbar-bounce { 0% { transform: translateY(0); } 35% { transform: translateY(-4px); } 65% { transform: translateY(1px); } 100% { transform: translateY(0); } }
.foot.pop { animation: toolbar-bounce .26s cubic-bezier(.34,1.56,.64,1); }

/* Hold-to-repost (owner, 2026-07-24): press and HOLD the heart, this bar fills, and at
   the top the poll is reposted. Mounted into the heart button (position:relative) and
   floated to its LEFT — the rail hugs the right edge, so the bar grows into the open space
   over the post rather than off-screen. pointer-events:none: it is pure feedback and must
   never intercept the release that a short tap needs to reach the like handler. */
.repost-hold {
  position: absolute; right: calc(100% + 10px); top: 50%; transform: translateY(-50%);
  display: flex; flex-direction: column; align-items: flex-end; gap: 5px;
  pointer-events: none; white-space: nowrap; z-index: 4;
}
.repost-hold-label {
  font-size: 12px; font-weight: 800; color: #fff; text-shadow: 0 1px 3px rgba(0,0,0,.75);
}
.repost-hold-track {
  width: 128px; height: 8px; border-radius: 999px;
  background: rgba(255,255,255,.28); overflow: hidden;
  box-shadow: 0 1px 5px rgba(0,0,0,.5);
}
.repost-hold-fill {
  display: block; height: 100%; width: 0; border-radius: 999px;
  background: var(--accent);
  animation: repost-fill var(--hold, 900ms) linear forwards;
}
@keyframes repost-fill { from { width: 0; } to { width: 100%; } }
/* Reduced motion: no sweep, but the bar still shows full so the gesture is not invisible —
   js/repost.js sets the width inline in that case, and the timer still fires. */
@media (prefers-reduced-motion: reduce) {
  .repost-hold-fill { animation: none; }
}

/* The liked-just-now flourish: a handful of small hearts spawned by js/heartburst.js,
   each with its own random --dx (horizontal drift) and --rot (tilt) so the cascade
   reads as organic rather than N identical copies moving in lockstep. Anchored to
   .foot for the same reason as toolbar-bounce above — whatever spawns these must
   survive that slide's own action row closing around it. forwards keeps the final
   (invisible) frame so js/heartburst.js's own removal — not the animation resetting
   — is what takes it out of the DOM. */
.heart-burst {
  position: absolute; right: 8px; bottom: 8px;
  width: 14px; height: 14px; color: var(--accent);
  pointer-events: none;
  animation: heart-float .9s ease-out forwards;
}
@keyframes heart-float {
  0%   { transform: translate(0, 0) scale(.5) rotate(0deg); opacity: 0; }
  15%  { opacity: 1; transform: translate(0, 0) scale(1) rotate(0deg); }
  100% { transform: translate(var(--dx, 0px), -64px) scale(.85) rotate(var(--rot, 0deg)); opacity: 0; }
}

/* Confetti on a vote (js/celebrate.js). The host is a zero-size point positioned by JS
   at the chosen option's centre — on the SLIDE, not in the option, which is
   overflow:hidden and would clip every spark. Each spark flies out to its own --dx/--dy
   (with a downward gravity bias baked into --dy) and tumbles to its --rot as it fades. */
.confetti { position: absolute; z-index: 5; width: 0; height: 0; pointer-events: none; }
.confetti span {
  position: absolute; left: 0; top: 0; width: 8px; height: 8px; border-radius: 2px;
  transform: translate(-50%, -50%);
  animation: confetti-fly .95s cubic-bezier(.16,.68,.4,1) forwards;
}
@keyframes confetti-fly {
  0%   { transform: translate(-50%, -50%) scale(.3) rotate(0); opacity: 1; }
  20%  { opacity: 1; }
  100% {
    transform: translate(calc(-50% + var(--dx, 0px)), calc(-50% + var(--dy, 0px))) scale(1) rotate(var(--rot, 0deg));
    opacity: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .act[data-tip]::after { transition: opacity .1s linear !important; transform: none !important; }
  .heart-burst, .confetti { display: none; }
  .act.like [data-heart].pop, .act.comment svg.pop, .act.share svg.pop, .foot.pop {
    animation: none !important; transform: none !important;
  }
  /* The answer flourish, off: the bars still fill and the tick still shows, they just
     do not spring or cascade. */
  .opt.just-voted, .quiz-opt.right, .quiz-opt.wrong,
  .opt.mine .tick, .quiz-opt.right .tick, .quiz-opt.wrong .tick {
    animation: none !important; transform: none !important;
  }
  .voted .opts .opt .bar { transition-delay: 0s !important; }
  /* The ambient background stops drifting. The mesh and grain stay — they are colour,
     not motion — so the slide keeps its look without anything moving on screen. */
  .slide-bg::before { animation: none !important; transform: none !important; }
}

/* swipe affordance on the first card only */
.hint {
  position: absolute; left: 0; right: 0;
  bottom: calc(max(78px, calc(env(safe-area-inset-bottom) + 70px)) + var(--navbar-h));
  text-align: center; color: var(--text-2); font-size: 13px;
  animation: bob 1.8s ease-in-out infinite; pointer-events: none;
}
@keyframes bob { 0%,100% { transform: translateY(0); opacity: .75 } 50% { transform: translateY(-6px); opacity: 1 } }

/* Shrunk to brand + bell + dashboard + auth now that + and theme live in .tabbar and
   Settings — a topbar with two fewer controls to scan is itself part of "cleaner". */
.topbar {
  position: fixed; top: 0; left: 0; right: 0; z-index: 10;
  display: flex; align-items: center; justify-content: space-between;
  padding: max(10px, env(safe-area-inset-top)) 12px 10px;
  background: linear-gradient(var(--bg), transparent);
  pointer-events: none;
}
.topbar > * { pointer-events: auto; }
/* The right-hand cluster (bell, dashboard, account) on ONE centreline. As a plain
   inline span its children sat on the text baseline, and the avatar button — shorter
   than the 44px icon buttons — rode visibly low beside the bell (owner's screenshot,
   2026-07-23). Flex centring is the same fix .icon-btn itself got. */
.topbar-actions { display: flex; align-items: center; gap: 2px; }
/* An <a href="/"> now — the wordmark IS the home control since the tab bar's home
   button died. Inherit + no underline keeps it looking like the wordmark it was. */
/* The wordmark became the Meliya logo mark (owner, 2026-07-26): an icon top-left, not the
   internal "MaxPoll" name. A compact mark also frees the width the three feed tabs need. */
.brand { display: inline-flex; align-items: center; color: inherit; text-decoration: none; }
.brand-mark { display: block; width: 32px; height: 32px; }
/* ---- the bottom chrome: one transparent bar ---------------------------- */
/* The version label, small enough to ignore, always there when you need to answer
   "did my change actually ship?". Filled in from /health by js/version.js. It lives
   in the bottom bar's LEFT cell now — the separate .verbar strip is gone. */
.ver {
  font-weight: 600; font-size: 8px; letter-spacing: 0.03em;
  color: var(--text-2); pointer-events: auto;
}
/* inline-flex centering, not inline flow: an inline SVG sits on the text baseline,
   which leaves the descender gap under it — every icon rode ~3px high of its
   button's true centre (measured on the tab bar), worst on the create button where
   a 24px glyph made the offset visible at a glance. */
.icon-btn {
  display: inline-flex; align-items: center; justify-content: center;
  background: transparent; border: 0; color: var(--text-1); font: inherit;
  font-size: 20px; padding: 8px 10px; min-height: 44px; min-width: 44px; cursor: pointer; border-radius: 12px;
}
/* The avatar-upload <label> lesson again (see index.html): any author `display` beats
   the UA's [hidden] rule, so the flex above would quietly reveal every icon button
   that starts hidden — #dashboard flashed into view for anonymous readers the moment
   the flex landed, and the e2e suite caught it. */
.icon-btn[hidden] { display: none; }
.icon-btn:active { background: var(--surface); }

/* The bottom bar: version label left, create button centred — a TRANSPARENT overlay
   (product owner, 2026-07-23), not a solid strip. The feed reads straight through it,
   like the rail; what marks it as chrome is only what it holds.

   Home is GONE (same review: "the home button is useless") — the brand wordmark in
   the topbar is the home control — and the bell went back to the topbar when the bar
   turned transparent: a lone bell floating over the feed's corner read as clutter,
   not as a bar.

   pointer-events: none on the bar, auto on its children — the empty cells of a
   transparent overlay must never swallow a tap meant for the option pill or the rail
   behind them (the same arrangement .topbar already uses).

   A 3-column grid, not flex: the create button belongs at the exact centre of the
   SCREEN with unequal neighbours (the version label has real width, the right cell
   is empty), and 1fr/auto/1fr holds that without absolute positioning. align-items
   centres each cell's content above the safe-area padding.

   Signed OUT only the version line renders, so --navbar-h shrinks to just clear it:
   every bottom-anchored element (slides, rail, hint, toast) tracks through the one
   variable. */
.tabbar {
  position: fixed; left: 0; right: 0; bottom: 0; z-index: 10;
  display: grid; grid-template-columns: 1fr auto 1fr; align-items: center;
  padding-inline: 14px;
  height: calc(var(--navbar-h) + env(safe-area-inset-bottom));
  padding-bottom: env(safe-area-inset-bottom);
  pointer-events: none;
}
.tabbar > * { pointer-events: auto; }
html:not(.signed-in) { --navbar-h: 26px; }
/* Pinned to the extreme bottom-left corner (owner, 2026-07-26) rather than centred in the
   bar's left cell, where a quiz's "Next question" button overlapped it. Absolute inside the
   fixed .tabbar, tucked just above the home-indicator safe area, and out of the grid so the
   centred create button is unaffected. Non-interactive — it is a status line, not a control,
   so it never eats a tap meant for the content beneath it. */
.tabbar .ver {
  position: absolute; left: 6px; bottom: calc(env(safe-area-inset-bottom) + 2px);
  opacity: 0.75; pointer-events: none;
}
/* The create button reads as the primary action — a filled circle at the bar's
   centre, the same visual weight a reels app gives its own compose control, sized
   down from 52px now that it floats over content instead of sitting on a strip.
   padding: 0 kills .icon-btn's 8px/10px, which was pushing the glyph off-centre
   inside the fixed circle; the flex centring comes from .icon-btn itself. The
   tinted shadow lifts it off the feed without a hard outline. */
.tabbar-create {
  grid-column: 2;
  width: 44px; height: 44px; min-width: 44px; min-height: 44px;
  padding: 0; border-radius: 50%;
  background: var(--accent); color: #fff;
  box-shadow: 0 3px 12px color-mix(in srgb, var(--accent) 45%, transparent);
}
.tabbar-create svg { width: 20px; height: 20px; }
.tabbar-create:active {
  background: color-mix(in srgb, var(--accent) 80%, black);
  transform: scale(.94);
}

.state { display: grid; place-content: center; height: 100dvh; text-align: center; padding: 24px; gap: 8px; }
.state h2 { margin: 0; font-size: 20px; }
.state p { margin: 0; color: var(--text-2); }
.spinner {
  width: 30px; height: 30px; border: 3px solid var(--border); border-top-color: var(--accent);
  border-radius: 50%; animation: spin .8s linear infinite; margin: 0 auto 6px;
}
@keyframes spin { to { transform: rotate(360deg) } }

/* The loading splash (owner, 2026-07-26): an animated intro focused on the brand — the Meliya
   icon (two faces facing off) pops in with a glow, then the name and a fresh encouraging
   line rise under it. A small branded moment every open. Disabled under reduced motion. */
.loading-splash .load-inner { display: grid; justify-items: center; gap: 2px; }
.loading-splash .load-logo { margin-bottom: 4px; }
.load-logo-mark {
  display: inline-block;   /* transparent art — its neon glow is baked in, no tile/shadow */
  /* Pop in once, then keep gently breathing + glowing so the wait feels alive rather than
     frozen behind a spinner. The breathe is delayed until the pop finishes. */
  animation: ace-pop .55s cubic-bezier(.2, .8, .2, 1) both,
             ace-breathe 3.2s .55s ease-in-out infinite;
}
.load-name {
  font-size: 30px; font-weight: 800; letter-spacing: -0.02em; color: var(--text-1);
  animation: ace-rise .5s .55s both;
}
.load-quote {
  margin: 10px auto 0; max-width: 300px; min-height: 2.9em; font-size: 15px; font-weight: 600;
  color: var(--text-2); line-height: 1.45; animation: ace-rise .5s .75s both;
}
/* Each rotated line fades up in place (set as an inline animation by nextQuote). */
@keyframes ace-quote-fade { from { opacity: 0; transform: translateY(6px) } to { opacity: 1; transform: none } }
@keyframes ace-pop { from { opacity: 0; transform: scale(.7) } to { opacity: 1; transform: none } }
@keyframes ace-rise { from { opacity: 0; transform: translateY(9px) } to { opacity: 1; transform: none } }
@keyframes ace-breathe {
  0%, 100% { transform: none; filter: none; }
  50% { transform: scale(1.06); filter: drop-shadow(0 0 12px color-mix(in srgb, var(--accent) 55%, transparent)); }
}
@media (prefers-reduced-motion: reduce) {
  .load-logo-mark, .load-name, .load-quote { animation: none }
}

.toast {
  position: fixed; left: 50%;
  bottom: calc(max(26px, env(safe-area-inset-bottom)) + var(--navbar-h));
  transform: translate(-50%, 20px); opacity: 0; pointer-events: none;
  background: var(--surface); border: 1px solid var(--border); color: var(--text-1);
  padding: 12px 18px; border-radius: 999px; font-size: 14px; font-weight: 600;
  transition: opacity .2s ease, transform .2s ease; z-index: 50;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

dialog {
  border: 1px solid var(--border); border-radius: 20px; padding: 0;
  background: var(--surface); color: var(--text-1);
  width: min(520px, calc(100vw - 28px));
}
dialog::backdrop { background: rgba(0,0,0,.62); backdrop-filter: blur(2px); }

/* First-run interest picker (owner, 2026-07-27): a welcoming full-ish panel of the same
   .pref-chips the Settings picker uses, shown once to a new signup. */
.onboarding .onboarding-body { padding: 24px; max-height: min(82vh, 640px); overflow-y: auto; }
.onboarding h2 { margin: 0 0 4px; font-size: 22px; }
.onboarding .dim { margin: 0 0 16px; font-size: 14px; line-height: 1.5; color: var(--text-2); }
.onboarding .settings-block-label { margin: 16px 0 8px; }
.onboarding-actions { display: flex; flex-direction: column; align-items: center; gap: 10px; margin-top: 24px; }
.onboarding-actions .btn.primary { width: 100%; padding: 13px; font-size: 16px; }

/* The bottom sheet (2026-07-24): a panel that slides up from the bottom edge instead of a
   centred modal — the app's replacement for pop-ups. Fixed and full-height so its own
   scrim catches an outside tap, but the PANEL hugs the bottom and animates in from below. */
/* Tap-to-explain: a lesson paragraph/section is a tap target for the AI tutor. */
.lesson-hero {
  width: 100%; aspect-ratio: 16 / 9; object-fit: cover;
  border-radius: 14px; margin: 0 0 14px; display: block; background: var(--surface-2);
}
.lesson-taphint {
  margin: 0 0 14px; font-size: 13px; color: var(--accent);
  background: color-mix(in srgb, var(--accent) 10%, transparent);
  border-radius: 10px; padding: 8px 12px;
}
/* The lesson READING material is non-selectable: on iOS a long-press (our press-and-hold-to-explain
   gesture) otherwise triggers the system text selection + "Look Up" dictionary bar, which hijacks the
   gesture before our tutor can open. Disabling selection + the touch callout lets press-and-hold reach
   our own handler cleanly. The chat input stays selectable/editable (exempted below). */
.lesson .lp-textbook, .lesson .lesson-funfact, .lesson .lesson-realworld {
  -webkit-user-select: none; user-select: none; -webkit-touch-callout: none;
}
.lesson input, .lesson textarea, .lesson [contenteditable] {
  -webkit-user-select: text; user-select: text; -webkit-touch-callout: default;
}
.lp-textbook .lesson-intro,
.lp-textbook .lesson-section { cursor: pointer; border-radius: 10px; transition: background .12s ease; }
.lp-textbook .lesson-intro:hover,
.lp-textbook .lesson-section:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); }
.lp-textbook .lesson-section { padding: 4px 8px; margin-left: -8px; margin-right: -8px; }

/* The explain sheet (its own structure — not the .bottom-sheet component). */
.explain-sheet { position: fixed; inset: 0; z-index: 60; display: flex; align-items: flex-end; justify-content: center; }
.explain-sheet[hidden] { display: none; }
.explain-backdrop { position: absolute; inset: 0; background: rgba(0, 0, 0, .45); }
.explain-card {
  position: relative; width: 100%; max-width: 560px;
  background: var(--surface); color: var(--text-1);
  border: 1px solid var(--border); border-bottom: 0; border-radius: 20px 20px 0 0;
  padding: 20px 20px calc(20px + env(safe-area-inset-bottom));
  max-height: 80dvh; overflow-y: auto;
}
.explain-close {
  position: absolute; top: 10px; right: 12px; width: 32px; height: 32px;
  border: 0; background: transparent; color: var(--text-2); font-size: 24px; line-height: 1; cursor: pointer;
}
.explain-sel { margin: 4px 34px 12px 0; font-size: 13px; color: var(--text-2); font-style: italic; }
.explain-body { margin: 0 0 16px; min-height: 40px; }
.explain-answer { font-size: 16px; line-height: 1.55; }
.explain-hint { color: var(--text-2); font-size: 14px; margin: 0; }
.explain-ai { color: var(--text-2); font-size: 12px; margin: 10px 0 0; }
.explain-actions { display: flex; flex-direction: column; gap: 10px; }
.explain-ask { display: flex; gap: 8px; }
.explain-ask input {
  flex: 1; min-width: 0; padding: 10px 12px; border-radius: 10px;
  border: 1px solid var(--border); background: var(--bg); color: var(--text-1); font-size: 15px;
}

.bottom-sheet {
  position: fixed; inset: 0; z-index: 40;
  display: flex; align-items: flex-end; justify-content: center;
}
.bottom-sheet[hidden] { display: none; }
/* While closing (the .open class is gone but the element is not yet hidden for the
   slide-down), the full-screen container must NOT keep eating taps/swipes meant for the
   page beneath it — only an OPEN sheet's scrim is interactive. */
.bottom-sheet:not(.open) { pointer-events: none; }
.bottom-sheet.open { pointer-events: auto; }
.bottom-sheet-scrim {
  position: absolute; inset: 0; background: rgba(0, 0, 0, .4);
  opacity: 0; transition: opacity .25s ease;
}
.bottom-sheet.open .bottom-sheet-scrim { opacity: 1; }
.bottom-sheet-panel {
  position: relative; width: 100%; max-width: 560px;
  background: var(--surface); color: var(--text-1);
  border: 1px solid var(--border); border-bottom: 0; border-radius: 20px 20px 0 0;
  padding: 8px 20px calc(20px + env(safe-area-inset-bottom));
  max-height: 85dvh; overflow-y: auto;
  transform: translateY(100%);
  transition: transform .28s cubic-bezier(.22, .61, .36, 1);
}
.bottom-sheet.open .bottom-sheet-panel { transform: translateY(0); }
.bottom-sheet-panel h2 { margin: 0 0 4px; font-size: 20px; }
.bottom-sheet-panel p.sub { margin: 0 0 18px; color: var(--text-2); font-size: 14px; }
/* The drag grip: a pure affordance saying "this came from the bottom and goes back there". */
.sheet-grip { width: 40px; height: 4px; border-radius: 999px; background: var(--border); margin: 4px auto 14px; }
@media (prefers-reduced-motion: reduce) {
  .bottom-sheet-scrim, .bottom-sheet-panel { transition: none; }
}
.dlg { padding: 22px; }
.dlg h2 { margin: 0 0 4px; font-size: 20px; }
.dlg p.sub { margin: 0 0 18px; color: var(--text-2); font-size: 14px; }
label { display: block; font-size: 12px; color: var(--text-2); margin: 0 0 6px; font-weight: 700; text-transform: uppercase; letter-spacing: .05em; }
input[type=text], input[type=email], input[type=password], input[type=number], .dlg select {
  font: inherit; width: 100%; padding: 13px 14px; margin-bottom: 10px;
  border-radius: 12px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text-1); min-height: 48px;
}
input[type=text]:focus-visible, input[type=email]:focus-visible, input[type=password]:focus-visible,
input[type=number]:focus-visible, .dlg select:focus-visible {
  outline: 2px solid var(--accent); outline-offset: 1px;
}
.dlg-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 12px; }
.btn { font: inherit; font-weight: 700; cursor: pointer; border-radius: 12px; border: 1px solid var(--border);
       background: var(--surface-2); color: var(--text-1); padding: 12px 18px; min-height: 48px; }
.btn.primary { background: var(--accent); border-color: transparent; color: #fff; }
.dlg-err { color: var(--danger); font-size: 14px; margin: 0 0 10px; }

/* ---- sign-in / create account ------------------------------------------ */
.link-btn {
  font: inherit; font-size: 13px; font-weight: 600; color: var(--accent);
  background: none; border: 0; padding: 0; cursor: pointer; text-decoration: underline;
}
.dlg-divider {
  display: flex; align-items: center; gap: 10px; margin: 16px 0 10px;
  color: var(--text-2); font-size: 12px; text-transform: uppercase; letter-spacing: .05em;
}
.dlg-divider::before, .dlg-divider::after { content: ''; flex: 1; height: 1px; background: var(--border); }
.signin-providers { display: flex; flex-direction: column; gap: 8px; margin-bottom: 4px; }
.signin-providers .btn { width: 100%; }
/* Google leads: a full-width, prominent first tap. */
.signin-providers .provider-google { font-size: 15px; }

/* The friendly one-liner under each heading, and the small helper hints (password rule, resend). */
.signin-sub { margin: 0 0 16px; color: var(--text-2); font-size: 14px; line-height: 1.4; }
.signin-hint { margin: -4px 0 12px; color: var(--text-2); font-size: 13px; line-height: 1.4; }
.signin-hint .link-btn { font-size: 13px; }

/* Privacy: the sign-up agreement checkbox and the blocking accept-to-continue gate. */
.signin-agree { display: flex; gap: 10px; align-items: flex-start; font-size: 13px; line-height: 1.4; margin: 6px 0 12px; color: var(--text-1); }
.signin-agree input { margin-top: 3px; flex: none; }
.privacy-gate { border: 0; border-radius: 16px; padding: 0; max-width: 440px; }
.privacy-gate::backdrop { background: rgba(0, 0, 0, 0.55); }

/* The trial disclaimer, shown on every app open (chrome/trial-notice.js). A warm, unmissable modal. */
.trial-notice {
  border: 0; border-radius: 18px; padding: 0; max-width: 420px; width: calc(100% - 32px);
  background: var(--surface); color: var(--text-1); box-shadow: 0 18px 60px rgba(0, 0, 0, 0.35);
}
.trial-notice::backdrop { background: rgba(0, 0, 0, 0.6); }
.trial-notice-inner { padding: 28px 24px 24px; text-align: center; }
.trial-notice-badge { font-size: 44px; line-height: 1; margin-bottom: 8px; }
.trial-notice-title { font-family: var(--font-display, inherit); font-size: 24px; margin: 0 0 8px; }
.trial-notice-body { color: var(--text-2); font-size: 15px; line-height: 1.5; margin: 0 0 20px; }
.trial-notice-ok {
  width: 100%; padding: 14px 20px; font-size: 16px; font-weight: 700; cursor: pointer;
  color: #fff; border: 0; border-radius: 999px; background: var(--accent);
}
.trial-notice-ok:active { transform: translateY(1px); }

/* Birth date (sign-up only): month select + year input side by side, year taking the slack. */
.signin-birth-row { display: flex; gap: 8px; }
.signin-birth-row select { flex: 1 1 auto; min-width: 0; }
.signin-birth-row input { flex: 0 0 96px; width: 96px; }

/* The post-register "check your inbox" panel — a warm, unmissable confirmation. */
.signin-sent { text-align: center; }
.signin-sent h2 { margin: 6px 0 8px; }
.signin-sent .signin-sub { margin-bottom: 12px; }
.signin-sent .dlg-actions { justify-content: center; }
.signin-sent-mark {
  font-size: 44px; line-height: 1; margin: 6px 0 4px;
}

/* ---- linked accounts (the profile's "signed in with" section) ---------- */
.linked-accounts { display: flex; flex-direction: column; gap: 6px; }
.linked-row {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 0; border-bottom: 1px solid var(--border); font-size: 14px;
}
.linked-row:last-child { border-bottom: 0; }
.chip.linked { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); }

/* ---- the profile picture picker ----------------------------------------- */
.avatar-section { margin-bottom: 18px; }
.avatar-current { display: flex; align-items: center; gap: 14px; flex-wrap: wrap; }
.avatar-preview {
  width: 64px; height: 64px; border-radius: 999px; object-fit: cover; flex: 0 0 auto;
  border: 1px solid var(--border); background: var(--surface-2);
}
.avatar-preview.letter {
  display: flex; align-items: center; justify-content: center;
  font-size: 24px; font-weight: 800; color: var(--text-1);
}
.avatar-actions { display: flex; flex-wrap: wrap; gap: 8px; }
.avatar-gallery-picker {
  margin-top: 12px; display: grid; grid-template-columns: repeat(auto-fill, minmax(56px, 1fr));
  gap: 8px; max-height: 220px; overflow-y: auto; padding: 2px;
}
.avatar-gallery-picker[hidden] { display: none; }
.avatar-gallery-item {
  width: 100%; aspect-ratio: 1; border-radius: 12px; padding: 0; cursor: pointer;
  border: 2px solid var(--border); background: var(--surface-2); overflow: hidden;
}
.avatar-gallery-item img { width: 100%; height: 100%; object-fit: cover; display: block; }
.avatar-gallery-item:hover, .avatar-gallery-item:focus-visible { border-color: var(--accent); }

/* ---- identity in the topbar ------------------------------------------- */
.auth-slot { position: relative; display: inline-flex; align-items: center; vertical-align: middle; }
.btn-sm {
  font: inherit; font-size: 14px; font-weight: 700; cursor: pointer;
  background: var(--surface); border: 1px solid var(--border); color: var(--text-1);
  border-radius: 999px; padding: 9px 14px; min-height: 40px;
}
.who { background: none; border: 0; padding: 2px; cursor: pointer; line-height: 0; border-radius: 999px; }
.who:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.avatar {
  width: 32px; height: 32px; border-radius: 999px; object-fit: cover;
  border: 1px solid var(--border); background: var(--surface-2); display: inline-block;
}
.avatar.letter {
  display: inline-flex; align-items: center; justify-content: center;
  font-size: 14px; font-weight: 800; color: var(--text-1); line-height: 1;
}
.avatar.sm { width: 26px; height: 26px; font-size: 12px; flex: 0 0 auto; }
.avatar.lg { width: 88px; height: 88px; font-size: 32px; }

.menu {
  position: absolute; top: calc(100% + 8px); right: 0; z-index: 30; min-width: 200px;
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
  padding: 6px; box-shadow: 0 10px 30px rgba(0,0,0,.35);
}
.menu-who { display: grid; gap: 2px; padding: 10px 12px 12px; border-bottom: 1px solid var(--border); }
.menu-who strong { font-size: 14px; }
.menu-who span { font-size: 12px; color: var(--text-2); word-break: break-all; }
.menu-item {
  display: block; width: 100%; text-align: left; font: inherit; font-size: 14px; font-weight: 600;
  background: none; border: 0; color: var(--text-1); padding: 12px; min-height: 44px;
  border-radius: 10px; cursor: pointer;
}
.menu-item:hover, .menu-item:focus-visible { background: var(--surface-2); }
.menu-item.danger { color: var(--danger); }

/* ---- the dashboard overlay -------------------------------------------- */
/* A dialog, not a route: staff moderate without leaving the feed they came from. */
dialog.admin {
  width: min(1040px, calc(100vw - 20px)); max-width: none;
  height: min(88dvh, 900px); padding: 0;
  display: none; grid-template-rows: auto auto 1fr;
}
dialog.admin[open] { display: grid; }
.admin-head {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
  padding: 14px 16px; border-bottom: 1px solid var(--border);
}
.admin-head h2 { margin: 0; font-size: 18px; flex: 0 0 auto; }
.admin-head .icon-btn { margin-left: auto; }
.tabs { display: flex; gap: 6px; }
.tab {
  font: inherit; font-size: 14px; font-weight: 700; cursor: pointer;
  background: transparent; border: 1px solid transparent; color: var(--text-2);
  border-radius: 999px; padding: 9px 14px; min-height: 40px;
}
.tab.on { background: var(--surface-2); border-color: var(--border); color: var(--text-1); }

.admin-err {
  margin: 0; padding: 12px 16px; font-size: 14px; font-weight: 600;
  color: var(--danger); background: color-mix(in srgb, var(--danger) 12%, transparent);
  border-bottom: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
}
.panel { min-height: 0; overflow-y: auto; padding: 14px 16px 20px; display: flex; flex-direction: column; gap: 14px; }
.panel[hidden] { display: none; }
/* flex: none is load-bearing. overflow-x:auto silently computes overflow-y to auto
   too, so as a shrinkable flex child this became its own scroll container and clipped
   every agent card after the first — visible only in a screenshot, never in a DOM
   assertion. The PANEL scrolls; this only ever scrolls sideways for a wide table. */
.scroller { overflow-x: auto; flex: 0 0 auto; }
.note {
  margin: 0; font-size: 13px; line-height: 1.5; color: var(--text-2);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 12px; padding: 12px 14px;
}
#user-search {
  font: inherit; width: 100%; padding: 12px 14px; min-height: 46px;
  border-radius: 12px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text-1);
}
#user-search:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

.grid { width: 100%; border-collapse: collapse; font-size: 14px; }
.grid th {
  text-align: left; font-size: 11px; letter-spacing: .06em; text-transform: uppercase;
  color: var(--text-2); font-weight: 700; padding: 0 10px 8px; white-space: nowrap;
}
.grid td { padding: 10px; border-top: 1px solid var(--border); vertical-align: middle; }
.who-cell { display: flex; align-items: center; gap: 10px; min-width: 190px; }
.who-text { display: grid; gap: 1px; min-width: 0; }
.who-text strong { display: flex; align-items: center; gap: 6px; font-size: 14px; }
.dim { color: var(--text-2); font-size: 13px; word-break: break-word; }
.controls { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; }

.chip {
  display: inline-block; font-size: 11px; font-weight: 700; letter-spacing: .04em;
  color: var(--text-2); background: var(--surface-2); border: 1px solid var(--border);
  padding: 4px 9px; border-radius: 999px; margin: 2px 4px 2px 0; white-space: nowrap;
}
.chip.role { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
.chip.service, .chip.built { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); }
.chip.st-warned, .chip.st-suspended { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 42%, transparent); }
.chip.st-terminated { color: #fff; background: var(--danger); border-color: transparent; }
.chip.st-modified { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); }

/* AI-instructions editor (admin "AI prompts" tab): one card per agent prompt. */
.prompt-group { margin: 8px 0 0; font-size: 13px; text-transform: uppercase; letter-spacing: .04em; color: var(--text-2); }
.prompt-row { border: 1px solid var(--border); border-radius: 12px; padding: 12px; display: flex; flex-direction: column; gap: 8px; }
.prompt-head { display: flex; align-items: center; gap: 8px; }
.prompt-desc { margin: 0; font-size: 13px; }
.prompt-text {
  width: 100%; box-sizing: border-box; resize: vertical; min-height: 90px; padding: 10px;
  font: 13px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace;
  border: 1px solid var(--border); border-radius: 8px; background: var(--surface-2); color: var(--text-1);
}
.prompt-actions { gap: 8px; }
/* Inline SVG, not a padlock glyph: ＋/◐/🔒 render as tofu boxes on Android. */
.lock { display: inline-flex; color: var(--text-2); cursor: help; }
.muted-note { font-size: 12px; color: var(--text-2); }

.toggle {
  font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-1);
  border-radius: 999px; padding: 8px 12px; min-height: 40px; white-space: nowrap;
}
.toggle.on { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 50%, transparent); }
.toggle.danger { color: var(--danger); }
/* Disabled means the server would refuse it — say so rather than look broken. */
.toggle[disabled], .status-select[disabled] { opacity: .45; cursor: not-allowed; }
.status-select {
  font: inherit; font-size: 13px; font-weight: 600; min-height: 40px; padding: 8px 10px;
  border-radius: 10px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text-1); cursor: pointer;
}

.agent {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 14px; padding: 14px; margin-bottom: 10px;
}
.agent header { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 6px; }
.agent p { margin: 0 0 10px; font-size: 13px; }
.kv { display: grid; grid-template-columns: auto 1fr; gap: 4px 12px; margin: 0 0 12px; font-size: 13px; }
.kv dt { color: var(--text-2); font-weight: 700; }
.kv dd { margin: 0; word-break: break-all; }
.agent footer { display: flex; gap: 8px; flex-wrap: wrap; }

.agent-form { border-top: 1px solid var(--border); padding-top: 16px; }
.agent-form h3 { margin: 0 0 12px; font-size: 15px; }
.agent-form .row { display: flex; gap: 10px; flex-wrap: wrap; }
.agent-form .row > span { flex: 1 1 220px; min-width: 0; }
.agent-form select {
  font: inherit; width: 100%; padding: 13px 14px; margin-bottom: 10px; min-height: 48px;
  border-radius: 12px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text-1);
}
.check { display: flex; align-items: center; gap: 8px; margin: 0 auto 0 0;
         font-size: 13px; text-transform: none; letter-spacing: 0; color: var(--text-1); }
.check input { width: 18px; height: 18px; accent-color: var(--accent); }

/* ---- the review queue -------------------------------------------------- */
/* The tab badge: a count, never a zero. Nothing waiting means no badge at all, so the
   presence of the pill is itself the signal and the number only says how much. */
.badge-count {
  display: inline-block; margin-left: 6px; padding: 1px 7px; min-width: 20px;
  font-size: 11px; font-weight: 800; line-height: 18px; text-align: center;
  border-radius: 999px; background: var(--danger); color: #fff;
}
.badge-count[hidden] { display: none; }

.review-item {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 14px; padding: 14px; margin-bottom: 12px;
}
.review-item header { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 10px; }

/* The submission verbatim, and visibly quoted: a moderator must never be unsure which
   words are the author's and which are the product's. */
.sub-content {
  border-left: 3px solid var(--border); padding: 2px 0 2px 12px; margin-bottom: 10px;
}
.sub-content .q { margin: 0 0 8px; font-size: 15px; font-weight: 700; line-height: 1.35; }
.sub-content .opts { margin: 0; padding-left: 18px; font-size: 13px; color: var(--text-2); }
.sub-content .opts li { margin: 2px 0; }
/* In a review card the held comment IS the submission, so it reads at the same weight as
   a poll's question rather than at the smaller size it takes inside the comment sheet.
   It is the thing being judged, not a footnote to the poll it replies to. */
.sub-content .comment-body { margin: 0 0 6px; font-size: 15px; color: var(--text-1); }
.review-item .author { margin: 0 0 10px; }

.verdict {
  background: var(--surface); border: 1px solid var(--border);
  border-radius: 12px; padding: 10px 12px; margin-bottom: 12px;
}
.verdict-chips { display: flex; flex-wrap: wrap; gap: 2px; margin-bottom: 8px; }
/* The model's own sentence, unedited — R12.5: it has returned a reason contradicting
   its own decision, and only reading it catches that. */
.verdict .reason {
  margin: 0 0 6px; padding: 0 0 0 10px; font-size: 13px; line-height: 1.5;
  border-left: 2px solid var(--accent); color: var(--text-1);
}
.verdict .dim { margin: 0; }

/* Colour AND words, always paired — see --warn above. */
.chip.sev-high { color: #fff; background: var(--danger); border-color: transparent; }
.chip.sev-medium { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 50%, transparent); }
.chip.sev-low { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); }
.chip.dec-reject { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 42%, transparent); }
.chip.dec-approve { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); }
.chip.cat { color: var(--text-2); }

.age { font-size: 12px; color: var(--text-2); }
/* An item past the target says so in words too, for the same reason the severity does. */
.age.overdue { color: var(--danger); font-weight: 700; }

.held { margin: 0; font-size: 13px; font-weight: 600; color: var(--danger); }
.held.mine { color: var(--accent-2); font-weight: 600; margin-bottom: 8px; }

.decide { display: block; }
.decide label { margin-bottom: 6px; }
.reason-input {
  font: inherit; font-size: 14px; width: 100%; padding: 10px 12px; resize: vertical;
  border-radius: 12px; border: 1px solid var(--border);
  background: var(--surface); color: var(--text-1);
}
.reason-input:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.decide-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 10px; }

/* An empty queue is the GOOD state, so it is not styled as an error or an absence. */
.empty-good {
  margin: 0; padding: 22px 16px; text-align: center; font-size: 14px; font-weight: 600;
  color: var(--accent-2); background: var(--surface-2);
  border: 1px solid color-mix(in srgb, var(--accent-2) 30%, transparent); border-radius: 14px;
}

/* ---- the Learning review tab (AI lessons + templates) ------------------ */
.lrn-group { margin-bottom: 22px; }
.lrn-group-head { display: flex; align-items: center; gap: 8px; margin: 0 0 12px; font-size: 16px; }
.lrn-card {
  background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
  padding: 14px 16px; margin-bottom: 12px;
}
.lrn-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; }
.lrn-note { margin: 8px 0 0; font-size: 13px; color: var(--warn); }
.lrn-detail { margin-top: 14px; border-top: 1px solid var(--border); padding-top: 14px; }
.lrn-sub { margin: 16px 0 8px; font-size: 14px; color: var(--text-2); text-transform: uppercase; letter-spacing: .04em; }
.lrn-material .lrn-msec { margin: 0 0 12px; }
.lrn-material .lrn-msec strong { display: block; margin-bottom: 2px; }
.lrn-refs { margin: 0; padding-left: 20px; }
.lrn-quiz { margin: 0; padding-left: 22px; display: flex; flex-direction: column; gap: 12px; }
.lrn-q { font-weight: 600; margin-bottom: 6px; }
.lrn-choices { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 5px; }
.lrn-choice { padding: 7px 10px; border: 1px solid var(--border); border-radius: 10px; font-size: 14px; }
.lrn-choice.is-correct { border-color: color-mix(in srgb, var(--accent-2) 55%, transparent); background: color-mix(in srgb, var(--accent-2) 10%, transparent); }
.lrn-sample { margin: 8px 0; }
.lrn-actions { margin-top: 14px; display: flex; flex-direction: column; gap: 10px; }
.lrn-notes {
  width: 100%; box-sizing: border-box; resize: vertical; padding: 8px 10px;
  border: 1px solid var(--border); border-radius: 10px; background: var(--bg); color: var(--text-1); font: inherit;
}
.lrn-notes-inline {
  flex: 1; min-width: 120px; padding: 7px 10px; border: 1px solid var(--border);
  border-radius: 10px; background: var(--bg); color: var(--text-1); font: inherit;
}
.chip.kind-revised { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 50%, transparent); }

/* Owner-only account hiding (dashboard). A hidden row is shown dimmed to the owner, with a chip. */
.chip.hidden-user { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 45%, transparent); }
tr.is-hidden-user { opacity: .6; }

/* the inline lesson editor */
.led { display: flex; flex-direction: column; gap: 8px; }
.led-lab { font-size: 13px; color: var(--text-2); }
.led-block, .led-ref, .led-q { border: 1px solid var(--border); border-radius: 10px; padding: 10px; margin: 0; display: flex; flex-direction: column; gap: 6px; }
.led input, .led textarea {
  width: 100%; box-sizing: border-box; padding: 7px 9px; border: 1px solid var(--border);
  border-radius: 8px; background: var(--bg); color: var(--text-1); font: inherit;
}
.led-choice { display: flex; align-items: center; gap: 8px; }
.led-choice .led-choice-text { flex: 2; }
.led-choice .led-choice-exp { flex: 3; }
.led-correct { display: flex; align-items: center; gap: 4px; white-space: nowrap; font-size: 13px; color: var(--text-2); }
.led-correct input { width: auto; }

/* ---- the notification inbox -------------------------------------------- */
/* Narrower than the dashboard: this is a reading column, not a table of controls. */
dialog.inbox {
  width: min(560px, calc(100vw - 20px)); max-width: none;
  height: min(80dvh, 780px); padding: 0;
  display: none; grid-template-rows: auto auto 1fr;
}
dialog.inbox[open] { display: grid; }
/* The bell has to be a positioning context for its badge, and the badge sits ON it
   rather than beside it — the topbar has no room to grow by a pill. */
.icon-btn.bell { position: relative; overflow: visible; }
.icon-btn.bell .badge-count {
  position: absolute; top: 2px; right: 0; margin: 0;
  line-height: 16px; min-width: 17px; padding: 0 5px; font-size: 10px;
  border: 2px solid var(--bg);
}
#inbox-read-all { margin-left: auto; }
/* margin-left:auto on the close button would fight the one above; the head is a flex
   row, so the button after an auto-margined sibling simply follows it. */
dialog.inbox .admin-head .icon-btn { margin-left: 0; }
#inbox-list { display: flex; flex-direction: column; gap: 10px; }

.inbox-item {
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 14px; padding: 12px 14px; cursor: pointer;
}
.inbox-item:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) { .inbox-item:hover { border-color: var(--accent); } }
.inbox-item header { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 8px; }
.inbox-item .when { font-size: 12px; color: var(--text-2); margin-left: auto; }
.inbox-title { margin: 0; font-size: 15px; line-height: 1.35; color: var(--text-1); }
.inbox-body { margin: 6px 0 0; font-size: 13px; line-height: 1.5; color: var(--text-2); }
.inbox-go { margin: 8px 0 0; font-size: 12px; font-weight: 700; color: var(--accent); }

/* Unread, in THREE non-colour signals plus colour: a marker shape, the word "New", and
   a heavier title with a thicker left edge. Roughly one man in twelve has some
   colour-vision deficiency; delete every colour below and the state still reads. */
.inbox-item.unread { border-left: 4px solid var(--accent); }
.inbox-item.unread .inbox-title { font-weight: 800; }
.inbox-item.read { opacity: .82; }
.inbox-item.read .inbox-title { font-weight: 500; }
.unread-dot {
  width: 9px; height: 9px; border-radius: 50%; background: var(--accent);
  display: inline-block; flex: 0 0 auto;
}
.new-flag {
  font-size: 10px; font-weight: 800; letter-spacing: .08em; text-transform: uppercase;
  color: var(--accent);
}

.chip.kind-moderation_rejected { color: var(--danger); border-color: color-mix(in srgb, var(--danger) 42%, transparent); }
.chip.kind-moderation_approved { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 45%, transparent); }
.chip.kind-result_flipped { color: var(--warn); border-color: color-mix(in srgb, var(--warn) 50%, transparent); }
.chip.kind-milestone, .chip.kind-poll_trending { color: var(--accent); border-color: color-mix(in srgb, var(--accent) 40%, transparent); }
.chip.kind-reposted { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 40%, transparent); }
/* An unrecognised kind from a newer server: neutral, and still labelled in words. */
.chip.kind-unknown { color: var(--text-2); }

/* The rejection notice. The moderator's sentence to the author is the single most
   important thing this screen ever shows, so it is quoted, given the danger edge, and
   rendered WHOLE — deliberately no line-clamp, no max-height and no ellipsis here.
   Truncating a reason turns an explained decision back into an arbitrary one. */
.rejection {
  margin-top: 10px; padding: 10px 12px;
  background: var(--surface); border-radius: 12px;
  border: 1px solid color-mix(in srgb, var(--danger) 30%, transparent);
}
.rejection-head {
  margin: 0 0 6px; font-size: 11px; font-weight: 800;
  letter-spacing: .05em; text-transform: uppercase; color: var(--danger);
}
.rejection-reason {
  margin: 0; padding: 0 0 0 10px; font-size: 14px; line-height: 1.55;
  border-left: 3px solid var(--danger); color: var(--text-1);
  white-space: pre-wrap; overflow-wrap: anywhere;
}

/* ---- comments, likes and reports (Phase 5) ------------------------------ */

/* Account-only controls: rendered for everyone, revealed only once /auth/me has said
   there IS someone. Hidden-by-default rather than added later, so nothing ever flashes
   into view and then disappears for an anonymous reader (R15.0c). */
.acct-only { display: none; }
html.signed-in .acct-only { display: inline-flex; }
/* ...but the rail's report button is ALSO a `.act`, the same fixed-height flex icon its
   like/comment/share siblings are. The inline-flex reveal above shrank it (no fixed
   height), so it sat out of the rhythm underneath share — the "icons in the side are not
   aligned" the owner screenshotted (2026-07-24, signed in only, which is why it hid from
   the anonymous screenshots). Re-assert the button's own layout so a revealed rail button
   is the same shape as an always-on one. */
html.signed-in .act.acct-only { display: flex; }

/* The feed is the scroll container, not the body — so `overflow:hidden` on <body>, the
   usual modal fix, does absolutely nothing here and the feed keeps snapping behind an
   open sheet. Freezing #feed itself is what actually stops it. */
html.sheet-open #feed { overflow: hidden; }

/* The comment sheet. A dialog over the feed, like the inbox: reading the argument under
   a poll should not cost you your place in it. */
dialog.sheet {
  width: min(560px, calc(100vw - 20px)); max-width: none;
  height: min(82dvh, 800px); padding: 0;
  display: none; grid-template-rows: auto auto 1fr auto;
}
dialog.sheet[open] { display: grid; }
dialog.sheet .admin-head .icon-btn { margin-left: auto; }
/* overscroll-behavior: contain is the other half of the scroll-trap fix. Without it a
   flick that reaches the end of the comment list hands the remaining momentum to the
   feed underneath, which then snaps to another poll behind the open sheet. */
#panel-comments { overscroll-behavior: contain; }
#comment-list { display: flex; flex-direction: column; gap: 12px; }

/* The comments PAGE (2026-07-24): a pager view, not a modal. Unlike the other pager pages
   it must NOT scroll as a whole — the list scrolls while the composer stays pinned at the
   bottom, so a reader can always type without hunting for the box. */
.comments-page { overflow: hidden; }
.comments-body {
  flex: 1 1 auto; overflow-y: auto; overscroll-behavior: contain;
  padding: 4px 16px 16px;
}
.comments-page #comment-composer {
  flex: 0 0 auto; background: var(--bg);
  padding-bottom: var(--navbar-h);   /* clear the fixed tab bar overlaying the bottom edge */
}

/* article.comment, NOT bare .comment: the rail's comments button is `.act.comment`,
   and the unscoped selector reached it too — overriding .act's centered column with
   flex-start/10px and shoving that one icon+count hard left of its neighbours. The
   element type is the scope because the two can never converge: a comment row is an
   <article>, the rail control is a <button>. */
article.comment { display: flex; gap: 10px; align-items: flex-start; }
.comment-main { min-width: 0; flex: 1 1 auto; }
.comment header { display: flex; align-items: baseline; gap: 8px; flex-wrap: wrap; }
.comment-author { font-size: 14px; font-weight: 700; overflow-wrap: anywhere; }
.comment .when { font-size: 12px; color: var(--text-2); }
/* A stranger's prose: wrapped hard so a 40-character unbroken string cannot widen the
   sheet, and pre-wrap so their paragraph breaks survive. */
.comment-body {
  margin: 3px 0 0; font-size: 14px; line-height: 1.5; color: var(--text-1);
  white-space: pre-wrap; overflow-wrap: anywhere;
}
.comment.mine .comment-author::after {
  content: 'you'; margin-left: 6px; padding: 1px 6px; border-radius: 999px;
  font-size: 10px; font-weight: 800; letter-spacing: .06em; text-transform: uppercase;
  color: var(--accent); border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent);
}

/* The like / dislike / reply row under a comment (2026-07-24). Quiet text-grey buttons so
   they read as secondary to the comment itself; the active reaction warms to the accent. */
.comment-actions { display: flex; align-items: center; gap: 4px; margin: 6px 0 0; }
.creact {
  display: inline-flex; align-items: center; gap: 5px;
  background: none; border: 0; color: var(--text-2); cursor: pointer;
  font: inherit; font-size: 13px; font-weight: 650; line-height: 1;
  padding: 5px 8px; border-radius: 999px;
}
.creact svg { flex: none; }
.creact:hover, .creact:focus-visible { background: var(--surface-2); color: var(--text-1); }
.creact:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.creact.up.on { color: var(--accent); }
.creact.down.on { color: var(--danger); }
.creact-n { font-variant-numeric: tabular-nums; }
.creact.reply { margin-left: auto; }

/* A reply sits indented under its parent, with a hairline rail marking the thread. */
.comment-replies {
  margin: 10px 0 2px; padding-left: 12px;
  border-left: 2px solid var(--border);
  display: flex; flex-direction: column; gap: 14px;
}

/* The "Replying to X" banner above the composer. */
.reply-banner {
  display: flex; align-items: center; justify-content: space-between; gap: 8px;
  font-size: 13px; color: var(--text-2);
  background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 10px; padding: 6px 10px; margin: 0 0 8px;
}
.reply-banner strong { color: var(--text-1); }
.reply-cancel {
  background: none; border: 0; color: var(--text-2); cursor: pointer;
  font-size: 15px; line-height: 1; padding: 2px 4px; border-radius: 6px;
}
.reply-cancel:hover, .reply-cancel:focus-visible { color: var(--text-1); background: var(--surface); }

/* The report flag: quiet by default — it is a safety valve, not a call to action — and
   only reaches the danger colour on hover or focus. */
.act-flag {
  font: inherit; cursor: pointer; flex: 0 0 auto; line-height: 0;
  background: none; border: 0; color: var(--text-2);
  padding: 6px; margin: -6px -6px 0 0; border-radius: 10px;
  min-width: 34px; min-height: 34px; align-items: center; justify-content: center;
}
.act-flag:hover, .act-flag:focus-visible { color: var(--danger); background: var(--surface-2); }
.act-flag:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }

/* No comments yet is NEUTRAL — not the inbox's green "good" state and not an error.
   Nobody has commented on a new poll yet is simply a fact, and an invitation. */
.empty-comments {
  margin: 0; padding: 26px 16px; text-align: center;
  font-size: 14px; font-weight: 600; color: var(--text-2);
  background: var(--surface-2); border: 1px dashed var(--border); border-radius: 14px;
}

/* The composer, and its signed-out replacement, share the footer row. */
#comment-composer { border-top: 1px solid var(--border); }
.composer { padding: 12px 16px max(12px, env(safe-area-inset-bottom)); }
.composer label { margin-bottom: 6px; }
.composer textarea {
  font: inherit; font-size: 15px; width: 100%; padding: 11px 13px; resize: vertical;
  border-radius: 12px; border: 1px solid var(--border);
  background: var(--surface-2); color: var(--text-1);
}
.composer textarea:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.composer .dlg-actions { margin-top: 8px; }
.composer.signed-out {
  display: flex; align-items: center; gap: 12px; flex-wrap: wrap;
}
.composer-note { margin: 0; flex: 1 1 220px; font-size: 13px; line-height: 1.45; color: var(--text-2); }
.composer.signed-out .btn { flex: 0 0 auto; }

/* The held-for-review notice reuses the error banner's slot but NOT its colour: a
   comment waiting on a moderator is not a failure, and telling an author it is in red
   is its own small lie. */
.admin-err.notice {
  color: var(--text-1);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
  border-bottom-color: color-mix(in srgb, var(--accent) 32%, transparent);
  font-weight: 600;
}

/* The report reasons. Full-height rows, because at 390px a radio dot alone is a 12px
   target and the label is the only thing anyone actually aims at. */
.reasons { display: flex; flex-direction: column; gap: 2px; margin-bottom: 6px; }
.radio {
  display: flex; align-items: center; gap: 10px; cursor: pointer;
  margin: 0; padding: 10px 12px; min-height: 46px; border-radius: 10px;
  font-size: 15px; font-weight: 500; text-transform: none; letter-spacing: 0;
  color: var(--text-1);
}
.radio:hover { background: var(--surface-2); }
.radio input { width: 18px; height: 18px; accent-color: var(--accent); flex: 0 0 auto; }
.radio:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: -2px; }

/* A 390px phone: the users table cannot fit five columns, so it stacks. */
@media (max-width: 640px) {
  /* Full-screen, like the other two sheets. A 82dvh dialog on a phone leaves a strip of
     feed showing at the top that invites a tap on a poll behind the sheet. */
  dialog.sheet { width: 100vw; height: 100dvh; max-height: none; border-radius: 0; border: 0; }
  .foot { gap: 8px; }
  dialog.inbox { width: 100vw; height: 100dvh; max-height: none; border-radius: 0; border: 0; }
  dialog.admin { width: 100vw; height: 100dvh; max-height: none; border-radius: 0; border: 0; }
  .grid thead { display: none; }
  .grid, .grid tbody, .grid tr, .grid td { display: block; width: 100%; }
  .grid tr { border-top: 1px solid var(--border); padding: 12px 0; }
  .grid td { border: 0; padding: 4px 0; }
  /* td.who-cell, not .who-cell: the block rule above is `.grid td`, which outspecifies
     a bare class and would otherwise drop the avatar onto its own line. */
  .grid td.who-cell { display: flex; }
  .controls { padding-top: 6px; }
  /* The tab row wraps rather than scrolls sideways at 390px, and the decision buttons
     go full width so neither can be a half-visible target on a safety-critical screen. */
  .tabs { flex-wrap: wrap; }
  .review-item { padding: 12px; }
  .decide-actions .toggle { flex: 1 1 130px; }
}

/* ---- the profile form: a grid, and three typeahead fields --------------- */
/* Seven fields stacked one per row made a dialog taller than most phone screens, which
   put Save below the fold and made the pairs that belong together (first/last name,
   birth year/month) read as unrelated. NOTE the class is .field-grid, not .grid — .grid
   is already the admin tables in this stylesheet, and its mobile rules force display:
   block, which would silently flatten this. */
/* A flex column so the heading stays put, the fields scroll, and Save is always on
   screen. Measured: at 360x740 the form is ~1150px tall, so on any phone the Save button
   was ~400px below the fold — a form that looks like it cannot be submitted. Two columns
   alone could never fix that, because narrow screens collapse back to one. */
/* No longer a <dialog> (Phase 19c) — the owner's own editor is a plain <section>
   instead of a modal now, which is what actually closes out the account of the dialog
   that would not close. See docs/GOTCHAS.md: the earlier <dialog> version of this rule
   applied `display: flex` UNCONDITIONALLY, without the `[open]` qualifier every other
   dialog variant in this file uses, so it overrode the browser's own
   `dialog:not([open]){display:none}` regardless of specificity — the element rendered
   in normal document flow on every page load, open or not.
   `:not([hidden])` here is that same lesson applied to a plain element: `display` must
   be scoped to the state that means "visible", or [hidden]'s own `display:none` loses
   to this rule the moment the cascade compares them, whatever the specificity.
   `position: fixed` + centering is load-bearing, not decoration: a <dialog> renders
   itself centered in the viewport via the browser's own top-layer regardless of where
   it sits in the document or how far the page has scrolled. A plain element has no such
   behaviour — it renders wherever normal flow puts it, which on a profile page is BELOW
   a full-viewport-height feed, and un-hiding it there left it scrolled off-screen with
   nothing wrong reported by any DOM assertion (only a screenshot caught it, same lesson
   as the flex-item bug elsewhere in this file). Fixed positioning is what makes this
   behave like the dialog it replaced. */
/* Full-screen slide-in pages (2026-07-25 — the app is shedding its pop-ups): Settings and
   the navigation map both slide in from the LEFT, reached by a shortcut or a right-swipe,
   and a left-swipe or the close button send them back. */
.profile-edit-panel, .nav-map-panel {
  position: fixed; inset: 0; z-index: 40;
  background: var(--bg); color: var(--text-1);
  transform: translateX(-100%);
  transition: transform .28s cubic-bezier(.22, .61, .36, 1);
}
.profile-edit-panel[hidden], .nav-map-panel[hidden] { display: none; }
.profile-edit-panel:not([hidden]), .nav-map-panel:not([hidden]) { display: flex; flex-direction: column; }
.profile-edit-panel.open, .nav-map-panel.open { transform: translateX(0); }
@media (prefers-reduced-motion: reduce) {
  .profile-edit-panel, .nav-map-panel { transition: none; }
}

/* ---- the navigation map (2026-07-25) --------------------------------------
   A visual of every page and the swipes/shortcuts between them; each tile is a tap target
   that jumps there. */
.nav-map-body { overflow-y: auto; padding: 8px 16px calc(24px + var(--navbar-h)); }
.nav-section { margin: 18px 0 4px; font-size: 13px; letter-spacing: .06em; text-transform: uppercase; color: var(--text-2); }
.nav-lead { margin: 0 0 12px; }

/* The graph: a pannable/zoomable node-link drawing of the whole app (owner, 2026-07-25).
   The SVG scales to the panel width and never overflows it, so the map fits on a page;
   the ＋／− buttons and drag are for looking closer when a reader wants to. */
.nav-graph {
  position: relative; margin: 4px 0 8px;
  background: var(--bg); border: 1px solid var(--border); border-radius: 16px;
  overflow: hidden;
}
.nav-graph-svg {
  display: block; width: 100%; height: auto;
  touch-action: none;            /* drag pans the graph rather than scrolling the panel */
  cursor: grab;
}
.nav-graph-svg:active { cursor: grabbing; }
#nav-scene { transition: transform .08s linear; }

.nav-zoom {
  position: absolute; top: 8px; right: 8px; z-index: 2;
  display: flex; flex-direction: column; gap: 5px;
}
.nav-zoom button {
  width: 30px; height: 30px; border-radius: 9px; cursor: pointer;
  border: 1px solid var(--border); background: var(--surface-2); color: var(--text-1);
  font-size: 17px; line-height: 1; display: flex; align-items: center; justify-content: center;
}
.nav-zoom button:hover { border-color: var(--accent); }

.nav-node { cursor: pointer; }
.nav-node rect { fill: var(--surface-2); stroke: var(--border); stroke-width: 1.5; transition: stroke .12s ease; }
.nav-node text { fill: var(--text-1); font-size: 15px; font-weight: 700; pointer-events: none; }
.nav-node:hover rect, .nav-node:focus-visible rect { stroke: var(--accent); }
.nav-node:focus-visible { outline: none; }
.nav-node.home rect { fill: color-mix(in srgb, var(--accent) 16%, var(--surface-2)); stroke: var(--accent); stroke-width: 2.2; }

.edge { fill: none; stroke: var(--text-2); stroke-width: 2.2; opacity: .75; }
.edge.swipe { stroke: var(--text-1); opacity: .55; }
.edge.shortcut { stroke: var(--accent); stroke-dasharray: 5 6; opacity: .85; }
#nav-arrow path { fill: var(--accent); }

/* Legend for the two link styles. */
.nav-legend { display: flex; flex-wrap: wrap; gap: 8px 18px; align-items: center; margin: 0 2px 4px; font-size: 12px; color: var(--text-2); }
.nav-legend .k { display: inline-flex; align-items: center; gap: 7px; }
.nav-legend .k::before { content: ''; width: 24px; border-top: 2.2px solid var(--text-1); opacity: .6; }
.nav-legend .k.tap::before { border-top-style: dashed; border-color: var(--accent); opacity: 1; }

.nav-shortcuts { list-style: none; margin: 6px 0 0; padding: 0; display: flex; flex-direction: column; gap: 2px; }
.nav-shortcuts li {
  display: flex; align-items: center; gap: 8px; padding: 9px 0; border-bottom: 1px solid var(--border);
  font-size: 14px;
}
.nav-shortcuts li:last-child { border-bottom: 0; }
.nav-sc-where { flex: 1 1 auto; min-width: 0; }
.nav-sc-arrow { color: var(--text-2); flex: none; }
.nav-sc-to { flex: none; font-weight: 700; color: var(--accent); }
/* padding-bottom: 0 is load-bearing, not tidying. A sticky `bottom: 0` rests against the
   CONTENT box, so any bottom padding on the scroll container leaves a gap of exactly that
   height below the bar — which fields then scroll through, in full view, under the Save
   button. Measured at 360px: the bar sat at 687 while the scrollport ended at 709. The
   action bar supplies that spacing with its own padding instead. */
/* Preferences (theme) is always visible in #profile-edit — it isn't account-bound,
   unlike the rest of the panel. The account form and the "sign in" hint are the two
   halves of that split, toggled by the same html.signed-in class auth.js already
   stamps for .acct-only above, so no extra JS is needed to show/hide them. */
.settings-prefs { padding: 14px 16px 6px; border-bottom: 1px solid var(--border); }
.settings-row {
  display: flex; align-items: center; justify-content: space-between;
  font-size: 15px; font-weight: 600; padding: 4px 0 10px;
}
/* Feed interest chips (2026-07-25). A block per axis (types, genres); each is a wrap of
   toggle chips, the chosen ones lit in the accent. */
.settings-block { padding: 8px 0 12px; }
.settings-block-label { display: block; font-size: 13px; font-weight: 700; color: var(--text-2); margin: 0 0 8px; }
.feed-prefs-hint { margin: 8px 0 0; font-size: 13px; }
.pref-chips { display: flex; flex-wrap: wrap; gap: 8px; }

/* Notifications settings: a master toggle button, then per-category switches when it's on. */
#notif-toggle.is-on, #maxi-voice-toggle.is-on { background: var(--accent); color: #fff; }
.notif-hint { margin: 2px 0 6px; font-size: 13px; color: var(--text-2); }
.notif-kinds { display: flex; flex-direction: column; gap: 8px; padding: 4px 0 6px; }
.notif-kind { display: flex; align-items: center; gap: 10px; font-size: 14px; font-weight: 600; cursor: pointer; }
.notif-kind input { width: 18px; height: 18px; accent-color: var(--accent); }
.lesson-notify { max-width: 320px; margin: 18px auto 0; }
.pref-chip {
  font: inherit; font-size: 13px; font-weight: 650; cursor: pointer;
  color: var(--text-1); background: var(--surface-2); border: 1px solid var(--border);
  border-radius: 999px; padding: 7px 13px; line-height: 1;
}
.pref-chip:hover { border-color: color-mix(in srgb, var(--accent) 50%, var(--border)); }
.pref-chip:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.pref-chip.on {
  color: #fff; background: var(--accent); border-color: var(--accent);
}

.settings-signin-hint {
  display: block; padding: 16px; margin: 0;
  color: var(--text-2); font-size: 14px;
}
html.signed-in .settings-signin-hint { display: none; }
#profile-form { display: none; }
html.signed-in #profile-form { display: block; }

.profile-form { overflow-y: auto; min-height: 0; padding-bottom: 0; }

/* Pinned to the bottom of the scrolling area. The negative inline margins let its
   background bleed through the form's 22px side padding — without them, fields scroll
   visibly through the gutters on either side of the button. */
.profile-form .dlg-actions {
  position: sticky; bottom: 0; z-index: 2;
  margin: 12px -22px 0; padding: 14px 22px;
  background: var(--surface);
  border-top: 1px solid var(--border);
}

.profile-form .group {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  margin: 12px 0 8px; font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-2);
}
.profile-form .group:first-of-type { margin-top: 0; }
/* Tightened so the whole form clears a 900px-tall laptop without scrolling. It was
   overflowing by ~40px, which left the last row sliced in half by the sticky Save bar —
   readable, but it looks like a rendering fault rather than "there is more below". */
.profile-form .sub { margin-bottom: 12px; }

.field-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 0 12px; }
.field-grid .wide { grid-column: 1 / -1; }
/* Gender, birth year and birth month are all short: three across removes a whole row of
   scrolling, and a full-width <select> for "Gender" was mostly empty space. Applied as a
   min-width rule so the narrow default below still wins on a phone. */
@media (min-width: 560px) { .field-grid.three { grid-template-columns: repeat(3, 1fr); } }
/* One column on a phone: two 130px-wide inputs are worse than one readable column. */
@media (max-width: 460px) { .field-grid { grid-template-columns: 1fr; } }

/* Positioning context for that field's own suggestion list. */
.field { position: relative; min-width: 0; }

/* ---- the suggestion list ------------------------------------------------ */
/* Overlays what follows rather than pushing it down — a list that reflowed the form
   would move the very field the reader is typing into. */
.cb-list {
  position: absolute; z-index: 20; left: 0; right: 0; top: 100%;
  margin: -6px 0 0; padding: 4px; list-style: none;
  max-height: 240px; overflow-y: auto;
  background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
  box-shadow: 0 12px 28px rgba(0,0,0,.35);
}
.cb-opt {
  padding: 10px 12px; border-radius: 8px; cursor: pointer;
  font-size: 15px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
}
/* .on is the keyboard highlight and hover is the pointer one; they deliberately look
   identical, so arrowing down and moving the mouse mean the same thing on screen. */
.cb-opt.on, .cb-opt:hover { background: var(--surface-2); }
.cb-opt.on { outline: 1px solid color-mix(in srgb, var(--accent) 50%, transparent); }

/* A public profile (/u/{id}, Phase 19). Sits alongside #feed, shown instead of it —
   see js/main.js's one mode branch. */
.profile-page { max-width: 640px; margin: 0 auto; padding: 28px 20px 80px; }
/* On a profile view the pager and the feed-mode toggle have nothing to show — hide them so
   neither the feed nor a stray stats page sits behind the profile. Author rules, because a
   bare `hidden` attribute loses to `.pager { display:flex }` / the toggle's own display. */
html.profile-view #pager,
html.profile-view .feed-modes { display: none; }

/* ---- Teaching / Learn tab (Phase 1) ------------------------------------------
   The curriculum browser, swapped in for the pager via html.learn-open. A class, not a bare
   `hidden`, for the same reason as the profile view. The feed-mode tabs stay visible. */
html.learn-open #pager { display: none; }
.learn-view { display: none; }
html.learn-open .learn-view {
  display: block; max-width: 640px; margin: 0 auto;
  padding: calc(max(10px, env(safe-area-inset-top)) + 64px) 18px calc(var(--navbar-h) + 28px);
  min-height: 100dvh;
}

/* ---- full-page sections: Inbox & Dashboard (2026-07-28) ----------------------------
   Were dialogs; now sections swapped in for the pager via html.inbox-open / html.admin-open
   — the SAME pattern as the Learn view above. The feed-mode wheel stays visible so the reader
   rolls back to the feed; there is no close button. The inner markup (.admin-head, .tabs,
   .tab, .panel, #inbox-list) is unchanged, so its styling still applies. */
.section-view {
  display: none;
  max-width: 1040px; margin: 0 auto; box-sizing: border-box;
  padding: calc(max(10px, env(safe-area-inset-top)) + 60px) 16px calc(var(--navbar-h) + 28px);
  min-height: 100dvh;
}
html.inbox-open #pager, html.admin-open #pager { display: none; }
html.inbox-open #inbox, html.admin-open #admin { display: block; }
/* The create-a-poll FAB is feed chrome — it has no meaning on the full-page views (a lesson,
   the inbox, the dashboard) and its fixed centre position collided with the lesson's swipe hint.
   Hide it there; #version (the status line) stays. */
html.learn-open #new-poll, html.inbox-open #new-poll, html.admin-open #new-poll, html.about-open #new-poll { display: none; }
.section-view.inbox { max-width: 620px; }   /* a reading column, not a table of controls */

/* ---- About page (2026-08-09) -------------------------------------------------------
   Opened from the top-left app icon, swapped in for the pager via html.about-open — the same
   view-mode pattern as Learn/Inbox. A reading column with the hero, mission, support and credits. */
html.about-open #pager { display: none; }
.about-view { display: none; }
html.about-open .about-view {
  display: block;
  padding: calc(max(10px, env(safe-area-inset-top)) + 64px) 18px calc(var(--navbar-h) + 28px);
  min-height: 100dvh;
}
.about-view-inner { max-width: 560px; margin: 0 auto; }
.about-hero { text-align: center; margin-bottom: 28px; }
.about-logo { width: 88px; height: 88px; border-radius: 22px; box-shadow: var(--card-shadow); }
.about-name { font-family: var(--font-display); font-size: 32px; margin: 12px 0 2px; }
.about-tagline { color: var(--muted); font-size: 15px; margin: 0; }
.about-block { margin: 0 0 22px; }
.about-block h2 { font-family: var(--font-display); font-size: 20px; margin: 0 0 8px; }
.about-block p { color: var(--text); line-height: 1.55; margin: 0 0 8px; }
.about-support {
  border: 1px solid var(--border); border-radius: 18px; padding: 18px 18px 20px;
  background: color-mix(in srgb, var(--accent) 6%, var(--surface));
  text-align: center;
}
.about-support h2, .about-support p { text-align: center; }
.about-support-emblem {
  display: block; margin: 2px auto 12px; width: 120px; height: 120px;
  border-radius: 20px; box-shadow: 0 6px 20px color-mix(in srgb, var(--accent) 30%, transparent);
}
.about-support-btns { display: flex; flex-direction: column; gap: 10px; margin-top: 14px; }
.about-support-btn {
  display: block; text-align: center; text-decoration: none;
  padding: 12px 16px; border-radius: 999px; font-weight: 700; font-size: 15px;
  color: #fff; background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 60%, #7048e8));
  box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 35%, transparent);
}
.about-support-btn:active { transform: translateY(1px); }
.about-support-soon { color: var(--muted); font-style: italic; margin-top: 12px; margin-bottom: 0; }
.about-version { text-align: center; color: var(--muted); font-size: 13px; margin: 28px 0 0; }

/* Feedback: the entry on the About page + the form it opens (in the same #about-view column). */
.about-feedback { text-align: center; }
.about-feedback-btn {
  display: inline-block; margin-top: 6px; padding: 11px 20px; border: 1.5px solid var(--border);
  border-radius: 999px; background: var(--surface); color: var(--text); font-weight: 700; font-size: 15px; cursor: pointer;
}
.about-feedback-btn:active { transform: translateY(1px); }
.fb-view { max-width: 560px; margin: 0 auto; }
.about-back { border: 0; background: none; color: var(--accent); font: inherit; font-weight: 700; cursor: pointer; padding: 4px 0; margin-bottom: 8px; }
.fb-title { font-family: var(--font-display, inherit); font-size: 26px; margin: 4px 0 6px; }
.fb-lead { color: var(--text); line-height: 1.55; margin: 0 0 18px; }
.fb-form { display: flex; flex-direction: column; gap: 14px; }
.fb-field { display: flex; flex-direction: column; gap: 6px; }
.fb-q { font-weight: 700; font-size: 14px; }
.fb-q em { font-weight: 400; color: var(--muted); font-style: normal; }
.fb-input { width: 100%; border: 1px solid var(--border); border-radius: 10px; padding: 10px 12px; font: inherit; background: var(--bg); color: var(--text); box-sizing: border-box; }
textarea.fb-input { resize: vertical; }
.fb-images { display: flex; flex-wrap: wrap; gap: 8px; }
.fb-thumb { position: relative; width: 64px; height: 64px; border-radius: 10px; overflow: hidden; border: 1px solid var(--border); }
.fb-thumb img { width: 100%; height: 100%; object-fit: cover; }
.fb-thumb-x { position: absolute; top: 2px; right: 2px; width: 20px; height: 20px; border: 0; border-radius: 50%; background: rgba(0,0,0,0.6); color: #fff; font-size: 14px; line-height: 1; cursor: pointer; }
.fb-add-img { display: inline-block; border: 1px dashed var(--border); border-radius: 10px; padding: 8px 14px; color: var(--accent); font-weight: 600; cursor: pointer; width: fit-content; }
.fb-add-img.is-busy { opacity: 0.6; pointer-events: none; }
/* Honeypot: kept in the DOM but off-screen so a human never sees or fills it (bots do). */
.fb-hp { position: absolute; left: -9999px; width: 1px; height: 1px; opacity: 0; }
.fb-submit {
  margin-top: 4px; padding: 13px 18px; border: 0; border-radius: 999px; font-weight: 700; font-size: 15px; cursor: pointer;
  color: #fff; background: linear-gradient(135deg, var(--accent), color-mix(in srgb, var(--accent) 60%, #7048e8));
  box-shadow: 0 4px 14px color-mix(in srgb, var(--accent) 35%, transparent);
}
.fb-submit:disabled { opacity: 0.7; cursor: default; }
.fb-error { color: var(--danger); font-size: 14px; margin: 4px 0 0; }
.fb-thanks { text-align: center; padding-top: 12px; }
.fb-thanks-icon { font-size: 44px; }

/* The top-left app icon breathes gently so it reads as "there is something here" (it opens About).
   Guarded by prefers-reduced-motion below. */
@keyframes brand-breathe {
  0%, 100% { transform: scale(1); filter: drop-shadow(0 0 0 transparent); }
  50% { transform: scale(1.06); filter: drop-shadow(0 0 6px color-mix(in srgb, var(--accent) 55%, transparent)); }
}
.brand-mark { animation: brand-breathe 3.2s ease-in-out infinite; will-change: transform; }
@media (prefers-reduced-motion: reduce) { .brand-mark { animation: none; } }
/* The inbox unread badge rides its wheel tab now (it left the topbar with the bell). */
.feed-mode .badge-count {
  position: absolute; top: -3px; right: -3px; margin: 0;
  line-height: 15px; min-width: 16px; padding: 0 4px; font-size: 9px;
  border: 2px solid var(--bg);
}
.learn-head h1 { font-size: 26px; font-weight: 800; margin: 0 0 4px; }
.learn-sub { color: var(--text-2); margin: 0 0 20px; }
.learn-step h2 { font-size: 18px; font-weight: 700; margin: 6px 0 14px; }
.learn-back { background: none; border: none; color: var(--accent); font: inherit; font-weight: 600; padding: 4px 0; margin: 0 0 6px; cursor: pointer; }
.learn-cards { display: grid; gap: 10px; }
.learn-card { display: flex; flex-direction: column; gap: 3px; text-align: left; padding: 16px 18px; border-radius: 14px; cursor: pointer; background: var(--surface); border: 1px solid var(--border); color: var(--text-1); }
@media (hover: hover) { .learn-card:hover { border-color: var(--accent); } }
.learn-card-title { font-size: 16px; font-weight: 650; }
.learn-card-sub { font-size: 13px; color: var(--text-2); }
.learn-units { display: flex; flex-direction: column; gap: 14px; }
.learn-unit { padding: 16px 18px; border-radius: 14px; background: var(--surface); border: 1px solid var(--border); }
.learn-unit h3 { font-size: 16px; font-weight: 700; margin: 0 0 4px; display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.learn-code { font-size: 11px; font-weight: 600; color: var(--text-2); border: 1px solid var(--border); border-radius: 6px; padding: 1px 6px; }
.learn-unit-desc { color: var(--text-2); font-size: 13px; margin: 0 0 8px; }
.learn-topics { margin: 0; padding-left: 0; list-style: none; display: flex; flex-direction: column; gap: 6px; }
/* Topics are directly openable (all material unlocked for review) — each is a tappable row. */
.learn-topic {
  display: flex; align-items: center; justify-content: space-between; gap: 10px;
  padding: 10px 12px; border: 1px solid var(--border); border-radius: 10px;
  background: var(--bg); cursor: pointer; transition: background .12s ease, border-color .12s ease;
}
.learn-topic:hover, .learn-topic:focus-visible { background: var(--surface-2); border-color: var(--accent); outline: none; }
.learn-topic-go { color: var(--accent); font-weight: 700; }
/* Learners (non-admin) see topics as a plain, non-clickable list — access is via the study plan. */
.learn-topic-plain { cursor: default; background: transparent; }
.learn-topic-plain:hover, .learn-topic-plain:focus-visible { background: transparent; border-color: var(--border); }
.learn-loading, .learn-empty { color: var(--text-2); padding: 20px 0; }
.learn-foot { color: var(--text-2); font-size: 13px; margin-top: 18px; text-align: center; }

/* The signed-out "free previews used" wall (2026-08-10) and the in-lesson preview banner. Warm, not a
   hard block — the reader has already seen value; this invites them to keep it. */
.learn-wall { text-align: center; padding: 24px 8px; max-width: 460px; margin: 0 auto; }
.learn-wall-icon { font-size: 40px; }
.learn-wall-lead { font-weight: 700; font-size: 18px; margin: 10px 0 6px; }
.learn-wall p { color: var(--text); line-height: 1.55; }
.learn-wall .learn-enroll { margin-top: 16px; }
.learn-wall-note { color: var(--text-2); font-size: 13px; margin-top: 10px; }
.lesson-preview-banner {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 4px;
  margin: 0 0 14px; padding: 10px 14px; border-radius: 12px; font-size: 13px; line-height: 1.4;
  color: var(--text); text-align: center;
  background: color-mix(in srgb, var(--accent) 12%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--accent) 30%, transparent);
}
.lesson-preview-signin {
  border: 0; background: none; padding: 0; font: inherit; font-weight: 700; cursor: pointer;
  color: var(--accent); text-decoration: underline;
}

/* Admin curriculum-request fulfilment form (2026-08-10): the request context + duplicate warnings. */
.fulfil-meta { margin: 12px 0; display: grid; gap: 8px; }
.fulfil-meta > div { display: grid; grid-template-columns: 130px 1fr; gap: 8px; }
.fulfil-meta dt { color: var(--text-2); font-size: 13px; margin: 0; }
.fulfil-meta dd { margin: 0; }
.fulfil-dups {
  margin: 14px 0; padding: 12px 14px; border-radius: 12px;
  background: color-mix(in srgb, var(--warn) 12%, var(--surface));
  border: 1px solid color-mix(in srgb, var(--warn) 40%, transparent);
}
.fulfil-dups-head { font-weight: 700; margin: 0 0 6px; }
.fulfil-dups ul { margin: 0 0 6px; padding-left: 18px; }
.fulfil-dups li { margin: 2px 0; }
.fulfil-dups li span { color: var(--text-2); font-size: 13px; }
.fulfil-dups-note { color: var(--text-2); font-size: 13px; margin: 0; }

/* Block editor for generated lessons (per-block AI edits + formatting). */
.be-editor { display: flex; flex-direction: column; gap: 14px; }
.be-group-head { margin: 8px 0 4px; font-size: 14px; color: var(--text-2); }
.be-block { border: 1px solid var(--border); border-radius: 12px; padding: 10px 12px; margin: 0 0 10px; background: var(--surface); }
.be-block legend { font-size: 13px; color: var(--text-2); padding: 0 4px; }
.be-heading { width: 100%; font-weight: 700; border: 1px solid var(--border); border-radius: 8px; padding: 6px 8px; margin-bottom: 6px; background: var(--bg); color: var(--text); }
/* All editor buttons share one calm chip style (works in the toolbar, the AI row and the read-only card). */
.be-block .be-fmt, .be-block .be-btn, .be-block .be-icon {
  border: 1px solid var(--border); background: var(--bg); color: var(--text); border-radius: 7px;
  padding: 4px 9px; font-size: 12px; cursor: pointer; line-height: 1.4;
}
.be-block .be-btn:hover, .be-block .be-fmt:hover, .be-block .be-icon:hover { border-color: var(--accent); color: var(--accent); }
.be-block button:disabled { opacity: 0.5; cursor: default; }
.be-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-bottom: 6px; }
.be-del { color: var(--danger); }
.be-body { width: 100%; min-height: 84px; border: 1px solid var(--border); border-radius: 8px; padding: 8px; font: inherit; background: var(--bg); color: var(--text); resize: vertical; }
.be-preview { margin-top: 6px; padding: 8px 10px; border-radius: 8px; background: color-mix(in srgb, var(--accent) 5%, var(--surface)); font-size: 14px; }
.be-preview:empty { display: none; }
.be-visual-note { color: var(--text-2); font-size: 12px; margin: 6px 0 0; }
.be-add { border: 1px dashed var(--border); background: none; color: var(--accent); border-radius: 8px; padding: 6px 12px; font-weight: 600; cursor: pointer; }
.be-thinking { opacity: 0.7; }
.rt-list { margin: 6px 0; padding-left: 20px; }

/* Read-only block: renders like the finished MATERIAL (no card, no toolbar) so a review reads cleanly.
   The whole block is a tap target that opens editing; a faint pencil is the only affordance. */
.be-block.be-ro {
  border: 1px solid transparent; background: none; padding: 6px 30px 6px 8px; margin: 0;
  position: relative; cursor: pointer; border-radius: 10px;
}
.be-ro:hover, .be-ro:focus-visible { border-color: color-mix(in srgb, var(--accent) 35%, transparent); background: color-mix(in srgb, var(--accent) 4%, transparent); outline: none; }
.be-ro h3 { margin: 0 0 4px; }
.be-empty { color: var(--text-2); font-style: italic; }
.be-pencil { position: absolute; top: 6px; right: 8px; font-size: 14px; opacity: 0.28; transition: opacity 0.15s; }
.be-ro:hover .be-pencil, .be-ro:focus-visible .be-pencil { opacity: 0.85; color: var(--accent); }
.be-realworld-head { margin: 10px 0 4px; font-family: var(--font-display, inherit); }
.be-foot-left { display: flex; gap: 4px; align-items: center; }
.be-media-row { display: flex; flex-wrap: wrap; gap: 6px; align-items: center; margin-top: 10px; }
.be-media-row .be-upload { cursor: pointer; }
.be-media-row .lesson-photo, .be-active .lesson-photo { margin: 8px 0; }
.be-active figure.lesson-photo img { max-width: 100%; border-radius: 8px; }
.be-pending, .lesson-img-pending { margin: 8px 0; padding: 8px 12px; border-radius: 8px; font-size: 13px; color: var(--text-2); background: color-mix(in srgb, var(--accent) 8%, var(--surface)); border: 1px dashed color-mix(in srgb, var(--accent) 30%, transparent); }

/* The one open block: highlighted, with the AI quick-actions row and a Done/Delete footer. */
.be-active { border-color: var(--accent); box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 22%, transparent); }
.be-ai-row { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; margin-top: 8px; }
.be-ai-label { font-size: 12px; color: var(--text-2); margin-right: 2px; }
.be-ai-row .be-age { width: 56px; flex: 0 0 56px; border: 1px solid var(--border); border-radius: 7px; padding: 3px 6px; background: var(--bg); color: var(--text); }
.be-active-foot { display: flex; justify-content: space-between; align-items: center; gap: 8px; margin-top: 10px; }
.be-done { background: var(--accent); color: #fff; border-color: var(--accent) !important; font-weight: 700; }
.be-done:hover { color: #fff !important; opacity: 0.92; }

/* Human-in-the-loop review switch on the dashboard Learning tab. */
.mod-toggle {
  display: flex; align-items: flex-start; gap: 10px; margin: 0 0 16px; padding: 12px 14px;
  border: 1px solid var(--border); border-radius: 12px; background: var(--surface); cursor: pointer;
}
.mod-toggle input { margin-top: 3px; }
.mod-toggle small { display: block; color: var(--text-2); font-size: 13px; margin-top: 2px; }
/* Enroll button + the study-plan view (topics in order with progress states). */
.learn-enroll {
  display: block; width: 100%; margin: 22px 0 4px; padding: 15px 18px;
  border: 0; border-radius: 14px; background: var(--accent); color: #fff;
  font: inherit; font-size: 16px; font-weight: 700; cursor: pointer;
}
@media (hover: hover) { .learn-enroll:hover { filter: brightness(1.06); } }

/* "Create your own course" — the hero entry at the Learn home, then its intake form. */
.learn-make {
  display: flex; align-items: center; gap: 13px; width: 100%; margin: 18px 0 22px; padding: 16px 18px;
  border: 0; border-radius: 16px; cursor: pointer; text-align: left; font: inherit; color: #fff;
  background: linear-gradient(120deg, var(--accent), color-mix(in srgb, var(--accent) 55%, #a855f7));
  box-shadow: 0 6px 18px color-mix(in srgb, var(--accent) 30%, transparent);
}
@media (hover: hover) { .learn-make:hover { filter: brightness(1.05); } }
.learn-make:active { transform: translateY(1px); }
/* Fixed icon + chevron columns so the text block has one clean left edge and the row stays centered
   however many lines the subtitle wraps to. */
.learn-make-icon { flex: none; width: 34px; font-size: 28px; line-height: 1; text-align: center; }
.learn-make-text { display: flex; flex-direction: column; gap: 3px; flex: 1; min-width: 0; }
.learn-make-title { font-size: 17px; font-weight: 800; line-height: 1.2; }
.learn-make-sub { font-size: 13px; line-height: 1.35; opacity: 0.92; }
.learn-make-go { flex: none; align-self: center; font-size: 24px; font-weight: 700; opacity: 0.8; }

/* The "Request a topic or curriculum" affordance in School curriculums — a friendly dashed accent pill,
   not the browser-default grey button. */
.learn-request {
  display: inline-flex; align-items: center; gap: 6px; margin: 16px 0 4px; padding: 10px 18px;
  border: 1.5px dashed color-mix(in srgb, var(--accent) 55%, transparent); border-radius: 999px;
  background: color-mix(in srgb, var(--accent) 9%, transparent); color: var(--accent);
  font: inherit; font-weight: 700; font-size: 14px; cursor: pointer;
}
@media (hover: hover) { .learn-request:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); } }
.learn-request:active { transform: translateY(1px); }

/* Family section (child accounts, slice 2c): the active-learner banner, the per-child dashboard cards,
   and the add-a-child consent row. Theme-aware via the same vars the rest of Learn uses. */
.learn-active-banner {
  display: flex; align-items: center; gap: 10px; margin: 0 0 16px; padding: 11px 14px;
  border-radius: 14px; cursor: pointer;
  background: color-mix(in srgb, var(--accent) 16%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, transparent); color: var(--text-1);
}
.learn-active-icon { font-size: 22px; line-height: 1; }
.learn-active-text { flex: 1; min-width: 0; font-size: 14px; }
.learn-active-switch { flex: none; font-weight: 700; color: var(--accent); font-size: 14px; }

/* Dashboards (Family + Classroom) use the DESKTOP width — the lesson view stays narrow (640px) for
   readability, but a roster/family list is a poor use of a phone-width column on a big screen. */
@media (min-width: 900px) {
  html.learn-open .learn-view:has(.family),
  html.learn-open .learn-view:has(.classroom) { max-width: 980px; }
}

/* Learner + class cards: one column on a phone, a responsive auto-fill grid once there's room. */
.family-cards { display: grid; grid-template-columns: 1fr; gap: 14px; margin: 6px 0 12px; }
@media (min-width: 540px) { .family-cards { grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); } }

.family-card {
  padding: 16px; border-radius: 18px; background: var(--card, rgba(127,127,127,0.06));
  border: 1px solid var(--line, rgba(127,127,127,0.18));
  display: flex; flex-direction: column; gap: 12px; text-align: left;
}
.family-card.is-active { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent) inset; }
.family-card-head { display: flex; align-items: center; gap: 10px; }
.family-avatar { font-size: 24px; line-height: 1; flex: none; }
.family-name { flex: 1; min-width: 0; font-size: 17px; font-weight: 800; color: var(--text-1); }
.family-icon-btn { flex: none; border: 0; background: none; cursor: pointer; font-size: 16px; line-height: 1; padding: 5px; border-radius: 9px; }
.family-icon-btn.family-edit { margin-left: auto; }
@media (hover: hover) { .family-icon-btn:hover { background: var(--surface-2, rgba(127,127,127,0.14)); } }

/* Compact stat pills + a mastery progress bar — the same visual language across both dashboards. */
.stat-pills { display: flex; flex-wrap: wrap; gap: 6px; }
.stat-pill { font-size: 12px; font-weight: 700; padding: 4px 10px; border-radius: 999px; color: var(--text-1);
  background: color-mix(in srgb, var(--accent) 13%, transparent); }
.mastery { display: flex; align-items: center; gap: 10px; }
.mastery-bar { flex: 1; height: 8px; border-radius: 999px; overflow: hidden; background: var(--track, rgba(127,127,127,0.22)); }
.mastery-bar > span { display: block; height: 100%; border-radius: 999px; background: var(--good, #40c057); transition: width .3s ease; }
.mastery-label { flex: none; font-size: 12px; color: var(--text-2); }

/* The class card reuses .family-card; this row is its "N students · Open ›" line. */
.family-card-stats { display: flex; justify-content: space-between; gap: 8px; font-size: 13px; color: var(--text-2); }
.family-card-actions { display: flex; gap: 8px; flex-wrap: wrap; margin-top: 2px; }
.family-learn-as {
  border: 0; border-radius: 999px; padding: 8px 16px; cursor: pointer; font: inherit; font-weight: 700; font-size: 14px;
  color: #fff; background: var(--accent);
}
.family-active-tag { font-size: 13px; font-weight: 700; color: var(--accent); align-self: center; }
.family-add-entry { margin-top: 4px; }
.family-consent { display: flex; gap: 10px; align-items: flex-start; font-size: 13px; line-height: 1.4; color: var(--text-1); }
.family-consent input { margin-top: 3px; flex: none; }
.learn-back-inline { border: 0; background: none; color: var(--text-2); font: inherit; cursor: pointer; text-decoration: underline; align-self: flex-start; }

/* Encouragement notes (slice 2c-ii): the child-facing card on the Learn home, and the guardian's
   composer + history in the Family section. */
.family-note-btn { flex: none; margin-left: 10px; border: 1px solid var(--line, rgba(127,127,127,0.3)); background: none; color: var(--text-1); font: inherit; font-size: 13px; border-radius: 999px; padding: 7px 14px; cursor: pointer; }
.family-notes-list { display: flex; flex-direction: column; gap: 8px; margin-top: 14px; }
.family-note-item { padding: 10px 12px; border-radius: 12px; background: var(--card, rgba(127,127,127,0.08)); font-size: 14px; color: var(--text-1); }
.family-note-seen { color: var(--text-2); font-size: 12px; }
.learn-note-card {
  display: flex; gap: 12px; align-items: flex-start; margin: 0 0 16px; padding: 14px 16px; border-radius: 16px;
  background: color-mix(in srgb, var(--accent) 14%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 35%, transparent);
}
.learn-note-mark { font-size: 26px; line-height: 1; }
.learn-note-text { flex: 1; min-width: 0; color: var(--text-1); }
.learn-note-text strong { display: block; margin-bottom: 4px; }
.learn-note-body { margin: 4px 0 0; font-size: 15px; line-height: 1.4; }

/* Teacher classrooms (Slice 3): class cards reuse the family-card look; the roster is a list of rows. */
.classroom-card { width: 100%; text-align: left; cursor: pointer; border: 1px solid var(--line, rgba(127,127,127,0.22)); }
.roster-row { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.roster-email { color: var(--text-2); font-size: 13px; }
.class-actions { display: flex; gap: 8px; margin: 12px 0 16px; flex-wrap: wrap; }
.assign-row { align-items: flex-start; }
.assign-kind { display: inline-block; font-size: 11px; text-transform: uppercase; letter-spacing: .03em; color: var(--text-2); }
.assign-note { margin-top: 4px; font-size: 13px; color: var(--text-2); }
.assign-target-to { font-size: 12px; font-weight: 700; color: var(--accent); }
/* Student drill-down: a name that opens the detail, and per-assignment completion badges. */
.link-btn { border: 0; background: none; padding: 0; font: inherit; font-weight: 700; color: var(--accent); cursor: pointer; text-align: left; }
.assign-status { flex: none; font-size: 12px; font-weight: 700; padding: 3px 10px; border-radius: 999px; white-space: nowrap; background: var(--surface-2, rgba(127,127,127,0.14)); color: var(--text-2); }
.assign-status.is-done { color: var(--good, #2f9e44); background: color-mix(in srgb, var(--good, #2f9e44) 16%, transparent); }
.assign-status.is-progress { color: var(--accent); background: color-mix(in srgb, var(--accent) 15%, transparent); }
.assign-status.is-todo { color: var(--text-2); }
.class-progress { width: 100%; border-collapse: collapse; font-size: 14px; margin-top: 8px; }
.class-progress th, .class-progress td { text-align: left; padding: 8px 6px; border-bottom: 1px solid var(--line, rgba(127,127,127,0.2)); }
.class-progress th { color: var(--text-2); font-weight: 700; font-size: 12px; }
.assignment-card .learn-card-icon { font-size: 22px; }

.course-form { display: flex; flex-direction: column; gap: 16px; }
.course-field { display: flex; flex-direction: column; gap: 7px; }
.course-q { font-size: 14px; font-weight: 700; color: var(--text-1); }
.course-q em { font-style: normal; font-weight: 500; color: var(--text-2); }
.course-input {
  width: 100%; padding: 13px 14px; border-radius: 12px; border: 1px solid var(--border);
  background: var(--surface); color: var(--text-1); font: inherit; font-size: 16px;
}
.course-input:focus { outline: none; border-color: var(--accent); box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 22%, transparent); }
.course-age { max-width: 120px; }
.course-depth { display: flex; gap: 8px; }
.course-depth-opt {
  flex: 1; display: flex; flex-direction: column; gap: 2px; padding: 11px 8px; cursor: pointer;
  border: 1.5px solid var(--border); border-radius: 12px; background: var(--surface);
  color: var(--text-1); font: inherit; text-align: center;
}
.course-depth-opt.is-on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 12%, var(--surface)); }
.course-depth-label { font-size: 14px; font-weight: 700; }
.course-depth-sub { font-size: 11px; color: var(--text-2); }
.course-error { color: var(--danger, #e5484d); font-size: 14px; font-weight: 600; margin: 2px 0 0; }

/* Promoted "community" courses — a shared course another learner built. A soft accent edge sets them
   apart from the seeded grade cards without shouting. */
.learn-community { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); position: relative; }

/* Community Learning browse (slice 1): search box, category chips, sort toggle and the per-card badge. */
.community-search {
  width: 100%; box-sizing: border-box; margin: 4px 0 12px; padding: 11px 14px;
  border-radius: 12px; border: 1px solid var(--border); background: var(--surface);
  color: var(--text); font-size: 15px;
}
.community-search:focus { outline: none; border-color: var(--accent); }
.community-chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 12px; }
.community-chip {
  padding: 7px 13px; border-radius: 999px; border: 1px solid var(--border);
  background: var(--surface); color: var(--text); font-size: 13px; font-weight: 700;
  cursor: pointer; display: inline-flex; align-items: center; gap: 6px;
}
.community-chip.is-on { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, var(--surface)); }
.community-chip-n { font-size: 11px; font-weight: 700; opacity: 0.7; }
.community-sort { display: inline-flex; gap: 6px; margin-bottom: 14px; }
.community-sort-opt {
  padding: 7px 13px; border-radius: 10px; border: 1px solid var(--border);
  background: var(--surface); color: var(--text-2); font-size: 13px; font-weight: 700; cursor: pointer;
}
.community-sort-opt.is-on { border-color: var(--accent); color: var(--text); background: color-mix(in srgb, var(--accent) 12%, var(--surface)); }
.community-badge {
  align-self: flex-start; margin-bottom: 2px; padding: 2px 9px; border-radius: 999px;
  font-size: 11px; font-weight: 800; letter-spacing: 0.02em;
  background: color-mix(in srgb, var(--accent) 16%, var(--surface)); color: var(--accent);
}

/* A course card plus its rating widget stacked as one grid cell (the card is a <button>, so the star
   widget — itself buttons — cannot nest inside it). */
.learn-community-cell { display: flex; flex-direction: column; }
.community-rating {
  display: flex; align-items: center; gap: 8px; padding: 6px 14px 2px;
}
.community-stars { display: inline-flex; gap: 2px; }
.community-star {
  padding: 2px; border: 0; background: none; cursor: pointer; line-height: 1;
  font-size: 18px; color: var(--border); transition: color 0.12s ease, transform 0.12s ease;
}
.community-star.is-on { color: #f5b301; }
.community-star:hover { transform: scale(1.15); }
.community-rating-meta { font-size: 12px; font-weight: 700; color: var(--text-2); }
.community-rating-n { font-weight: 600; opacity: 0.75; }
.community-rating-new { color: var(--accent); }
.community-rating-pulse .community-star.is-on { animation: community-star-pop 0.4s ease; }
@keyframes community-star-pop { 0% { transform: scale(1); } 45% { transform: scale(1.35); } 100% { transform: scale(1); } }

/* The foot of a community card holds the star widget and, tucked to the right, the report link. */
.community-card-foot { display: flex; align-items: center; justify-content: space-between; gap: 8px; }
.community-report {
  padding: 2px 6px; border: 0; background: none; cursor: pointer; flex: none;
  font-size: 11px; font-weight: 600; color: var(--text-2); opacity: 0.7; text-decoration: underline;
}
.community-report:hover { opacity: 1; color: var(--text); }
.community-report:disabled { cursor: default; text-decoration: none; opacity: 0.55; }

.learn-progress { height: 6px; border-radius: 3px; background: var(--surface); border: 1px solid var(--border); overflow: hidden; margin: 4px 0 18px; }
.learn-progress > span { display: block; height: 100%; background: var(--accent); border-radius: 3px; transition: width 0.3s ease; }
.learn-plan { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.learn-plan-item {
  display: flex; align-items: center; gap: 12px; padding: 13px 16px;
  border-radius: 12px; background: var(--surface); border: 1px solid var(--border);
}
.learn-plan-num { flex: 0 0 22px; height: 22px; border-radius: 50%; display: grid; place-items: center;
  font-size: 12px; font-weight: 700; color: var(--text-2); background: var(--bg); border: 1px solid var(--border); }
.learn-plan-title { flex: 1 1 auto; font-size: 15px; font-weight: 600; }
.learn-plan-state { flex: 0 0 auto; font-size: 12px; font-weight: 700; color: var(--text-2); }
.learn-plan-item.is-available { border-color: var(--accent); }
.learn-plan-item.is-available .learn-plan-num { background: var(--accent); color: #fff; border-color: var(--accent); }
.learn-plan-item.is-available .learn-plan-state { color: var(--accent); }
.learn-plan-item.is-mastered .learn-plan-state { color: var(--good, #2ecc71); }
.learn-plan-item.is-locked { opacity: 0.6; }
.learn-plan-item.is-open { cursor: pointer; }
@media (hover: hover) { .learn-plan-item.is-open:hover { border-color: var(--accent); } }

/* ---- the lesson page (Slice 1): material + diagrams + math + practice quiz ---- */
.lesson { max-width: 640px; }
.lesson-title { font-size: 24px; font-weight: 800; margin: 4px 0 10px; }
.lesson-intro { font-size: 16px; line-height: 1.55; color: var(--text-1); margin: 0 0 8px; }
.lesson-section { margin: 22px 0; }
.lesson-section h3 { font-size: 17px; font-weight: 700; margin: 0 0 6px; }
.lesson-section p { font-size: 15px; line-height: 1.6; color: var(--text-1); margin: 0; }
/* The required real-world "where this shows up" block — tinted so it reads as its own callout. */
.lesson-realworld { margin: 24px 0 8px; padding: 4px 14px 12px; border-radius: 14px;
  background: color-mix(in srgb, var(--accent) 9%, transparent);
  border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border)); }
.lesson-realworld > .lp-side-head { color: var(--accent); margin-top: 12px; }
/* The FUN block (grade <= 9): a surprising fact + a clean joke + optional funny media. Warm, playful. */
.lesson-funfact { margin: 24px 0 8px; padding: 4px 14px 14px; border-radius: 14px;
  background: color-mix(in srgb, var(--warn) 12%, transparent);
  border: 1px solid color-mix(in srgb, var(--warn) 30%, var(--border)); }
.lesson-funfact > .lp-side-head { color: var(--warn); margin-top: 12px; }
.lesson-fun-fact { margin: 6px 0; }
.lesson-fun-joke { margin: 6px 0; font-style: italic; }
/* A picture in a section (photo), distinct from a drawn diagram. */
.lesson-photo { margin: 12px 0 4px; }
.lesson-photo img { display: block; width: 100%; height: auto; border-radius: 12px; }
.lesson-photo figcaption { margin-top: 6px; font-size: 12px; color: var(--text-2); text-align: center; }
.lesson-refs { margin: 26px 0 8px; }
.lesson-refs h3 { font-size: 16px; font-weight: 700; margin: 0 0 8px; }
.lesson-refs ul { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 8px; }
.lesson-ref { display: inline-block; background: var(--surface); border: 1px solid var(--border); color: var(--accent);
  border-radius: 10px; padding: 10px 14px; font: inherit; font-weight: 600; text-decoration: none; cursor: pointer; text-align: left; width: 100%; }
@media (hover: hover) { .lesson-ref:hover { border-color: var(--accent); } }
.lesson-practice-head { font-size: 17px; font-weight: 700; margin: 28px 0 10px; }
/* Reuse the feed's quiz slide inline: strip the full-screen pager styling. */
.lesson-quiz .slide.quiz {
  position: static; height: auto; min-height: 0; width: auto; scroll-snap-align: none;
  padding: 0; display: block; background: none;
}
/* A practice quiz is not a feed post: hide the background bloom, the "Quiz" badge and the
   like/comment/share engagement rail that the reused slide brings with it. */
.lesson-quiz .slide-bg, .lesson-quiz .slide-bg-img, .lesson-quiz .quiz-badge, .lesson-quiz .rail,
.lesson-quiz .badge-row { display: none; }

/* ---- lesson practice pager (lessonpager.js): [ textbook | question | stats ] per question ---- */
/* Height follows the ACTIVE page (set in JS), so the short question page never inherits the tall
   textbook's height; overflow hidden clips the off-screen pages. */
.lpager { position: relative; overflow: hidden; width: 100%; margin-top: 8px; touch-action: pan-y; transition: height .26s ease; }
/* FIVE pages now (material · question · stats · chat · game): the strip is 5× the viewport and each
   page 1/5 of it, and every view has a slide rule. Adding the game page to VIEWS in JS without these
   made the game view unreachable — the strip stayed on material, so "Play Speed Match" looked frozen. */
.lpager-strip { display: flex; align-items: flex-start; width: 500%; transform: translateX(0); transition: transform .26s ease; will-change: transform; }
.lpager.dragging .lpager-strip { transition: none; }
.lpager[data-view="material"] .lpager-strip { transform: translateX(0); }
.lpager[data-view="question"] .lpager-strip { transform: translateX(-20%); }
.lpager[data-view="stats"]    .lpager-strip { transform: translateX(-40%); }
.lpager[data-view="chat"]     .lpager-strip { transform: translateX(-60%); }
.lpager[data-view="game"]     .lpager-strip { transform: translateX(-80%); }
.lpager-page { flex: 0 0 20%; width: 20%; box-sizing: border-box; padding-bottom: 10px; }
/* No card around the textbook or the stats: the content sits directly on the page, exactly like the
   question page does, so the three pages read as one surface rather than boxes within a box. */
.lp-inner { border: 0; background: none; border-radius: 0; padding: 0; }
.lp-textbook .lesson-section:first-of-type { margin-top: 12px; }
.lp-side-head { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: var(--text-2); margin: 0 0 10px; }
.lp-return { margin-top: 16px; border: 0; background: none; color: var(--accent); font: inherit; font-weight: 600; cursor: pointer; padding: 0; }
.lp-forward { display: inline-block; margin-top: 20px; padding: 10px 18px; border: 1px solid var(--accent); border-radius: 999px; }

/* ---- lesson chat (lessonchat.js): the fourth page ---- */
.lp-chat-inner { display: flex; flex-direction: column; }
.lp-chat-list { display: flex; flex-direction: column; gap: 10px; margin-bottom: 12px; }
.lp-chat-empty { color: var(--text-2); font-size: 14px; margin: 4px 0; }
.lp-msg { border-bottom: 1px solid var(--border); padding-bottom: 8px; }
.lp-msg-head { display: flex; gap: 8px; align-items: baseline; }
.lp-msg-who { font-weight: 700; font-size: 14px; }
.lp-msg-when { color: var(--text-2); font-size: 12px; }
.lp-msg-body { margin: 2px 0 0; font-size: 15px; line-height: 1.45; white-space: pre-wrap; }
.lp-chat-form { display: flex; flex-direction: column; gap: 8px; }
.lp-chat-input { width: 100%; box-sizing: border-box; resize: vertical; border: 1px solid var(--border);
  border-radius: 10px; background: var(--bg); color: var(--text-1); padding: 10px; font: inherit; }
.lp-chat-send { align-self: flex-end; }

/* The question's OWN visual + context, shown WITH the question (not a swipe away). Kept compact so
   the question itself stays in view — the FULL-size diagrams live on the textbook page. */
.lp-qsupport { margin-bottom: 4px; }
.lp-qsupport .learn-diagram { margin-top: 0; padding: 8px; }
.lp-qsupport .learn-diagram .diag { max-height: 190px; }
.lp-qmedia { margin: 0 0 10px; }
.lp-qmedia img, .lp-qmedia video { display: block; width: 100%; height: auto; border-radius: 12px; }
.lp-qcontext { font-size: 14px; line-height: 1.5; color: var(--text-2); margin: 8px 0 0; }

.lpager-hint { text-align: center; font-size: 12px; color: var(--text-2); margin: 8px 0 0; }
@media (hover: none) { .lpager-hint { opacity: .8; } }

/* ============================================================================
   BRIGHT & PLAYFUL LEARN THEME (student feedback: "ugly, boring, too dark").
   Scoped to html.learn-open, which is already set on the root during Learn
   (js/feed/feed.js). Re-declaring the tokens here re-themes the whole student
   experience with no JS; it stacks with the [data-theme] toggle. All rules are
   html.learn-open-scoped so the main feed is untouched.
   ============================================================================ */
html.learn-open {
  color-scheme: light;
  --bg: #fff9f0; --surface: #ffffff; --surface-2: #fff3e0;
  --border: #ffe0b3; --text-1: #2b2118; --text-2: #7a6a56;
  --accent: #ff7a1a; --accent-2: #12b886; --good: #12b886;
  --track: #ffe6cc; --danger: #e8590c; --warn: #f08c00;
  /* per-subject accent (default = the theme accent; overridden by [data-subject]) */
  --subject: var(--accent); --subject-2: var(--accent-2);
  --card-shadow: 0 3px 0 rgba(0,0,0,.05), 0 10px 24px rgba(255,122,26,.13);
}
/* Still playful, but for readers who keep the app in dark mode. */
html.learn-open[data-theme="dark"] {
  color-scheme: dark;
  --bg: #171015; --surface: #241a20; --surface-2: #2e2029;
  --border: #3a2a33; --text-1: #fff7f0; --text-2: #c9b6a8;
  --accent: #ff8a3d; --accent-2: #2ee6a6; --good: #2ee6a6;
  --track: #3a2a33; --danger: #ff7a45; --warn: #ffb84d;
  --card-shadow: 0 6px 22px rgba(0,0,0,.5);
}
/* Per-subject colour, keyed off a data-subject slug set by learn.js. --subject drives
   card accents, progress, and enroll button; --accent stays the interactive colour. */
html.learn-open[data-subject="mathematics"], html.learn-open[data-subject="math"] { --subject: #4c6ef5; --subject-2: #7048e8; }
html.learn-open[data-subject="science"] { --subject: #12b886; --subject-2: #20c997; }
html.learn-open[data-subject="english"], html.learn-open[data-subject="language-arts"], html.learn-open[data-subject="french"] { --subject: #e64980; --subject-2: #f06595; }
html.learn-open[data-subject="history"], html.learn-open[data-subject="social-studies"], html.learn-open[data-subject="geography"] { --subject: #f76707; --subject-2: #ff922b; }

/* Headings in the playful display face, bigger and friendlier. */
html.learn-open .learn-head h1 { font-family: var(--font-display); font-size: 30px; }
html.learn-open .learn-step h2 { font-family: var(--font-display); font-size: 22px; }
html.learn-open .learn-card-title { font-family: var(--font-display); font-size: 18px; font-weight: 600; }
html.learn-open .lesson-title { font-family: var(--font-display); font-size: 27px; }

/* Cards: rounded, filled, soft playful shadow, springy press, a subject-tinted rail. */
html.learn-open .learn-card, html.learn-open .learn-unit, html.learn-open .learn-plan-item {
  border-radius: 20px; border: 2px solid var(--border); box-shadow: var(--card-shadow);
  transition: transform .12s ease, border-color .12s ease, box-shadow .12s ease;
}
html.learn-open .learn-card { position: relative; padding-left: 20px; }
html.learn-open .learn-card::before {
  content: ""; position: absolute; left: 0; top: 12px; bottom: 12px; width: 6px;
  border-radius: 6px; background: linear-gradient(var(--subject), var(--subject-2));
}
html.learn-open .learn-card:active, html.learn-open .learn-plan-item.is-open:active { transform: scale(.985); }
@media (hover: hover) {
  html.learn-open .learn-card:hover { border-color: var(--subject); box-shadow: var(--card-shadow), 0 0 0 3px color-mix(in srgb, var(--subject) 22%, transparent); }
}
/* Emoji/icon chip prepended by learn.js on subject cards + unit headers. */
html.learn-open .learn-card-icon, html.learn-open .learn-unit-icon { font-size: 24px; line-height: 1; }
html.learn-open .learn-unit h3 { font-family: var(--font-display); display: flex; align-items: center; gap: 8px; }

/* Topic rows: rounded, filled, subject chevron. */
html.learn-open .learn-topic { border-radius: 14px; padding: 12px 14px; background: var(--surface-2); }
html.learn-open .learn-topic-go { color: var(--subject); font-size: 18px; }

/* The one big call-to-action: a fat subject-coloured pill. */
html.learn-open .learn-enroll, html.learn-open .lp-forward {
  background: linear-gradient(135deg, var(--subject), var(--subject-2)); color: #fff;
  border-radius: 18px; font-family: var(--font-display); font-size: 18px; font-weight: 600;
  box-shadow: 0 4px 0 color-mix(in srgb, var(--subject) 55%, #000), 0 10px 22px color-mix(in srgb, var(--subject) 35%, transparent);
}
html.learn-open .learn-enroll:active, html.learn-open .lp-forward:active { transform: translateY(2px); box-shadow: 0 2px 0 color-mix(in srgb, var(--subject) 55%, #000); }

/* Chunky, cheerful progress bar. */
html.learn-open .learn-progress { height: 14px; border-radius: 8px; border: none; background: var(--track); }
html.learn-open .learn-progress > span { background: linear-gradient(90deg, var(--subject), var(--subject-2)); border-radius: 8px; }

/* Plan: bigger colourful number circles; mastered = gold, up-next = pulsing ring. */
html.learn-open .learn-plan-num { flex-basis: 30px; height: 30px; font-family: var(--font-display); font-size: 15px; }
html.learn-open .learn-plan-item.is-available .learn-plan-num { background: var(--subject); border-color: var(--subject); animation: learn-pulse 2s ease-in-out infinite; }
html.learn-open .learn-plan-item.is-mastered .learn-plan-num { background: var(--good); border-color: var(--good); color: #fff; }
html.learn-open .learn-plan-title { font-size: 16px; }
@keyframes learn-pulse { 0%,100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--subject) 45%, transparent); } 50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--subject) 0%, transparent); } }
@media (prefers-reduced-motion: reduce) { html.learn-open .learn-plan-item.is-available .learn-plan-num { animation: none; } }

/* Friendly page labels — drop the shouty uppercase grey "MATERIAL/STATS". */
html.learn-open .lp-side-head { text-transform: none; letter-spacing: 0; font-family: var(--font-display); font-size: 15px; font-weight: 600; color: var(--subject); }
html.learn-open .lesson-realworld > .lp-side-head { color: var(--subject); }

/* Big, tappable, colourful answer buttons — Learn-scoped so the feed's quiz is unchanged. */
html.learn-open .lp-post .opt, html.learn-open .lesson-quiz .opt, html.learn-open .lesson-quiz .quiz-opt {
  min-height: 64px; border-radius: 18px; border-width: 2px; font-size: 17px; font-weight: 700;
  box-shadow: 0 2px 0 rgba(0,0,0,.05); transition: transform .1s ease, border-color .1s ease, background .1s ease;
}
@media (hover: hover) {
  html.learn-open .lesson-quiz .quiz-opt:not(:disabled):hover { border-color: var(--subject); transform: translateY(-1px); }
}
html.learn-open .lesson-quiz .quiz-opt:not(:disabled):active { transform: translateY(1px); }

/* ---- student rewards: XP bar, streak flame, badge shelf (rewards.js) ---- */
html.learn-open .reward-row { display: flex; align-items: center; gap: 8px; margin-top: 12px; }
html.learn-open .xp-level {
  font-family: var(--font-display); font-size: 13px; font-weight: 600; color: #fff;
  background: linear-gradient(135deg, var(--subject), var(--subject-2)); padding: 4px 10px; border-radius: 999px; white-space: nowrap;
}
html.learn-open .xp-bar { position: relative; flex: 1; height: 22px; border-radius: 11px; background: var(--track); overflow: hidden; display: flex; align-items: center; justify-content: center; }
html.learn-open .xp-bar > span { position: absolute; left: 0; top: 0; bottom: 0; background: linear-gradient(90deg, var(--subject), var(--subject-2)); border-radius: 11px; transition: width .5s ease; }
html.learn-open .xp-bar > i { position: relative; z-index: 1; font-style: normal; font-size: 12px; font-weight: 700; color: var(--text-1); }
html.learn-open .streak-flame { font-family: var(--font-display); font-weight: 600; font-size: 15px; color: var(--danger); white-space: nowrap; }
html.learn-open .badge-shelf { margin: 16px 0 4px; }
html.learn-open .badge-row { display: flex; flex-wrap: wrap; gap: 8px; }
html.learn-open .badge {
  display: inline-flex; align-items: center; gap: 6px; font-size: 20px; line-height: 1;
  background: var(--surface); border: 2px solid var(--border); border-radius: 14px; padding: 8px 12px; box-shadow: var(--card-shadow);
}
html.learn-open .badge em { font-style: normal; font-size: 12px; font-weight: 700; color: var(--text-2); }

/* The same rewards, on the reader's own PROFILE ("Your learning"). The learn tab's rules are scoped to
   html.learn-open and use the per-subject theme; the profile isn't in that context, so restyle here
   using the app accent. */
.profile-learning { margin: 12px 0 18px; }
.profile-learning .reward-row { display: flex; align-items: center; gap: 8px; margin: 8px 0 14px; }
.profile-learning .xp-level { font-weight: 700; font-size: 13px; padding: 4px 12px; border-radius: 999px; background: var(--accent); color: #fff; white-space: nowrap; }
.profile-learning .xp-bar { position: relative; flex: 1; height: 22px; border-radius: 11px; background: var(--track, var(--surface)); border: 1px solid var(--border); overflow: hidden; display: flex; align-items: center; justify-content: center; }
.profile-learning .xp-bar > span { position: absolute; left: 0; top: 0; bottom: 0; background: var(--accent); border-radius: 11px; transition: width .5s ease; }
.profile-learning .xp-bar > i { position: relative; z-index: 1; font-style: normal; font-size: 12px; font-weight: 700; color: var(--text); }
.profile-learning .streak-flame { font-weight: 700; font-size: 15px; color: var(--danger); white-space: nowrap; }
.profile-learning .badge-shelf { margin: 12px 0 4px; }
.profile-learning .badge-shelf .lp-side-head { font-size: 15px; font-weight: 700; margin: 0 0 8px; }
.profile-learning .badge-row { display: flex; flex-wrap: wrap; gap: 8px; }
.profile-learning .badge {
  display: inline-flex; align-items: center; gap: 6px; font-size: 20px; line-height: 1;
  background: var(--surface); border: 2px solid var(--border); border-radius: 14px; padding: 8px 12px; box-shadow: var(--card-shadow);
}
.profile-learning .badge em { font-style: normal; font-size: 12px; font-weight: 700; color: var(--text-2); }

/* ---- inline SVG diagrams (diagram.js) ---- */
.learn-diagram { margin: 14px 0 4px; padding: 12px; border-radius: 12px; background: var(--surface); border: 1px solid var(--border); }
.learn-diagram figcaption { margin-top: 6px; font-size: 12px; color: var(--text-2); text-align: center; }
.learn-diagram .diag { display: block; width: 100%; height: auto; max-height: 300px; }
.diag-axis { stroke: var(--text-2); stroke-width: 1.5; }
.diag-tick { stroke: var(--text-2); stroke-width: 1.2; }
.diag-tick-label, .diag-mark-label, .diag-bars-label, .diag-line-label, .diag-fig-label { fill: var(--text-2); font-size: 11px; font-weight: 600; }
.diag-mark-label { fill: var(--text-1); }
.diag-mark { fill: var(--accent); }
.diag-mark.open { fill: var(--bg); stroke: var(--accent); stroke-width: 2; }
.diag-grid { stroke: var(--border); stroke-width: 1; }
.diag-plot-line { fill: none; stroke: var(--accent); stroke-width: 2.5; }
/* A second formula series on the same axes (plot.js function-plot / plot3d). */
.diag-plot-line.s2 { stroke: #e0a03a; }
.diag-line-label.s2 { fill: #e0a03a; }
/* Reusable interactive controls (controls.js) — used by the interactive plot and any future visual. */
.plot-params { margin-top: 8px; }
.controls { display: flex; flex-direction: column; gap: 8px; }
.ctl { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--text-1); }
.ctl-label { min-width: 74px; display: inline-flex; align-items: center; gap: 6px; }
.ctl-val { color: var(--accent); font-weight: 700; min-width: 28px; }
.ctl-slider input[type="range"] { flex: 1; accent-color: var(--accent); }
.ctl-toggle .ctl-label { min-width: 0; cursor: pointer; }
.ctl-select select { background: var(--bg); color: var(--text-1); border: 1px solid var(--border); border-radius: 8px; padding: 4px 8px; font: inherit; }
.ctl-btn { width: 30px; height: 30px; border: 1px solid var(--border); background: var(--surface); color: var(--text-1);
  border-radius: 8px; font-size: 18px; line-height: 1; cursor: pointer; }
@media (hover: hover) { .ctl-btn:hover { border-color: var(--accent); } }

/* ---- animated scenes (scene.js): CSS motion presets, no library ---- */
.diag.scene { max-height: 300px; }
.scene-label { fill: var(--text-1); font: 600 12px system-ui; }
.scene-part { transform-box: fill-box; transform-origin: center; }
.scene-jiggle { animation: scene-jiggle 1.5s ease-in-out infinite; }
.scene-pulse  { animation: scene-pulse 5s ease-in-out infinite; }
.scene-drift  { animation: scene-drift 9s ease-in-out infinite; }
.scene-orbit  { animation: scene-orbit 7s linear infinite; }
.scene-spin   { animation: scene-spin 8s linear infinite; }
.scene-float  { animation: scene-float 3.4s ease-in-out infinite; }
@keyframes scene-jiggle { 0%,100%{transform:translate(0,0)} 25%{transform:translate(2px,-3px)} 50%{transform:translate(-3px,2px)} 75%{transform:translate(3px,2px)} }
@keyframes scene-pulse  { 0%,100%{transform:scale(1)} 50%{transform:scale(1.05)} }
@keyframes scene-drift  { 0%,100%{transform:translate(0,0)} 50%{transform:translate(14px,10px)} }
@keyframes scene-orbit  { 0%{transform:translate(0,0)} 25%{transform:translate(-22px,16px)} 50%{transform:translate(-8px,34px)} 75%{transform:translate(16px,16px)} 100%{transform:translate(0,0)} }
@keyframes scene-spin   { from{transform:rotate(0)} to{transform:rotate(360deg)} }
@keyframes scene-float  { 0%,100%{transform:translateY(0)} 50%{transform:translateY(-8px)} }
@media (prefers-reduced-motion: reduce) { .scene-part { animation: none !important; } }

/* ---- live physics sims (sim.js): a responsive canvas ---- */
.diag-sim { display: block; width: 100%; height: auto; max-height: 260px; }
.diag-cell { fill: none; stroke: var(--text-2); stroke-width: 1.5; }
.diag-cell.fill { fill: var(--accent); stroke: var(--accent); }
.diag-fig { fill: none; stroke: var(--text-1); stroke-width: 2; }
.diag-fig.fill { fill: color-mix(in srgb, var(--accent) 40%, transparent); stroke: var(--accent); }
.diag-point { fill: var(--accent); }
/* draw-in animation on reveal */
.diag.draw polyline, .diag.draw line, .diag.draw polygon { animation: diag-fade 0.5s ease both; }
@keyframes diag-fade { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .diag.draw polyline, .diag.draw line, .diag.draw polygon { animation: none; } }

/* ---- lightweight math (mathlite.js) ---- */
.math { font-style: italic; white-space: nowrap; }
.mfrac { display: inline-flex; flex-direction: column; vertical-align: middle; text-align: center; font-style: normal; margin: 0 2px; }
.mfrac .mnum { border-bottom: 1.4px solid currentColor; padding: 0 3px; line-height: 1.1; }
.mfrac .mden { padding: 0 3px; line-height: 1.1; }
.mtext { font-style: normal; }
.msqrt { border-top: 1.4px solid currentColor; padding: 0 2px; }
.msqrt::before { content: '√'; border-top: 0; margin-left: -2px; }
.profile-head { display: flex; flex-direction: column; align-items: center; text-align: center; gap: 6px; }
.profile-head h1 { font-size: 22px; margin: 10px 0 0; }
.profile-country { color: var(--text-2); margin: 0; }
.profile-stats { display: flex; gap: 22px; margin-top: 14px; }
.profile-stat { display: flex; flex-direction: column; align-items: center; }
.profile-stat strong { font-size: 18px; }
.profile-stat span { color: var(--text-2); font-size: 13px; }
.profile-actions { display: flex; justify-content: center; gap: 10px; margin: 18px 0; }
.profile-posts-empty { text-align: center; }
.profile-posts { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
.profile-post {
  display: flex; flex-direction: column; gap: 6px; padding: 12px;
  border: 1px solid var(--border); border-radius: 12px; background: var(--surface);
  text-decoration: none; color: inherit;
}
.profile-post-q { font-weight: 600; font-size: 14px; }
.profile-post-meta { color: var(--text-2); font-size: 12px; }

/* ---- the swipe-right results view (Phase 16, REQUIREMENTS §7d) --------- */

/* Same reveal mechanism as .acct-only (app.css above) — rendered for every poll,
   hidden until html.analytics-enabled says the feature is actually on. */
.analytics-only { display: none; }
html.analytics-enabled .analytics-only { display: inline-flex; }

/* The categorical palette (dataviz skill, references/palette.md) — four validated
   slots, assigned by OPTION POSITION and never reassigned, so an option keeps one
   identity across the bars, the stacked breakdowns, the map and the legend. A fresh
   set rather than reusing --accent/--accent-2: this app's own accent pair does not
   map onto the same slot in both themes (light accent-2 is the palette's green,
   dark accent-2 is closer to its aqua), which would silently break the validated
   CVD ordering these four are picked to hold. */
:root {
  --series-1: #3987e5; --series-2: #008300; --series-3: #d55181; --series-4: #c98500;
  --diverging-a: #3987e5; --diverging-b: #e66767; --diverging-mid: #383835;
}
:root[data-theme="light"] {
  --series-1: #2a78d6; --series-2: #008300; --series-3: #e87ba4; --series-4: #eda100;
  --diverging-a: #2a78d6; --diverging-b: #e34948; --diverging-mid: #f0efec;
}

/* The results VIEW — the pager's right-hand page, not an overlay. A full-height column
   that scrolls vertically for "more stats", sitting to the right of the feed until a
   swipe (or the poll's results button) brings it on screen. */
.results-page {
  flex: 0 0 100vw; width: 100vw;
  height: 100dvh; overflow-y: auto;
  display: flex; flex-direction: column;
  background: var(--bg); color: var(--text-1);
  scrollbar-width: none;
}
.results-page::-webkit-scrollbar { display: none; }

/* Sticky header with the "back to the poll" control, clear of the fixed topbar. */
.results-head {
  position: sticky; top: 0; z-index: 1;
  display: flex; align-items: center; gap: 10px;
  padding: calc(max(10px, env(safe-area-inset-top)) + 58px) 20px 12px;
  background: linear-gradient(var(--bg) 72%, transparent);
}
.results-head h2 { margin: 0; font-size: 18px; font-weight: 750; min-width: 0;
  overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.results-back {
  flex: 0 0 auto; display: inline-flex; align-items: center; gap: 4px;
  font: inherit; font-weight: 700; font-size: 14px; cursor: pointer;
  color: var(--text-1); background: var(--surface); border: 1px solid var(--border);
  border-radius: 999px; padding: 7px 14px 7px 10px; min-height: 40px;
}
.results-back:active { transform: scale(.97); }

/* ---- the context page (the pager's LEFT view, 2026-07-23) ------------------
   Shares .results-page's frame; what differs is the head (its Poll button points
   RIGHT, toward where the poll actually lies) and its own content classes, all
   built by js/context.js. */
.context-page .results-back { margin-left: auto; padding: 7px 10px 7px 14px; }
.context-question {
  margin: 0 0 14px; font-size: 22px; font-weight: 750; line-height: 1.25;
  letter-spacing: -0.01em; text-wrap: balance;
}
.context-background { margin: 0 0 18px; font-size: 16px; line-height: 1.6; }
.context-notes { margin: 0 0 18px; }
.context-notes dt { font-weight: 700; font-size: 14px; margin: 10px 0 2px; }
.context-notes dd { margin: 0; font-size: 14px; color: var(--text-2); line-height: 1.5; }
.context-source { font-size: 14px; }
.context-source a { color: var(--accent); }
.context-disclosure {
  margin-top: 18px; font-size: 12px; color: var(--text-2);
}
.results-back:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
@media (hover: hover) { .results-back:hover { border-color: var(--accent); } }

.results-body {
  min-height: 0; padding: 0 20px calc(max(18px, env(safe-area-inset-bottom)) + var(--navbar-h));
  width: 100%; max-width: 640px; margin: 0 auto;
}
.results-body h3 {
  margin: 18px 0 8px; font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-2);
}
.results-body h3 {
  margin: 18px 0 8px; font-size: 12px; font-weight: 700;
  text-transform: uppercase; letter-spacing: .06em; color: var(--text-2);
}
.results-body h3:first-child { margin-top: 0; }

/* ---- overview: one bar per option, sorted by share ---------------------- */
.results-bars { display: flex; flex-direction: column; gap: 10px; }
.results-bar-row { display: flex; align-items: center; gap: 10px; }
.results-bar-label { flex: 0 0 auto; max-width: 40%; font-size: 14px; font-weight: 600; }
.results-bar-track {
  flex: 1 1 auto; height: 20px; border-radius: 6px; background: var(--track);
  overflow: hidden; min-width: 0;
}
/* Bar spec (dataviz skill): rounded data-end, square at the baseline — here the
   "baseline" is the track's own left edge, so the fill only rounds its trailing end. */
.results-bar-fill { display: block; height: 100%; border-radius: 0 4px 4px 0; }
.results-bar-value { flex: 0 0 auto; font-size: 13px; font-weight: 700; font-variant-numeric: tabular-nums; }

/* ---- dimension tabs ------------------------------------------------------ */
.results-tabs { display: flex; gap: 6px; margin-bottom: 10px; flex-wrap: wrap; }
.results-tab {
  font: inherit; font-size: 13px; font-weight: 700; cursor: pointer;
  background: var(--surface-2); border: 1px solid var(--border); color: var(--text-2);
  border-radius: 999px; padding: 7px 14px; min-height: 36px;
}
.results-tab.on { color: var(--text-1); border-color: var(--accent); background: color-mix(in srgb, var(--accent) 14%, var(--surface-2)); }

/* ---- age/gender breakdown: 100%-stacked bars, one row per bucket -------- */
.results-bucket { display: flex; align-items: center; gap: 10px; margin-bottom: 8px; }
.results-bucket-label { flex: 0 0 72px; font-size: 13px; font-weight: 700; }
.results-stack {
  flex: 1 1 auto; display: flex; height: 18px; border-radius: 6px; overflow: hidden;
  background: var(--track); min-width: 0;
  /* The 2px surface gap between touching segments (mark spec) — a border would add
     data-weight ink that isn't data, so every segment is inset by half the gap instead. */
  gap: 2px;
}
.results-segment { display: block; height: 100%; }
.results-bucket-note { flex: 0 0 auto; font-size: 12px; min-width: 88px; text-align: right; }

/* R16.24a: a suppressed cell/bucket is never zero, never the lowest colour step, never
   simply absent — it renders hatched everywhere it appears. Always-on, unlike the
   dataviz skill's own CVD texture channel (which is opt-in) — this hatching is a
   STATUS, not an identity-backup channel, so it does not wait on an accessibility
   setting. A CSS gradient works here (a plain <div>); the map's own suppressed
   countries use a real SVG <pattern> instead — see results.js's ensureHatchPattern —
   since `background-image` has no effect on an SVG path's fill. */
.results-stack.suppressed {
  background-image: repeating-linear-gradient(45deg,
    var(--border) 0, var(--border) 2px, transparent 2px, transparent 8px);
  background-color: var(--surface-2);
}

/* ---- legend: identity is never colour alone ------------------------------ */
.results-legend { display: flex; flex-wrap: wrap; gap: 10px 16px; margin: 10px 0; }
.results-legend-item { display: inline-flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-2); }
.results-swatch { width: 12px; height: 12px; border-radius: 3px; flex: 0 0 auto; }

/* One rule per slot covers every place a slot number appears — bars, segments, map
   fills (as a CSS var, not a class, since paths are coloured from JS) and swatches. */
.series-1 { background: var(--series-1); fill: var(--series-1); }
.series-2 { background: var(--series-2); fill: var(--series-2); }
.series-3 { background: var(--series-3); fill: var(--series-3); }
.series-4 { background: var(--series-4); fill: var(--series-4); }

/* ---- the choropleth (never a dot per voter — R16.17/R16.18) -------------- */
.results-map { margin: 10px 0; }
.results-map svg { width: 100%; height: auto; display: block; }
.results-map path {
  fill: var(--track); stroke: var(--surface); stroke-width: .5;
}
.results-map path.has-data { cursor: default; }

/* ---- participation timeline: a line, never a second axis (R16.23c) ------ */
.results-timeline { width: 100%; height: 100px; display: block; }
.results-timeline-line { fill: none; stroke: var(--series-1); stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
.results-timeline-area { fill: var(--series-1); opacity: .1; stroke: none; }

/* ---- table view: the accessible version, and the thing people copy ------ */
.results-table-host { overflow-x: auto; }
.results-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.results-table th, .results-table td {
  padding: 8px 10px; text-align: right; border-bottom: 1px solid var(--border);
  font-variant-numeric: tabular-nums; white-space: nowrap;
}
.results-table th:first-child, .results-table td:first-child { text-align: left; font-variant-numeric: normal; }
.results-table th { color: var(--text-2); font-weight: 700; font-size: 12px; text-transform: uppercase; letter-spacing: .04em; }

.results-note { color: var(--text-2); font-size: 14px; padding: 16px 0; text-align: center; }
.results-caption, .results-credit { font-size: 12px; margin-top: 18px; }
.results-credit a { color: inherit; }

@media (prefers-reduced-motion: reduce) {
  * { animation-duration: .01ms !important; transition-duration: .01ms !important; }
  #feed { scroll-behavior: auto; }
}

/* ============================================================================
   Maxi — the Learn companion (js/learn/companion.js). A fixed mascot + speech
   bubble, visible ONLY under html.learn-open so it belongs to Learn and never
   the feed. Mounted at <body> level (outside the pager) so overflow can't clip
   it. Below the explain sheet (z 60) so a tutor sheet covers it.
   ============================================================================ */
.companion { display: none; }
html.learn-open .companion {
  display: block; position: fixed; z-index: 55;
  right: 12px; bottom: calc(90px + env(safe-area-inset-bottom, 0px));
  pointer-events: none;   /* the gap is click-through; children re-enable it */
}
.companion-mascot {
  pointer-events: auto; width: 88px; height: 94px; padding: 0; border: 0;
  background: none; cursor: pointer; display: block; margin-left: auto;
  filter: drop-shadow(0 7px 12px rgba(0,0,0,.26));
  animation: maxi-bob 3.6s ease-in-out infinite;
}
.companion-mascot:focus-visible { outline: 3px solid var(--accent); outline-offset: 3px; border-radius: 22px; }
@keyframes maxi-bob { 0%,100% { transform: translateY(0) rotate(-1.5deg) } 50% { transform: translateY(-7px) rotate(1.5deg) } }
.maxi-pop { animation: maxi-pop-kf .44s ease; }
@keyframes maxi-pop-kf { 0% { transform: scale(1) } 35% { transform: scale(1.18) rotate(-5deg) } 100% { transform: scale(1) } }

.maxi-face { width: 100%; height: 100%; overflow: visible; }
.maxi-face .maxi-shadow { fill: rgba(0,0,0,.13); }
.maxi-face .maxi-glow { fill: url(#maxiGlow); transform-origin: 60px 64px; transform-box: fill-box;
  animation: maxi-glow 3.2s ease-in-out infinite; }
@keyframes maxi-glow { 0%,100% { opacity: .55; transform: scale(.94) } 50% { opacity: .9; transform: scale(1.06) } }
.maxi-face .maxi-ear { fill: #f5a51e; }
.maxi-face .maxi-ear-in { fill: #0e857a; }
.maxi-face .maxi-cheek { fill: #ff8fb0; opacity: .55; }
.maxi-face .maxi-pupil { fill: #22303a; }
.maxi-face .maxi-shine-dot { fill: #fff; }
.maxi-face .maxi-stalk { stroke: #0e857a; stroke-width: 3; stroke-linecap: round; fill: none; }
.maxi-face .maxi-star { fill: #ffd43b; stroke: #f5a51e; stroke-width: 1; transform-origin: 60px 10px;
  transform-box: fill-box; animation: maxi-twinkle 2.4s ease-in-out infinite; }
.maxi-face .maxi-antenna { transform-origin: 60px 34px; transform-box: fill-box; animation: maxi-sway 3.8s ease-in-out infinite; }
@keyframes maxi-sway { 0%,100% { transform: rotate(-6deg) } 50% { transform: rotate(6deg) } }
@keyframes maxi-twinkle { 0%,100% { transform: scale(1); opacity: 1 } 50% { transform: scale(1.25); opacity: .8 } }
/* the strokes (eye arcs, brows, smile, thinking line, waving arm) */
.maxi-face path[fill="none"], .maxi-face .p-eyes-happy path, .maxi-face .p-eyes-wink path, .maxi-face .p-brows path {
  stroke: #22303a; stroke-width: 3.4; stroke-linecap: round; stroke-linejoin: round; fill: none;
}
.maxi-face .maxi-arm { stroke: #12a394; stroke-width: 6; }
.maxi-face .maxi-glove { fill: #ffc94d; stroke: #f5a51e; stroke-width: 1.5; }
.maxi-face .p-wonder { fill: #7c5cff; }

/* sparkle aura */
.maxi-face .maxi-orbit .tw { fill: #ffd43b; transform-box: fill-box; transform-origin: center;
  animation: maxi-tw 2.6s ease-in-out infinite; }
.maxi-face .maxi-orbit .d1 { fill: #ff9ecd; } .maxi-face .maxi-orbit .d2 { fill: #7dd3fc; }
.maxi-face .maxi-orbit .d3 { fill: #b794f6; } .maxi-face .maxi-orbit .d4 { fill: #ffd43b; }
.maxi-face .maxi-orbit .t2 { animation-delay: -.5s } .maxi-face .maxi-orbit .t3 { animation-delay: -1.2s }
.maxi-face .maxi-orbit .d1 { animation-delay: -.8s } .maxi-face .maxi-orbit .d2 { animation-delay: -1.6s }
.maxi-face .maxi-orbit .d3 { animation-delay: -2.1s } .maxi-face .maxi-orbit .d4 { animation-delay: -.3s }
@keyframes maxi-tw { 0%,100% { opacity: .25; transform: scale(.6) } 50% { opacity: 1; transform: scale(1.1) } }

/* Expressions: hide every swappable part, then reveal the set this mood needs. */
.maxi-face .p { display: none; }
.maxi-face[data-mood="idle"]  .p-eyes-open, .maxi-face[data-mood="idle"]  .p-smile,
.maxi-face[data-mood="happy"] .p-eyes-open, .maxi-face[data-mood="happy"] .p-smile,
.maxi-face[data-mood="wave"]  .p-eyes-open, .maxi-face[data-mood="wave"]  .p-smile, .maxi-face[data-mood="wave"] .p-hand,
.maxi-face[data-mood="cheer"] .p-eyes-happy, .maxi-face[data-mood="cheer"] .p-grin,
.maxi-face[data-mood="wink"]  .p-eyes-wink, .maxi-face[data-mood="wink"]  .p-smile,
.maxi-face[data-mood="wonder"] .p-eyes-wide, .maxi-face[data-mood="wonder"] .p-smile, .maxi-face[data-mood="wonder"] .p-brows, .maxi-face[data-mood="wonder"] .p-wonder,
.maxi-face[data-mood="wow"]   .p-eyes-wide, .maxi-face[data-mood="wow"]   .p-o, .maxi-face[data-mood="wow"] .p-brows, .maxi-face[data-mood="wow"] .p-wonder,
.maxi-face[data-mood="oops"]  .p-eyes-open, .maxi-face[data-mood="oops"]  .p-o, .maxi-face[data-mood="oops"] .p-sweat,
.maxi-face[data-mood="think"] .p-eyes-wide, .maxi-face[data-mood="think"] .p-line, .maxi-face[data-mood="think"] .p-brows, .maxi-face[data-mood="think"] .p-wonder {
  display: block;
}
/* Blink: JS toggles .is-blink briefly; squash whichever eyes are showing. */
.maxi-face .maxi-eyes { transform-box: fill-box; transform-origin: 50% 50%; transition: transform .08s ease; }
.maxi-face.is-blink .maxi-eyes { transform: scaleY(.12); }
/* A friendly wave. */
.maxi-face[data-mood="wave"] .p-hand { transform-box: fill-box; transform-origin: 0% 60%; animation: maxi-wave .55s ease-in-out 3; }
@keyframes maxi-wave { 0%,100% { transform: rotate(0) } 50% { transform: rotate(-26deg) } }
/* The wonder "?" floats up. */
.maxi-face .p-wonder { transform-box: fill-box; transform-origin: center; animation: maxi-wonder 1.6s ease-in-out infinite; }
@keyframes maxi-wonder { 0%,100% { transform: translateY(0) rotate(-6deg); opacity: .85 } 50% { transform: translateY(-4px) rotate(6deg); opacity: 1 } }

@media (prefers-reduced-motion: reduce) {
  .companion-mascot, .maxi-face .maxi-glow, .maxi-face .maxi-star, .maxi-face .maxi-antenna,
  .maxi-face .maxi-orbit .tw, .maxi-face .p-wonder, .maxi-face[data-mood="wave"] .p-hand { animation: none; }
}

.companion-bubble {
  pointer-events: auto; position: absolute; right: 0; bottom: 82px; width: min(240px, 68vw);
  background: var(--surface); color: var(--text-1);
  border: 2px solid var(--border); border-radius: 16px; padding: 11px 30px 11px 13px;
  box-shadow: var(--card-shadow, 0 8px 24px rgba(0,0,0,.18));
  font-size: 14px; line-height: 1.35; transform-origin: bottom right;
  animation: maxi-bubble-in .22s ease;
}
.companion-bubble[hidden] { display: none; }
@keyframes maxi-bubble-in { from { opacity: 0; transform: scale(.85) translateY(6px) } to { opacity: 1; transform: none } }
.companion-bubble::after {
  content: ""; position: absolute; right: 26px; bottom: -9px; width: 16px; height: 16px;
  background: var(--surface); border-right: 2px solid var(--border); border-bottom: 2px solid var(--border);
  transform: rotate(45deg);
}
.companion-text { margin: 0; max-height: 40vh; overflow-y: auto; }
.companion-actions:not(:empty) { margin-top: 8px; }
.companion-btn {
  font: 600 13px inherit; cursor: pointer; width: 100%;
  padding: 7px 10px; border-radius: 10px; border: 0;
  background: var(--accent); color: #fff;
}
.companion-btn:focus-visible { outline: 2px solid var(--text-1); outline-offset: 2px; }
.companion-ai { margin: 6px 0 0; font-size: 11px; color: var(--text-2); }
.companion-dismiss {
  position: absolute; top: 4px; right: 6px; width: 22px; height: 22px; padding: 0;
  border: 0; background: none; color: var(--text-2); font-size: 18px; line-height: 1; cursor: pointer;
}
.companion-dismiss:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; border-radius: 4px; }

/* ============================================================================
   Speed Match — the in-lesson math game (js/learn/games/). The fifth pager page.
   Scoped to html.learn-open so it inherits the playful Learn theme tokens.
   ============================================================================ */
.lp-actions { display: flex; flex-wrap: wrap; gap: 10px; }
.lp-game-cta { background: color-mix(in srgb, var(--accent-2) 16%, transparent); }
.lp-gamewrap { text-align: center; }
.sm-note { color: var(--text-2); padding: 24px 8px; }
.sm { display: flex; flex-direction: column; gap: 12px; }
.sm-head { display: flex; align-items: center; justify-content: space-between; font-weight: 700; }
.sm-score { font-family: var(--font-display, inherit); font-size: 24px; color: var(--accent); }
.sm-streak { color: var(--danger); font-size: 14px; }
.sm-timer { height: 10px; border-radius: 999px; background: var(--track); overflow: hidden; }
.sm-timer-fill { display: block; height: 100%; width: 100%; background: var(--accent-2); transition: width .12s linear; }
.sm-timer-fill.is-low { background: var(--danger); }
.sm-prompt { font-size: 22px; font-weight: 700; margin: 8px 0 4px; }
.sm-choices { display: grid; grid-template-columns: repeat(2, 1fr); gap: 10px; }
.sm-choice {
  position: relative; display: flex; align-items: center; gap: 8px; justify-content: center;
  padding: 16px 12px; font-size: 18px; font-weight: 600; cursor: pointer;
  border: 2px solid var(--border); border-radius: 14px; background: var(--surface); color: var(--text-1);
  transition: transform .08s ease, border-color .12s ease, background .12s ease;
}
@media (hover: hover) { .sm-choice:hover { border-color: var(--accent); } }
.sm-choice:active { transform: translateY(2px); }
.sm-choice .sm-key {
  position: absolute; top: 4px; left: 6px; font-size: 11px; font-weight: 700; color: var(--text-2);
  width: 16px; height: 16px; border-radius: 5px; background: var(--surface-2); display: grid; place-items: center;
}
.sm-choice.is-right { border-color: var(--good); background: color-mix(in srgb, var(--good) 18%, var(--surface)); }
.sm-choice.is-wrong { border-color: var(--danger); background: color-mix(in srgb, var(--danger) 16%, var(--surface)); }
.sm-feedback { font-weight: 700; margin: 6px 0 0; }
.sm-feedback.is-right { color: var(--good); }
.sm-feedback.is-wrong { color: var(--danger); }
.sm-done { padding: 20px 8px; }
.sm-done-title { font-family: var(--font-display, inherit); font-size: 24px; margin: 0 0 6px; }
.sm-done-score { font-size: 34px; font-weight: 800; color: var(--accent); margin: 0; }
.sm-done-sub { color: var(--text-2); margin: 4px 0 16px; }
.sm-replay {
  padding: 12px 24px; font-size: 16px; font-weight: 700; cursor: pointer; color: #fff;
  border: 0; border-radius: 999px; background: var(--accent);
}
.sm-replay:active { transform: translateY(2px); }

/* Games picker (lesson games page) + Bubble Pop template. Scoped via html.learn-open tokens. */
.game-picker { display: grid; gap: 12px; margin-top: 10px; }
.game-pick {
  display: flex; flex-direction: column; gap: 4px; text-align: left; cursor: pointer;
  padding: 16px; border: 2px solid var(--border); border-radius: 16px; background: var(--surface); color: var(--text-1);
  transition: transform .08s ease, border-color .12s ease;
}
.game-pick:active { transform: translateY(2px); }
@media (hover: hover) { .game-pick:hover { border-color: var(--accent); } }
.game-pick-title { font-family: var(--font-display, inherit); font-size: 19px; font-weight: 700; }
.game-pick-blurb { color: var(--text-2); font-size: 14px; }
.lp-game-change { display: block; margin: 12px auto 0; }

.bp { display: flex; flex-direction: column; gap: 10px; }
.bp-head { display: flex; align-items: center; justify-content: space-between; font-weight: 700; }
.bp-score { font-family: var(--font-display, inherit); font-size: 24px; color: var(--accent); }
.bp-streak { color: var(--danger); font-size: 14px; }
.bp-prompt { font-size: 22px; font-weight: 700; text-align: center; margin: 4px 0; min-height: 28px; }
.bp-arena { position: relative; height: 300px; border-radius: 16px; overflow: hidden;
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent-2) 12%, var(--surface)), var(--surface)); }
.bp-arena.is-urgent { background: linear-gradient(180deg, color-mix(in srgb, var(--danger) 14%, var(--surface)), var(--surface)); }
.bp-bubble {
  /* Centred on its left% anchor by margin-left ONLY — the game overrides `transform` each frame to
     float the bubble up, so centering must not live in transform. */
  position: absolute; margin-left: -33px;
  display: grid; place-items: center; width: 66px; height: 66px; padding: 0; cursor: pointer;
  font-size: 17px; font-weight: 700; color: var(--text-1);
  border: 2px solid color-mix(in srgb, var(--accent) 45%, var(--border)); border-radius: 50%;
  background: radial-gradient(circle at 34% 30%, #fff8, color-mix(in srgb, var(--accent) 22%, var(--surface)));
  transition: transform .16s linear, border-color .12s ease, background .12s ease;
}
@media (hover: hover) { .bp-bubble:hover { border-color: var(--accent); } }
.bp-bubble .bp-key { position: absolute; top: 2px; left: 8px; font-size: 10px; color: var(--text-2); }
.bp-bubble.is-right { border-color: var(--good); background: radial-gradient(circle at 34% 30%, #fff8, color-mix(in srgb, var(--good) 32%, var(--surface))); }
.bp-bubble.is-wrong { border-color: var(--danger); opacity: .6; }
.bp-done { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; gap: 4px; }
.bp-done-title { font-family: var(--font-display, inherit); font-size: 22px; margin: 0; }
.bp-done-score { font-size: 32px; font-weight: 800; color: var(--accent); margin: 0; }
.bp-done-sub { color: var(--text-2); margin: 0 0 12px; }
.bp-replay { padding: 12px 24px; font-size: 16px; font-weight: 700; cursor: pointer; color: #fff; border: 0; border-radius: 999px; background: var(--accent); }

/* Number Hop template: lily pads across a pond, a frog that hops to the picked pad. Same tokens. */
.nh { display: flex; flex-direction: column; gap: 10px; }
.nh-head { display: flex; align-items: center; justify-content: space-between; font-weight: 700; }
.nh-score { font-family: var(--font-display, inherit); font-size: 24px; color: var(--accent); }
.nh-streak { color: var(--danger); font-size: 14px; }
.nh-prompt { font-size: 22px; font-weight: 700; text-align: center; margin: 4px 0; min-height: 28px; }
.nh-timer { height: 10px; border-radius: 999px; background: var(--track); overflow: hidden; }
.nh-timer-fill { display: block; height: 100%; width: 100%; background: var(--accent-2); transition: width .12s linear; }
.nh-timer-fill.is-low { background: var(--danger); }
.nh-pond { position: relative; height: 210px; border-radius: 16px; overflow: hidden;
  background: linear-gradient(180deg, color-mix(in srgb, var(--accent-2) 16%, var(--surface)), color-mix(in srgb, var(--accent) 10%, var(--surface))); }
.nh-pads { position: absolute; inset: 0; }
.nh-pad {
  /* Centred on its left% anchor by margin-left only. */
  position: absolute; bottom: 24%; margin-left: -40px; width: 80px; min-height: 54px;
  display: grid; place-items: center; padding: 8px 6px; cursor: pointer;
  font-size: 17px; font-weight: 700; color: var(--text-1);
  border: 2px solid color-mix(in srgb, var(--good) 40%, var(--border));
  border-radius: 50% 50% 46% 46% / 60% 60% 40% 40%;
  background: radial-gradient(circle at 40% 30%, #fff6, color-mix(in srgb, var(--good) 20%, var(--surface)));
  transition: transform .1s ease, border-color .12s ease, background .12s ease;
}
@media (hover: hover) { .nh-pad:hover { border-color: var(--accent); } }
.nh-pad:active { transform: translateY(2px); }
.nh-pad .nh-key { position: absolute; top: 2px; left: 8px; font-size: 10px; color: var(--text-2); }
.nh-pad.is-right { border-color: var(--good); background: radial-gradient(circle at 40% 30%, #fff8, color-mix(in srgb, var(--good) 34%, var(--surface))); }
.nh-pad.is-wrong { border-color: var(--danger); opacity: .6; }
.nh-frog { position: absolute; bottom: 40%; margin-left: -15px; font-size: 30px; line-height: 1; z-index: 2;
  transition: left .28s cubic-bezier(.5, -0.35, .5, 1.4); }
.nh-frog.is-hop { animation: nh-hop .3s ease; }
@keyframes nh-hop { 0%, 100% { transform: translateY(0); } 45% { transform: translateY(-26px); } }
.nh-done { position: absolute; inset: 0; display: grid; place-content: center; text-align: center; gap: 4px; }
.nh-done-title { font-family: var(--font-display, inherit); font-size: 22px; margin: 0; }
.nh-done-score { font-size: 32px; font-weight: 800; color: var(--accent); margin: 0; }
.nh-done-sub { color: var(--text-2); margin: 0 0 12px; }
.nh-replay { padding: 12px 24px; font-size: 16px; font-weight: 700; cursor: pointer; color: #fff; border: 0; border-radius: 999px; background: var(--accent); }
@media (prefers-reduced-motion: reduce) { .nh-frog { transition: none; } .nh-frog.is-hop { animation: none; } }

/* Math-game feed card (Phase C): a tap-to-play card that mounts the live game in place. */
.badge.game-badge { color: var(--accent-2); border-color: color-mix(in srgb, var(--accent-2) 40%, transparent); }
.slide.game .game-host { width: 100%; max-width: 460px; margin: 0 auto; }
.feed-game-picker { display: grid; gap: 10px; margin-top: 12px; }
.feed-game-picker .game-play { font-size: 16px; }

/* "Phone a friend" hint (Learn quiz): the ask-Maxi button, and a ruled-out option. */
.quiz-hint {
  display: inline-block; margin: 4px 0 2px; padding: 8px 14px; cursor: pointer;
  font: 600 14px inherit; border-radius: 999px; border: 1px dashed var(--accent-2, var(--accent));
  background: color-mix(in srgb, var(--accent-2, var(--accent)) 12%, transparent); color: var(--text-1);
}
.quiz-hint:disabled { opacity: .6; cursor: default; }
.quiz-hint:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.quiz-opt.is-eliminated {
  opacity: .45; cursor: not-allowed; text-decoration: line-through;
  border-style: dashed; filter: grayscale(0.6);
}
.quiz-opt.is-eliminated .tick, .quiz-opt.is-eliminated .label { text-decoration: line-through; }

/* A personal-course lesson being generated in the background (js/learn/lesson.js showBuilding). */
.lesson-building { text-align: center; padding: 40px 16px; }
.lesson-building .spinner { margin: 0 auto 16px; }
.lesson-building-title { font-family: var(--font-display, inherit); font-size: 20px; font-weight: 700; margin: 0 0 6px; }
