/* Light palette. The theme is chosen by a head script in base.html that sets
   data-theme on <html> (stored choice, else OS preference) before this file
   loads. color-scheme keeps native widgets (inputs, scrollbars) in step. */
:root {
  color-scheme: light;
  --ink: #1a1a1a;
  --muted: #666;
  --faint: #6f6f6f;              /* WCAG AA: 4.90:1 on --bg, 5.02:1 on --panel (was #8a8a8a, 3.37/3.45:1) */
  --rule: #e2e0dc;
  /* A form control's boundary is NOT a divider. --rule is the hairline
     divider colour (~1.3:1) and is exactly right for the ~30 dividers, card
     edges and chips it paints; it was also doing duty as the resting border
     of every text input, select and textarea, where WCAG 2.1 SC 1.4.11 wants
     3:1 for "visual information required to identify user interface
     components". Inside a panel the control's own --panel fill adds nothing
     (1.00:1), so that hairline was the ONLY thing saying a field was there.
     Two variables, not one raised variable: darkening --rule to 3:1 would
     turn every hairline on the site into a heavy line (it#463). */
  --field-border: #8e8980;       /* WCAG 1.4.11: 3.39:1 on --bg, 3.48:1 on --panel, 3.22:1 on --panel-hover */
  --bg: #fdfcfa;
  --panel: #fff;
  --panel-hover: #f7f6f4;
  --accent: #014f86;
  --accent-strong: #013b64;      /* accent-button hover */
  --accent-contrast: #fff;       /* text on accent */
  --accent-soft: #e6f1f8;
  --accent-soft-hover: #d8e9f6;
  --accent-border: #c9def0;      /* border on accent-soft chips */
  --ok-bg: #eef6ee;   --ok-border: #cfe3cf;   --ok-ink: #2f6b34;
  /* verified-open chip/banner (issue #36): a distinct, more-saturated emerald
     so it does not read as the sage --ok "resolved" green */
  --open-bg: #e3f5ec; --open-border: #b3e0c8; --open-ink: #10794e;
  --warn-bg: #fff4e5; --warn-border: #f0dcc0; --warn-ink: #7a5100;
  --err-bg: #fdecec;  --err-border: #f0c9c9;  --err-ink: #8a2020;
  --info-bg: #eef2f8; --info-border: #d4dded; --info-ink: #33507d;
  --owner-bg: #f4effa; --owner-border: #ddd0ee; --owner-ink: #5b3d84;
  --danger: #a12626;
  --chip-bg: #fafafa;
  --score-bg: #eceae6;
  --vote-idle: #8f8a82;   /* 3.34:1 on --bg — WCAG 1.4.11 non-text pass (was #c9c4bc, 1.69:1); see #130 */
  --avatar-gone-bg: #f0efec;
  --shadow: 0 3px 10px rgba(0,0,0,.08);
  /* #113: edge-shadow colour for the display-math scroll cue. A dark shadow
     reads on the light panel; the dark theme overrides it to a soft light
     glow, since a black shadow is invisible on the dark panel. */
  --math-cue: rgba(0,0,0,.34);
  /* #113's two `local` mask layers hide the scroll shadow by painting the
     colour BEHIND the equation, so they must equal that colour or they paint
     a visible block instead of hiding one. Default to the page background —
     comments, .note, the detail-page Summary and the live .comment-preview
     all sit on --bg. Every container that gives an equation a different
     backdrop re-declares --math-mask NEXT TO its own `background:` line, so
     the two can never drift apart. */
  --math-mask: var(--bg);
  /* comment avatars: hue is per-user (inline style), theme sets the rest */
  --av-s: 42%; --av-l: 90%; --av-fg-s: 45%; --av-fg-l: 32%;
  --max: 68rem;
}

html[data-theme="dark"] {
  color-scheme: dark;
  --ink: #e6e4e1;
  --muted: #a39f98;
  --faint: #9a958c;              /* WCAG AA: 5.58:1 on --panel, 5.06:1 on --panel-hover (was #837f78, 4.17/3.78:1) */
  --rule: #3b3936;
  --field-border: #78736a;       /* WCAG 1.4.11: 3.84:1 on --bg, 3.53:1 on --panel, 3.20:1 on --panel-hover */
  --bg: #171614;
  --panel: #201e1c;
  --panel-hover: #282624;
  --math-cue: rgba(255,255,255,.30);   /* #113: light glow reads on dark panel */
  --accent: #7db4dd;
  --accent-strong: #a3c9e7;
  --accent-contrast: #10202e;
  --accent-soft: #1c2f42;
  --accent-soft-hover: #24405a;
  --accent-border: #2e4d6b;
  --ok-bg: #1d2a1e;   --ok-border: #31502f;   --ok-ink: #93c493;
  --open-bg: #12271d; --open-border: #245a3c; --open-ink: #5fd39b;
  --warn-bg: #2e2416; --warn-border: #55431f; --warn-ink: #d3a94f;
  --err-bg: #2e1a1a;  --err-border: #55302f;  --err-ink: #dd9090;
  --info-bg: #1c2331; --info-border: #2f405c; --info-ink: #9fb4d8;
  --owner-bg: #271f31; --owner-border: #443358; --owner-ink: #bda3dd;
  --danger: #dd8f8f;
  --chip-bg: #262421;
  --score-bg: #33302d;
  --vote-idle: #787369;   /* 3.84:1 on dark --bg — WCAG 1.4.11 non-text pass (was #57534d, 2.37:1); see #130 */
  --avatar-gone-bg: #2b2926;
  --shadow: 0 3px 10px rgba(0,0,0,.45);
  --av-s: 28%; --av-l: 24%; --av-fg-s: 42%; --av-fg-l: 78%;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  background: var(--bg);
  color: var(--ink);
  font: 16px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}

a { color: var(--accent); text-decoration: none; }
a:hover { text-shadow: 0.4px 0 0 currentColor; text-decoration: none; }

.wrap { max-width: var(--max); margin: 0 auto; padding: 0 1.5rem; }

/* ---- header ---- */
header.site {
  border-bottom: 1px solid var(--rule);
  background: var(--panel); --math-mask: var(--panel);
}
header.site .wrap {
  display: flex; align-items: baseline; gap: 1.5rem;
  padding-top: 1rem; padding-bottom: 1rem;
  /* Wrap at EVERY width, not only below the collapse breakpoint (issue-tracker
     #273). Flex line-breaking uses each item's max-content size, so when the
     nav no longer fits beside the brand the whole <nav> moves to a second line
     intact. Without this the row is nowrap, the nav is squeezed instead, and
     its own children wrap mid-list — the #70/#125 orphaned-nav layout. The
     breakpoint below cannot prevent that on its own because the nav's width
     depends on the signed-in member's display name (up to 24 chars), which no
     fixed pixel value can bound. See the breakpoint comment. */
  flex-wrap: wrap;
  row-gap: .75rem;
}
header.site .brand {
  white-space: nowrap;
  font-family: Georgia, "Times New Roman", serif;
  font-size: 1.25rem; font-weight: 700; color: var(--ink);
}
header.site .brand span { color: var(--accent); }
header.site nav {
  display: flex; gap: 1.25rem; margin-left: auto; font-size: .925rem;
  /* center, not baseline: the nav mixes text links with the two .nav-btn
     button boxes, and baseline-aligning a padded box against bare text drops
     it out of line (issue-tracker#11). */
  align-items: center; flex-wrap: wrap; justify-content: flex-end;
}
header.site nav a { white-space: nowrap; }
/* The two action entries (New Problem / Browse Problems) render as buttons;
   the rest of the nav stays plain text links. */
header.site nav a.nav-btn {
  padding: .3rem .75rem; border-radius: 4px;
  background: var(--accent); color: var(--accent-contrast);
  border: 1px solid var(--accent);
}
header.site nav a.nav-btn:hover {
  background: var(--accent-strong); text-decoration: none;
}
/* Header collapse (issue #70, extended across the tablet band in #125, re-pinned
   in #273). The desktop layout is a single flex row (brand + right-aligned nav);
   when the nav can't fit beside the brand its children used to wrap mid-list —
   orphaning the trailing links ("Your profile"/"Sign out") onto a right-aligned
   second row and dropping the BETA badge below the brand (the broken 641–925px
   band, plus the original <=375px horizontal-scroll case). Below the single-line
   fit-point we let the whole row wrap cleanly instead: brand + BETA + theme
   toggle stay on the first line and the entire nav drops to one full-width,
   left-aligned second line. #70 scoped this to <=40rem, which left the tablet
   band on the broken desktop layout.

   The breakpoint is pinned just above the measured single-line fit-point of the
   WIDEST nav (staff: Browse/New/My Problems/People/Curate/profile/Sign out).
   That measurement depends on the width of everything in the row, so it is
   re-taken whenever the brand copy OR the nav roster changes — #273 widened the
   brand from "LMI Public Square" (165px) to "Large Math Initiative" (186px) and
   the fit-point moved 955px -> 975px, hence 58rem (928px) -> 61rem (976px);
   #92 then added the "My Problems" link and the fit-point moved 975px ->
   1085px, hence 61rem -> 69rem (1104px).

   Re-measured for #92 the same way — headless Chromium against the served
   stylesheet, signed in as staff with the default "Your profile" label,
   sweeping 1px and reading the header row's height (76px = one line;
   120-124px = two). Without the new link the row fits from 1000px; with it,
   from 1086px. 69rem leaves 18px of headroom rather than the 2px a tighter
   value would, because the measurement moves with font loading. Read that
   18px as small: it is banked against jitter in ONE font, and the font caveat
   at the end of this comment measures a 52px swing between two plausible
   resolutions of the same `sans-serif` keyword.

   (The sentences that follow belong to #273, not to #92, and read as if 928px
   were the value THIS change replaced — it was not; #92 replaced 61rem.
   #273's own measurement: with the old 928px value in place the 929–974px
   band rendered the orphaned layout, header 77px -> 117px, and moving to
   61rem also repaired 929–954px, which 58rem had already gone stale on as the
   nav grew.)

   The breakpoint alone cannot be a correctness guarantee, because the nav's
   width includes the member's own display name (up to 24 chars) — no fixed
   pixel value bounds that. `flex-wrap: wrap` on the base `.wrap` rule above is
   the safety net that makes the orphaned layout unreachable at any width; the
   breakpoint's job is to give the common case the designed collapse rather than
   the fallback wrap.

   THE LONG-NAME FALLBACK WRAP IS NEW ON THIS BRANCH, and is accepted
   knowingly. An earlier version of this comment claimed it happened "both
   before and after this change"; that was false and is corrected here.
   Re-measured at 1920px, staff nav, reading the header row's height:

     display name        profile link      parent 61rem      with "My Problems"
     "Ann Leeee"    (9)        72px        76px, one row     76px, one row
     "Ann Leeeeee" (11)        88px        76px, one row     124px, TWO rows
     "Alexander Bonnet" (16)  118px        76px, one row     124px, TWO rows
     "Wilhelmina Featherstone" (23) 164px  76px, one row     120px, TWO rows
                                           (1100-1920px)     (1100-1920px)

   So the seventh nav item costs members whose display name renders past
   roughly 80px — about eleven characters IN THE FONT MEASURED HERE, which is
   not a fixed number; see the font caveat below — a two-row header at every
   desktop width, where the parent gave them one row.

   WHAT ACTUALLY RENDERS ON THAT SECOND ROW, because two earlier versions of
   this paragraph got it wrong. Above 69rem no media query applies, so this is
   NOT the designed collapse the block above describes — that is a media-query
   layout in which the entire nav drops to one full-width, left-aligned second
   line. It is the base `.wrap` rule's plain `flex-wrap` fallback, and the
   fallback puts different things on the second row depending on how wide the
   nav got (measured on the running fix, headless Chromium, staff nav, at
   1105 / 1440 / 1920px — identical shape at all three):

     11-16 char name (nav 696-726px)  the NAV STAYS ON ROW 1. The THEME TOGGLE
                                      alone drops to row 2, left-aligned under
                                      the brand beside ~1200px of white space.
                                      Header 76px -> 124px.
     23 char name    (nav 772px)      the NAV moves to row 2 (right-aligned,
                                      not full-width) and the toggle stays up
                                      on row 1. Header 76px -> 120px.

   Neither is pretty and neither is broken. What `flex-wrap` DOES guarantee is
   the part worth keeping: the nav never splits mid-list, so the ORPHANED-LINKS
   layout of #70/#125 — trailing links stranded on a right-aligned second row
   while the rest stay beside the brand — is unreachable at any width, and
   horizontal overflow measured 0 at every width and every name above.

   THE ~80px / ELEVEN-CHARACTER THRESHOLD IS A PROPERTY OF ONE FONT, not of the
   layout. `body` asks for `-apple-system, BlinkMacSystemFont, "Segoe UI",
   Roboto, sans-serif`; on Linux every name in that prefix misses, so the
   result is whatever `sans-serif` resolves to. Chromium on this dev box
   resolves Liberation Sans and the staff nav measures 682px — but fontconfig's
   own `fc-match sans-serif` on Debian/Ubuntu answers DejaVu Sans, under which
   the SAME nav measures 734px. That is 52px against the 18px of headroom
   banked above, so under DejaVu metrics the two-row header appears at every
   desktop width for staff even with the DEFAULT "Your profile" label (measured:
   header 76px -> 123px at 1105 / 1440 / 1920px).

   Bounded rather than alarming: it is staff-only (the staff nav is the widest
   roster), and the other plausible resolutions all still fit on one row — Noto
   Sans 640px, Ubuntu 640px, FreeSans 665px, Liberation Sans 682px. It has not
   been observed in the browser/font this box actually resolves. Recording it,
   not chasing it: no CSS change is warranted for a font nobody here renders in.

   The two-row header is the price of the link. The alternative on the table
   was capping `a.profile-link` with an ellipsis, which truncates every
   member's name on every page of the site to buy one nav item its preferred
   breakpoint, and the #92 re-scope forbade it.
   `problems/test_landing_copy.py` pins the brand string to this comment so a
   future brand change re-opens this measurement. */
@media (max-width: 69rem) {
  header.site .wrap {
    flex-wrap: wrap;
    align-items: center;
    row-gap: .75rem;
    column-gap: 1rem;
  }
  header.site .brand { margin-right: auto; }
  header.site .theme-toggle { order: 1; }
  header.site nav {
    order: 2;
    flex-basis: 100%;
    margin-left: 0;
    justify-content: flex-start;
    gap: .5rem .85rem;
  }
}

/* Narrow-end fit-point (issue-tracker #310). The 61rem block above pins the
   DESKTOP end of that same first-line contract; the narrow end has a fit-point
   of its own, and #273 moved it without re-measuring. Line 1 must hold
   brand + column-gap + `.beta`'s own margin-left + BETA + column-gap + toggle:
   with the inherited 1rem gap and .5rem margin that is 185.55 + 16 + 8 + 45.80
   + 16 + 44 = 315.35px, against a content box of viewport - 2 x 1.5rem `.wrap`
   padding. So it fits from 364px up. The pre-#273 brand "LMI Public Square"
   (165.0px) needed 294.8px and fitted from 344px, so widening the brand to
   "Large Math Initiative" (185.55px) broke 344-363px — including 360px, the
   commonest Android CSS width — where the toggle wrapped to a row of its own,
   flush left under the brand, and the header grew 156px -> 200px on every
   page.

   Halving the column-gap and dropping `.beta`'s margin-left — which only
   double-spaces the badge, since the flex column-gap already separates it —
   brings line 1 to 291.35px and the fit-point to 340px, below the pre-#273
   344px, so the whole regressed band is repaired. Measured against the served
   stylesheet in Chromium, signed in as agent-test, sweeping 1px at a time
   across 300-420px on /problems/ in both themes, with
   `document.documentElement.scrollWidth == clientWidth` at every width.

   The breakpoint is pinned just above the measured 364px fit-point, exactly as
   the desktop one is, so 375px and up are untouched. Re-measure BOTH ends
   whenever the brand copy changes; `problems/test_landing_copy.py` pins the
   brand string to this comment so a future brand change re-opens this
   measurement too. Below 340px the toggle still wraps — that case predates
   #273 (the narrower old brand broke at <=343px as well) and is out of scope
   for this regression. */
@media (max-width: 22.75rem) {
  header.site .wrap { column-gap: .5rem; }
  header.site .beta { margin-left: 0; }
}

@media (max-width: 40rem) {
  /* The /problems/ filter row is the other mobile overflow source (issue #70):
     the Subject <select> sizes to its widest option (~337px) and the search
     input carries a 17rem min-width, both wider than a 375px viewport. Stack
     the fields full-width and let the controls shrink so the page never
     scrolls horizontally. (Kept at 40rem — this is a phone-width concern,
     independent of the header band above.) */
  .filters form .field { flex: 1 1 100%; }
  .filters .field select,
  .filters .field input { width: 100%; max-width: 100%; }
  .filters .field input[type=search] { min-width: 0; }
}

/* Full-screen "analysing your paper" overlay (paper_upload.html). */
.working-overlay {
  position: fixed; inset: 0; z-index: 50;
  display: flex; align-items: center; justify-content: center;
  background: rgba(0, 0, 0, .55);
}
.working-overlay[hidden] { display: none; }
.working-card {
  background: var(--panel); --math-mask: var(--panel); border: 1px solid var(--rule); border-radius: 5px;
  box-shadow: var(--shadow);
  padding: 2rem 2.5rem; text-align: center; max-width: 22rem;
}
.spinner {
  width: 2.4rem; height: 2.4rem; margin: 0 auto 1rem;
  border: 4px solid var(--rule); border-top-color: var(--accent);
  border-radius: 50%;
  animation: spinner-turn .9s linear infinite;
}
@keyframes spinner-turn { to { transform: rotate(360deg); } }

/* Min-max filter pairs on /problems (v0.11). */
.range-pair { display: inline-flex; align-items: center; gap: .3rem; color: var(--faint); }

/* Curate score histograms (v0.11): CSS bars, no JS. */
.histograms {
  display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;
  margin: 1.25rem 0 2rem;
}
@media (max-width: 40rem) { .histograms { grid-template-columns: 1fr; } }
.histogram h3 { margin: 0 0 .5rem; font-size: 1rem; }
.hist-table { width: 100%; border-collapse: collapse; }
.hist-table td { padding: .1rem .3rem; border: none; vertical-align: middle; }
.hist-label { width: 1.8rem; text-align: right; color: var(--muted); font-size: .85rem; }
.hist-bar-cell { width: 100%; }
/* Bar and count share one flex row (issue #17). The count never wraps
   (white-space: nowrap); when a bar would fill the whole row the bar — not the
   number — shrinks to make room (flex-shrink on the bar, none on the count),
   so every row stays a single line of equal height with the count beside its
   bar. */
.hist-row { display: flex; align-items: center; }
.hist-bar {
  flex: 0 1 auto; height: .85rem;
  background: var(--accent); border-radius: 2px; min-width: 1px;
}
.hist-n {
  flex: 0 0 auto; padding-left: .4rem;
  font-size: .8rem; color: var(--faint); white-space: nowrap;
}

/* /scoring rubric tables (v0.11, issue #8). */
table.rubric { border-collapse: collapse; margin: .75rem 0 1.5rem; max-width: 52rem; }
table.rubric td { padding: .45rem .6rem; border-bottom: 1px solid var(--rule); vertical-align: top; }
.rubric-score { font-weight: 700; color: var(--accent); text-align: right; width: 1.6rem; }
.rubric-name { font-weight: 600; white-space: nowrap; }
/* On mobile the widest label (e.g. "Subfield-prominent") would pin every name
   cell to ~159px via nowrap, leaving the description column narrower than the
   labels (#140). Let short labels wrap so the description reclaims the width;
   desktop keeps nowrap (table has max-width: 52rem and ample room). */
@media (max-width: 40rem) {
  .rubric-name { white-space: normal; }
}

.beta {
  /* optically centered on the brand: baseline-aligning a small chip to
     large text leaves its box hanging low */
  align-self: center;
  display: inline-block; margin-left: .5rem; padding: .1rem .45rem;
  font-size: .7rem; font-weight: 600; letter-spacing: .04em; text-transform: uppercase;
  color: var(--accent); background: var(--accent-soft); border-radius: 3px;
  vertical-align: middle;
}

/* ---- typography ---- */
h1, h2, h3 { font-family: Georgia, "Times New Roman", serif; line-height: 1.25; }
h1 { font-size: 1.9rem; margin: 0 0 .5rem; }
h2 { font-size: 1.3rem; margin: 2rem 0 .75rem; }
h3 { font-size: 1.05rem; margin: 1.5rem 0 .5rem; }
.lede { font-size: 1.05rem; color: var(--muted); max-width: 46rem; }

/* Two-button chooser on /contribute: manual entry vs. extract-from-PDF. */
.chooser {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: .75rem;
  margin: 1.25rem 0 1.75rem;
  max-width: 46rem;
}
@media (max-width: 34rem) { .chooser { grid-template-columns: 1fr; } }
.choice {
  display: flex;
  flex-direction: column;
  gap: .3rem;
  padding: .9rem 1rem;
  border: 1px solid var(--rule);
  border-radius: 5px;
  background: var(--panel); --math-mask: var(--panel);
  text-decoration: none;
  color: var(--ink);
  transition: background .12s, border-color .12s;
}
.choice:hover { background: var(--accent-soft); --math-mask: var(--accent-soft); border-color: var(--accent-border); }
.choice strong { color: var(--accent); font-size: 1.02rem; }
.choice span { color: var(--muted); font-size: .9rem; }

/* Center the /contribute chooser landing (issue #16): heading, lede, and the
   card grid are centered on the page; each card's interior text stays
   left-aligned so multi-line descriptions read cleanly. */
.contribute-choose { text-align: center; }
.contribute-choose .lede,
.contribute-choose .chooser { margin-left: auto; margin-right: auto; }
.contribute-choose .choice { text-align: left; }

/* Extraction summary (paper_summary.html): candidate cards + status badges.
   Note `.candidate block` does NOT pick up `.field.block`, so the card look
   lives here. */
.candidate-list {
  list-style: none; padding: 0; margin: 1.25rem 0;
  display: flex; flex-direction: column; gap: 1rem;
}
.candidate {
  background: var(--panel); --math-mask: var(--panel);
  border: 1px solid var(--rule); border-radius: 5px;
  padding: 1rem 1.15rem;
}
.candidate h2 { margin: .5rem 0 .3rem; font-size: 1.15rem; }
.candidate-head { display: flex; flex-wrap: wrap; gap: .4rem; align-items: center; }
/* it#265: the chips declare their masks like every other painted surface.
   The test used to exempt them on the ground that a chip holds "a fixed
   status word from a template, never |tex output" — which is false: tag
   names, owner display names and story chip labels all reach them, and
   base.html runs renderMathInElement over document.body with ignoredClasses
   of only ['hint','crawl-dash','prompt-block'], so a chip is NOT excluded
   from KaTeX. Declaring the mask is cheaper than defending an exemption. */
.badge {
  display: inline-block; padding: .12rem .5rem; border-radius: 999px;
  font-size: .74rem; font-weight: 600; letter-spacing: .02em;
  border: 1px solid var(--rule); background: var(--chip-bg); --math-mask: var(--chip-bg); color: var(--muted);
}
.badge.ai { background: var(--info-bg); --math-mask: var(--info-bg); border-color: var(--info-border); color: var(--info-ink); }
.badge.ok { background: var(--ok-bg); --math-mask: var(--ok-bg); border-color: var(--ok-border); color: var(--ok-ink); }
.badge.muted { background: var(--chip-bg); --math-mask: var(--chip-bg); border-color: var(--rule); color: var(--faint); }
.muted { color: var(--muted); }

/* Longhand + main.wrap: the shorthand on .wrap (class > element) was
   silently zeroing main's vertical padding, leaving content flush against
   the header/footer rules. */
main.wrap { padding-top: 2.5rem; padding-bottom: 4rem; }

/* ---- stats ---- */
.stats { display: flex; flex-wrap: wrap; gap: 2.5rem; margin: 2rem 0; }
.stat .n {
  font-family: Georgia, serif; font-size: 2rem; font-weight: 700;
  color: var(--accent); line-height: 1;
}
.stat .k { font-size: .82rem; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }

/* ---- problem cards ---- */
.problem-card {
  background: var(--panel); --math-mask: var(--panel);
  border: 1px solid var(--rule); border-radius: 5px;
  padding: 1.1rem 1.25rem; margin-bottom: .85rem;
}
.problem-card h3 { margin: 0 0 .4rem; font-size: 1.05rem; }
.problem-card h3 a { color: var(--ink); }
.problem-card .tldr {
  color: var(--muted); font-size: .92rem; margin: 0 0 .6rem;
  display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical; overflow: hidden;
}

.meta { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; font-size: .8rem; color: var(--faint); }
/* `.meta` is a flex row for pills. A few places want only its typography for a
   running sentence — flex would make each text run and inline element its own
   unbreakable item, inserting the .5rem gap where a word space belongs and
   forcing the line to wrap at the item boundary. `.meta.prose` opts back into
   normal inline flow. */
.meta.prose { display: block; }

.pill {
  padding: .12rem .5rem; border-radius: 999px; border: 1px solid var(--rule);
  background: var(--chip-bg); --math-mask: var(--chip-bg); color: var(--muted); font-size: .76rem; white-space: nowrap;
}
.pill.imp { background: var(--accent-soft); --math-mask: var(--accent-soft); border-color: var(--accent-border); color: var(--accent); font-weight: 600; }
.pill.warn { background: var(--warn-bg); --math-mask: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-ink); }
.pill.resolved { background: var(--ok-bg); --math-mask: var(--ok-bg); border-color: var(--ok-border); color: var(--ok-ink); }
.pill.partial { background: var(--info-bg); --math-mask: var(--info-bg); border-color: var(--info-border); color: var(--info-ink); }
.pill.open { background: var(--open-bg); --math-mask: var(--open-bg); border-color: var(--open-border); color: var(--open-ink); }
.pill.owner { background: var(--owner-bg); --math-mask: var(--owner-bg); border-color: var(--owner-border); color: var(--owner-ink); }
/* issue-399: a machine-written attempt shares the version sequence with human
   ones, so provenance is not in the number and has to be plain at a glance
   in the list. Deliberately the INFO palette, not a warning: a model wrote
   it and a human judged it — that is a fact about the row, not a caution.
   Colours through variables, so dark mode follows. */
.pill.machine { background: var(--info-bg); --math-mask: var(--info-bg); border-color: var(--info-border); color: var(--info-ink); font-weight: 600; }
/* Score pills (Importance/Difficulty) are links to the /scoring/ rubric: keep the
   pill colour, but read as clickable. */
a.pill.score { cursor: pointer; text-decoration: none; }
a.pill.score:hover { border-color: var(--accent); }
/* Subject tags (v0.19): same chip language, link affordance on hover. */
a.pill.tag { cursor: pointer; text-decoration: none; }
a.pill.tag:hover { border-color: var(--accent); color: var(--accent); }

/* ---- filters ---- */
.filters {
  background: var(--panel); --math-mask: var(--panel); border: 1px solid var(--rule); border-radius: 5px;
  padding: 1rem 1.25rem; margin-bottom: 1.5rem;
}
.filters form { display: flex; flex-wrap: wrap; gap: .75rem; align-items: flex-end; }

/* Collapsible filter panel on narrow viewports (issue #73). The whole panel is
   a no-JS <details>: below the desktop breakpoint it collapses to a "Filters"
   summary so the first problem card is reachable without scrolling past ~10
   controls. The template adds `open` when any filter is active, so an applied
   view still shows its constraints. At >=64rem the summary is hidden and the
   form is forced visible regardless of open state, keeping desktop unchanged. */
.filters > summary {
  cursor: pointer; font-weight: 600; font-size: .95rem; color: var(--ink);
}
.filters[open] > summary { margin-bottom: .9rem; }
.filters-active-badge {
  margin-left: .45rem; font-size: .7rem; font-weight: 600;
  text-transform: uppercase; letter-spacing: .04em; color: var(--accent);
}
@media (min-width: 64rem) {
  .filters > summary { display: none; }
  /* Force the form visible on desktop no matter the <details> open state, over
     both collapse mechanisms browsers use (older display-based hiding of the
     children, and the newer ::details-content wrapper). */
  .filters:not([open]) > form { display: flex; }
  .filters::details-content { content-visibility: visible; }
}
.field { display: flex; flex-direction: column; gap: .25rem; }
.field label { font-size: .74rem; text-transform: uppercase; letter-spacing: .05em; color: var(--faint); }
.field select, .field input {
  font: inherit; font-size: .9rem; padding: .35rem .5rem;
  border: 1px solid var(--field-border); border-radius: 4px; background: var(--panel); color: var(--ink);
}
.field input[type=search] { min-width: 17rem; }
/* `.go` is the site's primary-action class. It used to be written `button.go`,
   so on the three CTAs that carry it on an <a> — "Submit a solution"
   (_problem_detail_body.html), the extraction flow's "Review & publish"
   (paper_summary.html) and "Submit a revision" (solution_detail.html) — the
   class was a no-op and they rendered as plain body links (#196). Key on the
   class, not the tag. `header.site nav a.nav-btn` (:122) and `a.solve-cta`
   below are the existing precedents for an accent-filled anchor. Every other
   `class="go"` is a <button>, and the narrower rules that must keep beating
   this one still do: `button.go.deny` (0,2,1), `button.go.big`,
   `button.go:disabled` and `.auth-email-form .go` (0,2,0) all outrank `.go`
   (0,1,0). */
.go {
  font: inherit; font-size: .9rem; padding: .4rem 1rem; cursor: pointer;
  background: var(--accent); color: var(--accent-contrast); border: 1px solid var(--accent); border-radius: 4px;
}
/* An <a> is inline and underlined-on-hover by default, so it needs the box a
   <button> gets for free. The metrics are those of `main form
   button[type=submit]` below, NOT of `.go` above: that rule is (0,1,3) and so
   already outranks `.go` on every submit button on the site — it, not `.go`,
   is what a "go" button actually measures (padding .45rem 1.1rem, font .92rem
   = 7.2px/17.6px/14.72px live). Matching it here is what makes an anchor CTA
   the same size as the real button beside it, which is the whole point of the
   class. */
a.go {
  display: inline-block; text-decoration: none;
  font-size: .92rem; padding: .45rem 1.1rem;
}
.go:hover { background: var(--accent-strong); }
/* The hover colour is pinned on the ANCHOR only, because only an anchor is at
   risk of falling back to link colour. Putting it on `.go:hover` (0,2,0)
   instead would in fact also be safe — the deny button keeps its accent text
   because `button.go.deny` declares `color` at (0,2,1) and so outranks it
   (measured, both themes) — but the narrower selector says what it means and
   cannot reach a button at all. The text-shadow reset kills the global
   `a:hover` smudge, which reads as a smear on a filled button. */
a.go:hover { color: var(--accent-contrast); text-shadow: none; }
.clear { font-size: .85rem; color: var(--muted); }

.count { color: var(--muted); font-size: .9rem; margin: 0 0 1rem; }

/* Relevance-band separator on the problem list (#275). Sorting by anything
   other than relevance puts strong matches first and the weak/fuzzy tail
   below; this line has to read as a section boundary, or the score reset at
   the boundary looks like the broken sort that was reported. A rule with the
   label sitting on it — flex, not a ::before/::after pair, so the rule stops
   short of the text at any label length. */
.band-split {
  display: flex; align-items: center; gap: .75rem;
  color: var(--muted); font-size: .82rem; text-transform: uppercase;
  letter-spacing: .04em; margin: 2rem 0 1rem;
}
.band-split::before, .band-split::after {
  /* `1 0 .75rem`, not `1`. With the `1 1 0%` that the shorthand `1` expands to,
     the rules have a zero flex-basis and therefore a zero scaled shrink factor:
     the instant free space goes negative they give up 100% of it and resolve to
     0px, and the boundary renders as a stray caption indistinguishable from the
     filter panel's own field labels. A non-zero basis with shrink disabled
     reserves the rule instead, and the label — the item that SHOULD absorb the
     shrink — does so below (issue-tracker #311).

     THE BASIS IS NOT FREE, and no non-zero value is. Every pixel reserved here
     is a pixel taken from a label that is already wrapping, so a large enough
     basis tips some query at some width into one more 21px line. Measured over
     15 ordinary queries x 9 widths (135 combinations) on the served page,
     counting combinations whose band grows a line vs. the 0-basis parent:

         basis     combos that gain a line   rule width when constrained
         0 (parent)     0  (but 0px rule — the defect)      0px
         .25rem         2                                    4px
         .5rem          5                                    8px
         .75rem         9   <- shipped                      12px

     .75rem's 9 are all at <= 375px: 'p vs np'/prime/tree/zeta at 320px,
     expander/goldbach/'navier stokes'/riemann at 360px, goldbach at 375px.
     That is the accepted trade: at .25rem the reserved mark is 4px, which on
     screen does not read as a boundary at all, so the cheaper basis pays a
     real (if smaller) cost for a rule that does not do its job. .75rem is the
     smallest measured value that renders as a visible mark, and it matches the
     container's own .75rem gap, so rule and space read as one unit.

     For scale: the defect it buys off is not marginal — 11 of those same 15
     queries render with NO rule at every width from 320px to 430px.

     Above the shrink threshold the basis is inert: 768px and 1280px, and the
     short "Weaker matches below" label at any width, measure byte-identical at
     every value. */
  content: ""; flex: 1 0 .75rem; height: 1px; background: var(--rule);
}
/* Pairs with the rules above. As a real element the label can finally be
   selected, so `min-width: auto` (which floors a flex item at its min-content
   width) can be turned off; `anywhere` then supplies the break opportunity a
   long unbroken query token otherwise denies it. `break-word` is not a
   substitute: only `anywhere` reduces the min-content size itself.

   Scope, precisely (issue-tracker #301): this contains THE BAND. The label
   stops overflowing at any token length, and #301's own reported token no
   longer pushes /problems/ into horizontal scroll. It does not make the page
   overflow-proof — a longer token still scrolls, and the element responsible
   is then <p class="count">, which nothing here touches. #301 stands on its
   own terms. Keep `overflow-wrap` in ONE place, on the item that actually
   shrinks; if #301's container-level copy also lands, reconcile the two rather
   than carrying both. */
.band-split > .band-split-label { min-width: 0; overflow-wrap: anywhere; }

/* Browse-depth boundary note (#333). Rendered only on the last page the
   depth cap serves, directly above the pager, and only when the cap actually
   truncates the result set. Panel-tinted rather than a warning colour: it is
   a signpost to the filters, not an error. Variables only, so it reads in
   both themes (AGENTS.md -> "Colors go through CSS variables"). */
.depth-cap {
  color: var(--muted); font-size: .88rem; line-height: 1.5;
  margin: 2rem 0 0; padding: .75rem 1rem;
  border: 1px solid var(--rule); border-radius: 4px; background: var(--panel); --math-mask: var(--panel);
}

/* ---- pager (problem list, issue #71) ---- */
.pager {
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: .6rem; margin: 1.5rem 0 .5rem;
}
.pager-btn {
  font-size: .9rem; padding: .4rem .9rem; border-radius: 4px;
  border: 1px solid var(--rule); background: var(--panel); --math-mask: var(--panel); color: var(--accent);
  white-space: nowrap;
}
.pager-btn:hover { border-color: var(--accent); text-decoration: none; }
.pager-btn.is-disabled { color: var(--faint); border-color: var(--rule); cursor: default; }
.pager-btn.is-disabled:hover { border-color: var(--rule); }
.pager-status { color: var(--muted); font-size: .88rem; white-space: nowrap; }

/* ---- detail ---- */
.layout { display: grid; grid-template-columns: minmax(0,1fr) 17rem; gap: 2.5rem; }
/* issue #258 — the provenance aside is SOURCE-FIRST now.
   #94 wanted provenance to read first once the grid collapses to one column on
   phones (the main column ends with the comment thread, so the Source card was
   landing below every comment). That was built as `.layout > aside { order: -1 }`
   — a purely visual move: the boxes painted at the top of the page but stayed
   DOM-last, so they were the LAST tab stops, after the whole comment thread.
   `order` is normatively visual-only (CSS Grid L1 / Flexbox L1, "Reordering and
   Accessibility"), so that inverted visual and focus order — WCAG 2.4.3.
   The fix is a real DOM move: `_problem_detail_body.html` now emits the <aside>
   before the main <div>, and the `order` hack is gone. On phones the single
   column simply follows source order, so visual and focus order agree with no
   CSS at all.
   Which leaves desktop, where the aside must still sit in the RIGHT-hand track
   even though it now comes first in the source — hence the explicit placement
   below. `.aside-first` (issue #195) is the marker for "this aside is
   source-first": `.layout` is shared with profile_detail.html and
   profile_edit.html, whose asides are metadata ABOUT a subject their main
   column has not introduced yet. Those stay DOM-last, do not carry the class,
   and auto-place into the right rail exactly as before.
   Both elements stay grid items, so the issue-#88 `min-width:0` guard against
   KaTeX horizontal scroll is preserved (no display:contents restructure). */
.layout > aside.aside-first { grid-column: 2; grid-row: 1; }
.layout > aside.aside-first ~ div { grid-column: 1; grid-row: 1; }
@media (max-width: 820px) {
  .layout { grid-template-columns: 1fr; }
  /* One column: hand both items back to auto-placement so they stack in
     source order — aside, then main. Declared as a reset rather than scoping
     the placement to `(min-width: 821px)`, because a fractional viewport
     (zoom, 820.5px) matches neither media query and would have auto-placed
     the source-first aside into the LEFT track. */
  .layout > aside.aside-first,
  .layout > aside.aside-first ~ div { grid-column: auto; grid-row: auto; }
}
/* issue #88: a grid item defaults to min-width:auto (min-content), so a single
   unbreakable KaTeX line grows the content column past the track and forces
   page-level horizontal scroll on mobile. Let the columns shrink to the track. */
.layout > * { min-width: 0; }

/* issue #351 — the #338/#88 failure class reached through the problem's OWN
   prose. `overflow-wrap: break-word` on the three containers that render
   long-form scraper/member text on the detail page:

   - `.statement` — the failure here is NOT #338's sideways page. This
     container already carries `overflow-x: auto` for #88's display math, so
     the PAGE stays straight and the prose slides into a scroll box whose
     overlay scrollbar is invisible until you scroll it — the #113 failure,
     where a sentence simply ends off-box. Few scraped statements hold a run
     long enough to trigger it, and most that do are really a data bug: a
     mis-paired `$` leaving LaTeX source unrendered as literal text (#352).
     The rule is kept mainly because this same class renders the
     member-editable solution body (solution_detail.html), where a pasted URL
     is one submission away.
   - `.tldr` — the summary paragraph, which has NO containment at all, so one
     unbreakable token there takes the whole page sideways at 375px (measured
     469 vs 375 on a seeded probe). That is the page-level defect #351 reports.
     A tl;dr is member-editable since v0.4, so a pasted URL is one edit away.
     The class is the one `_card.html` already puts on this same field.
   - `.banner` — carries the resolving-reference list, whose titles include
     machine-shaped strings such as Lean module paths; measured 461 vs 375
     before the fix.

   No corpus counts in this comment, on purpose (orchestrator ruling on #351):
   a number that has to be re-measured to stay true rots silently in prose. If
   a census is worth having it belongs in a command or a report — #352 builds
   exactly that for the mis-paired-delimiter class.

   `break-word`, not `break-all`/`anywhere`: it breaks a token only when that
   token cannot fit on a line of its own, so ordinary prose wrapping is
   untouched. Math is unaffected — KaTeX sets `white-space: nowrap` on
   `.katex .base` and `.katex-display > .katex`, and `overflow-wrap` cannot act
   where wrapping is off. For the same reason any census of RENDERED text must
   strip DISPLAY math (`$$..$$`, `\[..\]`) as well as inline (`$..$`, `\(..\)`)
   — all four static/katex-config.json pairs. LaTeX source that becomes
   rendered math is never wrappable text, so leaving it in counts equations as
   unbreakable tokens `overflow-wrap` was never able to touch and badly
   overstates the exposure of every prose field.

   Deliberately NOT on the titles (`h1`, `.problem-card h3`): no title in the
   corpus carries a token anywhere near long enough to need it, and a title
   broken mid-word is a worse default than a title that wraps.
   `test_titles_are_left_alone` pins that as a decision rather than an
   oversight. */

/* The centered main-column frame (it#397). ONE presentation, worn by every
   block in that column that reads as a framed unit: the problem statement,
   the Solve card and the Update-status form. It is a shared class rather
   than copied declarations precisely because two rule sets that must agree
   is the failure this project keeps hitting (it#373's stage roster,
   it#370's doc claims) — anything wearing `.frame` is of a piece with the
   statement by construction, and restyling the family is one edit.
   `.statement` stays on the selector so /solutions/<id>/ keeps its frame
   without needing the new class. */
.frame,
.statement {
  background: var(--panel); --math-mask: var(--panel);
  border: 1px solid var(--rule); border-radius: 5px;
  padding: 1.25rem 1.5rem; margin: 1rem 0 2rem; overflow-x: auto;
  overflow-wrap: break-word;
}
.tldr { overflow-wrap: break-word; }
.statement .katex-display { overflow-x: auto; overflow-y: hidden; padding: .25rem 0; }

/* it#397: the two owner-facing frames. They were sidebar furniture — small
   uppercase letter-spaced <h4> labels, .9rem type, controls sized for a
   narrow column — and the move is only half the job if that visual language
   comes with them. In the main column they take the column's own <h2>
   heading and body type (both inherited, hence no font rules here); what is
   left to say is the spacing and the control widths.

   `.field.block input/select/textarea` is already `width:100%`, so the
   controls fill the wider column with no new rule — which is also why
   it#349's clipped "Resolved by" <select> stops reproducing: at ~700px the
   option text has room it never had at ~370px. */
.frame > :last-child { margin-bottom: 0; }
/* Full-width fields in a wide column want the column's breathing room, not
   the panel's compressed values. */
.status-frame .decision-verdicts { margin-bottom: 1.25rem; }
.status-frame .field.block { margin-bottom: 1.25rem; }
/* The Solve card is a single centred call to action with one line of copy;
   `.solve-cta` already centres itself (see "Solve with AI" below). */
.solve-box .solve-cta { margin-block: .25rem; }

/* issue #88: contain KaTeX overflow so a too-wide equation scrolls WITHIN its
   own box instead of widening the page (uncontained inline/display math can't
   line-break and otherwise pins the content column above a 375px viewport).
   Applies wherever math renders — statement, tl;dr, notes, aside, comments. */
.katex-display { max-width: 100%; overflow-x: auto; overflow-y: hidden;
  /* issue #113 (from ux #109): #88 contained wide math in its own scroll box,
     but on desktop that box scrolls with NO visible cue (overlay scrollbars
     stay hidden until you scroll), so ~13% of display-math silently truncates.
     Pure-CSS scroll shadow (Lea Verou's background-attachment technique): the
     two `local` mask layers ride the content and cover the shadow at whichever
     end is fully reached; the two `scroll` radial shadows are pinned to the box
     edges and only show while there is more to scroll. Theme-aware via
     --math-cue and --math-mask (the mask MUST be the surface behind the
     equation — hard-coding it to --panel painted two lighter blocks on every
     --bg surface in dark mode, issue-tracker#265); no JS, no markup. When the
     equation fits, every layer is masked → invisible. */
  background:
    linear-gradient(to right, var(--math-mask) 30%, rgba(0,0,0,0)) left  center / 2.2rem 100% no-repeat local,
    linear-gradient(to left,  var(--math-mask) 30%, rgba(0,0,0,0)) right center / 2.2rem 100% no-repeat local,
    radial-gradient(farthest-side at 0    50%, var(--math-cue), rgba(0,0,0,0)) left  center / 1.2rem 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, var(--math-cue), rgba(0,0,0,0)) right center / 1.2rem 100% no-repeat scroll;
}
/* .math-glue is the nowrap wrapper base.html adds around EVERY inline formula
   plus its adjacent text, so a line never breaks between math and the word
   glued to it. It stays `display: inline` (baseline follows the text) — the
   scroll box below is applied ONLY to formulas that actually overflow.
   #117: making every glue an `inline-block` with `overflow-x: auto` broke the
   baseline for ALL inline math. Per CSS, an inline-block whose computed
   overflow is not `visible` takes its baseline from its bottom margin edge,
   not its content — so every formula's box aligned by its bottom edge to the
   text baseline and floated visibly above the line. */
.math-glue { white-space: nowrap; }
/* .math-glue--scroll (added by glueInlineMath only when the formula is wider
   than its line box) caps the width so a wide inline run scrolls in its own
   box and contributes zero to the page's min-content width — #113's goal —
   while the common, fitting case above keeps its text baseline.
   overflow-y MUST be hidden, not the default: with overflow-x alone, tall
   KaTeX struts (radicals, subscripts) that paint a couple px outside the
   line box turn into y-overflow, and on classic-scrollbar systems the
   vertical scrollbar steals width -> horizontal scrollbar -> stepper mess
   on a formula that fits. Hidden shows no bar and steals nothing; overlay-
   scrollbar systems already behaved this way, so this just unifies them. */
.math-glue--scroll { display: inline-block; max-width: 100%; overflow-x: auto;
  overflow-y: hidden;
  /* #113: same scroll-shadow cue for the handful of wide inline runs. */
  background:
    linear-gradient(to right, var(--math-mask) 30%, rgba(0,0,0,0)) left  center / 1.4rem 100% no-repeat local,
    linear-gradient(to left,  var(--math-mask) 30%, rgba(0,0,0,0)) right center / 1.4rem 100% no-repeat local,
    radial-gradient(farthest-side at 0    50%, var(--math-cue), rgba(0,0,0,0)) left  center / .9rem 100% no-repeat scroll,
    radial-gradient(farthest-side at 100% 50%, var(--math-cue), rgba(0,0,0,0)) right center / .9rem 100% no-repeat scroll;
}

aside .box {
  background: var(--panel); --math-mask: var(--panel);
  border: 1px solid var(--rule); border-radius: 5px;
  padding: 1rem 1.1rem; margin-bottom: 1rem; font-size: .9rem;
}
aside .box h4 {
  margin: 0 0 .6rem; font-size: .74rem; text-transform: uppercase;
  letter-spacing: .06em; color: var(--faint); font-weight: 600;
}
aside dl { margin: 0; display: grid; grid-template-columns: auto 1fr; gap: .35rem .75rem; }
aside dt { color: var(--muted); }
aside dd { margin: 0; }

/* `overflow-wrap` because the assessment notes are machine-written prose that
   routinely ends with a bare publisher/DOI URL (~1,655 problems carry a >=40
   character unbreakable token; the longest is 222). A URL is one unbreakable
   word, so without this it sets the PAGE's scroll width and the whole page
   scrolls sideways on a phone — the #84/#88 failure class, reached through
   prose instead of KaTeX, and invisible to an element-rect overflow sweep
   because the offending box is a text node (#338). `break-word`, not
   `break-all` or `anywhere`: it breaks a token only when that token cannot fit
   on a line of its own, so ordinary prose wrapping is untouched. `.c-text`
   (comment bodies) already does exactly this. */
.note { font-size: .88rem; color: var(--muted); border-left: 2px solid var(--rule); padding-left: .9rem; margin: .5rem 0 1.25rem; overflow-wrap: break-word; }

.banner {
  background: var(--warn-bg); --math-mask: var(--warn-bg); border: 1px solid var(--warn-border); color: var(--warn-ink);
  padding: .7rem 1rem; border-radius: 5px; font-size: .9rem; margin-bottom: 1.5rem;
  /* #351: the resolving-reference titles live in here — see the note above
     `.statement`. */
  overflow-wrap: break-word;
}
/* Banner action row: keep the banner's controls on their own line so a wrapped
   button never lands mid-sentence at 375 (issue #249). */
.banner-actions {
  display: flex; flex-wrap: wrap; gap: .6rem; align-items: center;
  margin: .6rem 0 0;
}
/* affirming variant for the verified-open banner (issue #36) */
.banner.ok {
  background: var(--open-bg); --math-mask: var(--open-bg); border-color: var(--open-border); color: var(--open-ink);
}
/* failure variant (issue #278): the paper-extraction summary marks a failed
   run with `class="banner error"`. Without this rule the modifier resolves to
   nothing and a hard failure renders in the base (caution/amber) palette —
   the same weight as the landing page's "Beta." notice. `.banner.warn` is
   spelled out alongside it so the modifier used at paper_summary.html:44
   stops silently depending on the base rule's colours. Both palettes come
   from variables that are redefined under html[data-theme="dark"], so dark
   mode follows automatically. */
.banner.error {
  background: var(--err-bg); --math-mask: var(--err-bg); border-color: var(--err-border); color: var(--err-ink);
}
.banner.warn {
  background: var(--warn-bg); --math-mask: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-ink);
}

footer.site {
  border-top: 1px solid var(--rule); padding: 1.5rem 0;
  font-size: .85rem; color: var(--faint);
}
/* The footer's lines are <p> so the Semantic Scholar attribution reads as its
   own statement rather than running on from the pilot disclaimer; the default
   1em paragraph margins would be far too airy at .85rem. */
footer.site p { margin: 0; }
footer.site p + p { margin-top: .4rem; }
footer.site .attribution a { color: inherit; text-decoration: underline; }
footer.site .attribution a:hover { color: var(--accent); }

.empty { color: var(--muted); padding: 3rem 0; text-align: center; }

/* ---- auth, forms, people (v0.2) ---- */
.inline-form { display: inline; margin: 0; }
button.linkish {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; font-size: .925rem; color: var(--accent);
}
button.linkish:hover { text-shadow: 0.4px 0 0 currentColor; }

.messages { list-style: none; margin: 0 0 1.5rem; padding: 0; }
.msg {
  padding: .7rem 1rem; border-radius: 5px; font-size: .92rem; margin-bottom: .5rem;
  background: var(--ok-bg); --math-mask: var(--ok-bg); border: 1px solid var(--ok-border); color: var(--ok-ink);
}
.msg.error { background: var(--err-bg); --math-mask: var(--err-bg); border-color: var(--err-border); color: var(--err-ink); }
.msg.warning { background: var(--warn-bg); --math-mask: var(--warn-bg); border-color: var(--warn-border); color: var(--warn-ink); }
.msg.info { background: var(--info-bg); --math-mask: var(--info-bg); border-color: var(--info-border); color: var(--info-ink); }

form.stack { display: block; }
/* Organisation only applies to partner accounts: hidden until the
   is_partner box is checked. CSS-only (:has); browsers without :has()
   simply always show it. Note the value still submits if typed and then
   the box is unchecked — harmless, the badge renders only for partners. */
form:has(#id_is_partner:not(:checked)) .field:has(#id_organisation) { display: none; }
.field.block { display: block; margin-bottom: 1rem; }
.field.block label {
  display: block; margin-bottom: .3rem;
  font-size: .74rem; text-transform: uppercase; letter-spacing: .05em; color: var(--faint);
}
.field.block input[type=text],
.field.block input[type=email],
.field.block input[type=url],
.field.block textarea,
.field.block select {
  width: 100%; font: inherit; font-size: .92rem; padding: .45rem .6rem;
  border: 1px solid var(--field-border); border-radius: 4px; background: var(--panel); color: var(--ink);
}
.field.block input[type=checkbox] { margin-right: .4rem; }
.hint { font-size: .8rem; color: var(--faint); margin: .25rem 0 0; }
.hint.error { color: var(--err-ink); }

aside .box form.stack .field.block label { margin-bottom: .25rem; }
aside .box textarea { font-size: .85rem; }

/* ---- relinquish ownership (v0.31 SPEC B; moved by it#397) ---- */
/* Sits at the foot of the EDIT view, under a horizontal rule — it left the
   problem page's owner card in v0.31.1. Ruled off and set in smaller type so
   it reads as a secondary affordance rather than a second call to action:
   keeping the problem is the safe option, so the release takes the
   de-emphasized button (.go.deny, shared with /curate/'s Deny) per the
   2026-08-20 button-emphasis policy. */
.release-form {
  margin: .9rem 0 0; padding-top: .75rem; border-top: 1px solid var(--rule);
}
/* it#397: relinquish moved to the foot of /problems/<slug>/edit/, under a
   real <hr>. That <hr> IS the separation the issue asks for, so the card
   footer's own border-top would draw a second line right under it. */
hr.edit-sep {
  border: 0; border-top: 1px solid var(--rule); margin: 2.5rem 0 0;
}
hr.edit-sep + .release-form { border-top: 0; padding-top: 0; margin-top: 1.25rem; }
.release-form .field.block { margin-bottom: .6rem; }
.release-form .hint { margin: 0 0 .6rem; }
/* The confirmation is a sentence the owner reads, not a field caption, so it
   must beat `.field.block label` (0,2,1) — which uppercases and letter-spaces
   captions — exactly as .link-unlimited does on /curate/invites/. */
.release-confirm,
.field.block label.release-confirm {
  display: flex; align-items: flex-start; gap: .4rem; margin-bottom: 0;
  font-size: .82rem; color: var(--muted);
  text-transform: none; letter-spacing: 0;
}
.release-confirm input[type=checkbox] { margin: .15rem 0 0; flex: none; }

/* The 15-minute note-edit window (v0.31 §H). The "(edited)" marker is
   deliberately quiet — it is a footnote on the prose, not a status — and
   matches the treatment the comments' own marker has had since v0.6. It is a
   <time> element, which the reset leaves inline with no styling of its own. */
.edited-marker {
  font-style: italic; font-size: .82em; color: var(--muted);
  cursor: help; margin-left: .25rem;
}
/* ---- the problem timeline (v0.31 §F) ----
   A plain stacked list, deliberately: no new layout machinery, no rail or
   dots to reflow. Two columns on a comfortable viewport (date | what
   happened) collapsing to one stacked block below 34rem, which is where the
   date column stops earning its width at 375px. Every colour is a theme
   token, so the section follows `html[data-theme=dark]` with the rest of the
   site and needs no dark rules of its own.

   #328's "Your recent notes" strip lived here; #329 removed it — the "Edit
   note" affordance is now `.tl-edit`, inline beside the entry it edits. */
.timeline { list-style: none; margin: .75rem 0 0; padding: 0; }
.tl-entry {
  display: grid;
  grid-template-columns: 7.5rem 1fr;
  gap: .15rem .9rem;
  padding: .55rem 0;
  border-top: 1px solid var(--rule);
  font-size: .93rem;
  line-height: 1.5;
}
.tl-entry:first-child { border-top: 0; }
.tl-when {
  color: var(--muted); font-size: .85rem; white-space: nowrap;
  padding-top: .1rem;
}
/* A source that records no date says so, rather than borrowing a plausible
   one (D-9). Italic + help cursor marks it as a gap, not a value. */
.tl-undated { font-style: italic; cursor: help; }
/* issue-399: a machine actor on the timeline is named but not a link — it has no
   profile. Weighted like the member links beside it so the line reads as one
   sentence, without pretending to be clickable. */
.tl-machine { font-weight: 600; }
/* Two guards, and they fix different things — dropping either brings the
   sideways scroll back. `min-width:0` lets the grid column itself shrink
   below its content (a `1fr` track floors at min-content otherwise), and
   `overflow-wrap` lets an unbreakable token break mid-word once it has.
   The timeline is the page's only member-prose surface, so it needs the
   same guard the site's other prose already carries — `.c-text` below.
   Without it a note holding one long hyphen-free token (a pasted journal
   URL is the everyday case: UAX-14 offers no break opportunity inside it)
   pushes the whole problem page sideways at 375px. That is the issue-88
   escape class, and it is what sent this back at Gate 2. Declared on the
   descendants as well as the inheriting parent so moving one of them out
   of `.tl-what` cannot silently un-fix it; locked by TimelineCssTests. */
.tl-what { min-width: 0; overflow-wrap: break-word; }
.tl-what strong { font-weight: 600; }
.tl-note, .tl-object { color: var(--muted); overflow-wrap: break-word; }
.tl-full { display: inline; }
.tl-full summary {
  display: inline; cursor: pointer; color: var(--accent); font-size: .88em;
}
.tl-full[open] summary { display: block; margin-bottom: .2rem; }
.tl-full .prose { display: block; margin-left: .9rem; overflow-wrap: break-word; }
.tl-edit { font-size: .85em; white-space: nowrap; }

@media (max-width: 34rem) {
  .tl-entry { grid-template-columns: 1fr; gap: .1rem; }
  .tl-when { padding-top: 0; }
}

.people-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(15rem, 1fr)); gap: .85rem;
}
/* The {% empty %} message is a direct grid child; span it across every column
   so it centres across the page instead of being trapped in column 1 (#119). */
.people-grid .empty { grid-column: 1 / -1; }
.person {
  background: var(--panel); --math-mask: var(--panel);
  border: 1px solid var(--rule); border-radius: 5px;
  padding: 1rem 1.15rem;
}
.person h3 { margin: 0 0 .25rem; font-size: 1rem; }
.person h3 a { color: var(--ink); }
.person h3 a:hover { color: var(--accent); }
.person .aff { margin: 0 0 .5rem; font-size: .88rem; color: var(--muted); }

/* allauth's default markup, lightly tamed */
main .wrap form input[type=email],
main form input[type=text]:not(.field input),
main form input[type=password] {
  font: inherit; padding: .45rem .6rem;
  border: 1px solid var(--field-border); border-radius: 4px;
  background: var(--panel); color: var(--ink);
}
main form button[type=submit] {
  font: inherit; font-size: .92rem; padding: .45rem 1.1rem; cursor: pointer;
  background: var(--accent); color: var(--accent-contrast); border: 1px solid var(--accent); border-radius: 4px;
}

/* ---- member tag editing on the problem page (issue #44) ---- */
/* The widget lives INSIDE the problem page's `.meta` pill row and deliberately
   adds no layout of its own: `display: contents` dissolves the swap container
   so its pills stay direct flex children of `.meta` and read exactly as they
   did before (v0.19). htmx needs a real element to target by id; `contents` is
   how it gets one without a box. */
.tag-edit { display: contents; }
/* "Edit tags" is a <button>, so the chip's link affordance has to be spelled
   out here — `a.pill.tag:hover` keys on the anchor. */
button.pill.tag-open { cursor: pointer; font: inherit; font-size: .76rem; }
button.pill.tag-open:hover { border-color: var(--accent); color: var(--accent); }
/* A chip in edit mode carries its own remove control. Both rules below are
   (0,2,0) or better on purpose: `main form button[type=submit]` (0,1,3) paints
   every submit inside a form as a full accent button, which on a .8rem meta
   row would turn each "x" into a CTA. */
.tag-edit .tag-chip { display: inline-flex; align-items: center; gap: .3rem; }
.tag-edit .tag-x {
  background: none; border: 0; padding: 0; cursor: pointer;
  font: inherit; font-size: .95rem; line-height: 1; color: var(--faint);
}
.tag-edit .tag-x:hover { color: var(--err-ink); }
.tag-edit .tag-done { font-size: .78rem; }
.tag-edit .tag-none { color: var(--faint); }
/* The add control, sized for the meta row rather than the page's full-size
   form controls. `.tag-edit .tag-add input[type=text]` is (0,3,1) so it
   outranks `main form input[type=text]:not(.field input)` (0,2,4). */
.tag-edit .tag-add { display: flex; gap: .35rem; align-items: center; margin: 0; }
.tag-edit .tag-add input[type=text] {
  font: inherit; font-size: .8rem; padding: .25rem .45rem; width: 12rem; max-width: 100%;
  border: 1px solid var(--field-border); border-radius: 4px;
  background: var(--panel); color: var(--ink);
}
.tag-edit .tag-add .go { font-size: .8rem; padding: .25rem .7rem; }
/* Errors and the audit line are sentences, not chips: give each the whole line
   of the wrapping flex row instead of letting it sit between two pills. */
.tag-edit .tag-msg { flex-basis: 100%; margin: 0; }
/* Narrow screens: the add control takes its own line and the input shrinks
   with it, so the meta row can never push the page into horizontal scroll
   ("contain, don't spill", cf. #139). 34rem is the sheet's existing narrow
   breakpoint (`.chooser`, and the meta row's own rule above) -- a sixth
   breakpoint value is how a stylesheet stops having a convention, and it is
   invisible in a screenshot at two widths. */
@media (max-width: 34rem) {
  .tag-edit .tag-add { flex-basis: 100%; }
  .tag-edit .tag-add input[type=text] { flex: 1 1 auto; width: auto; min-width: 0; }
}

/* ---- paper links (v0.2.x) ---- */
.paper-links { display: flex; flex-wrap: wrap; gap: .4rem; margin-top: .75rem; }
a.pill.link { color: var(--accent); background: var(--accent-soft); --math-mask: var(--accent-soft); border-color: var(--accent-border); }
a.pill.link:hover { text-decoration: none; background: var(--accent-soft-hover); --math-mask: var(--accent-soft-hover); }

/* ---- curator dashboard ---- */
.queue-item h3 { margin-bottom: .3rem; }
form.decide { display: flex; flex-wrap: wrap; gap: .5rem; align-items: center; }
form.decide input[type=text] {
  flex: 1 1 16rem; font: inherit; font-size: .88rem; padding: .4rem .6rem;
  border: 1px solid var(--field-border); border-radius: 4px;
  background: var(--panel); color: var(--ink);
}
button.go.deny { background: var(--panel); color: var(--accent); }
button.go.deny:hover { background: var(--accent-soft); }
.ledger { width: 100%; border-collapse: collapse; font-size: .88rem; margin-bottom: 2rem; }
.ledger td { padding: .45rem .6rem; border-bottom: 1px solid var(--rule); }
/* On mobile the 5-column ledger can't compress below its content width, so it
   pushed the whole /curate/ page into horizontal scroll (#139). Contain the
   overflow inside the table block instead ("contain, don't spill", cf. #88):
   the sideways scroll lives in the ledger, not the page. */
@media (max-width: 40rem) {
  .ledger {
    display: block; overflow-x: auto; -webkit-overflow-scrolling: touch;
    /* #174: #139 contained the scroll but left it with NO cue — exactly what
       #88 did to display math before #113. Overlay scrollbars stay hidden
       until you scroll and the ledger has no <thead>, so the table reads as a
       complete 3-column table and the status pill + decider columns look as
       if they don't exist. Same pure-CSS scroll shadow as .katex-display:
       the two `local` mask layers ride the content and cover the shadow at
       whichever end is fully reached; the two `scroll` radial shadows are
       pinned to the box edges and show only while there is more to scroll.
       Masked in --bg, not --panel — the ledger sits on the page background
       and its cells are transparent. When the table fits, every layer is
       masked → invisible. */
    background:
      linear-gradient(to right, var(--bg) 30%, rgba(0,0,0,0)) left  center / 2.2rem 100% no-repeat local,
      linear-gradient(to left,  var(--bg) 30%, rgba(0,0,0,0)) right center / 2.2rem 100% no-repeat local,
      radial-gradient(farthest-side at 0    50%, var(--math-cue), rgba(0,0,0,0)) left  center / 1.2rem 100% no-repeat scroll,
      radial-gradient(farthest-side at 100% 50%, var(--math-cue), rgba(0,0,0,0)) right center / 1.2rem 100% no-repeat scroll;
  }
  .ledger td { white-space: nowrap; }
  /* #174: nowrap on the long title column alone was 322 of the 397 hidden px
     (cf. #140, which relaxed the same way). Let it wrap; the short cells —
     date, actor, pill, decider — stay on one line. Targeted by class, not
     nth-child(3), so /curate/invites/ (third column: a date) is untouched. */
  .ledger td.ledger-title { white-space: normal; min-width: 10rem; }
}

/* ---- multi-use invite links (v0.28, /curate/invites/) ---- */
.link-form { margin-bottom: 2rem; }
/* The rate-ceiling note under the create button is prose, so hold it to the
   same measure as the section's .lede instead of letting it run the full
   width of the page. */
.link-form .hint { max-width: 46rem; }
/* The cap input is a number, not prose — keep it from stretching to the
   full-width the generic .field.block input rule gives text fields. */
.link-form input[type=number] { max-width: 9rem; }
/* Beat `.field.block label` (0,2,1), which uppercases field labels — this one
   is a checkbox's own sentence label, not a field caption. */
.link-unlimited,
.field.block label.link-unlimited {
  display: inline-flex; align-items: center; gap: .35rem; margin-top: .4rem;
  font-size: .85rem; color: var(--muted); text-transform: none; letter-spacing: 0;
}
.link-row {
  border: 1px solid var(--rule); border-radius: 6px; background: var(--panel); --math-mask: var(--panel);
  padding: .9rem 1rem; margin-bottom: 1rem;
}
.link-head { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; }
.link-count { color: var(--muted); font-size: .88rem; }
.link-url { display: flex; gap: .5rem; align-items: center; margin: .6rem 0 .4rem; }
.link-url input {
  flex: 1 1 20rem; min-width: 0; font-size: .82rem;
  font-family: ui-monospace, "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", monospace;
  padding: .35rem .5rem; border: 1px solid var(--field-border); border-radius: 4px;
  background: var(--panel-hover); color: var(--ink);
}
.link-meta { font-size: .82rem; color: var(--faint); }
.link-accounts { margin: .6rem 0 .4rem; }
.link-accounts > summary { cursor: pointer; font-size: .85rem; color: var(--muted); }
.link-accounts .ledger { margin: .5rem 0 0; }
/* Beat the global `main form button[type=submit]` fill (0,1,3) the same way
   .btn-auth does (#205) — Revoke is destructive and must not read as the
   page's primary action. */
.btn-revoke,
main form button.btn-revoke[type=submit] {
  font: inherit; font-size: .82rem; padding: .3rem .8rem; cursor: pointer;
  background: var(--panel); color: var(--danger);
  border: 1px solid var(--danger); border-radius: 4px;
}
.btn-revoke:hover,
main form button.btn-revoke[type=submit]:hover {
  background: var(--err-bg); color: var(--danger);
}

/* ---- sign-in card ---- */
.auth-card {
  max-width: 23rem; margin: 3rem auto 0;
  background: var(--panel); --math-mask: var(--panel);
  border: 1px solid var(--rule); border-radius: 8px; padding: 2rem 1.75rem 1.5rem;
}
.auth-card h1 { font-size: 1.5rem; margin: 0 0 .25rem; text-align: center; }
.auth-sub { color: var(--muted); font-size: .9rem; text-align: center; margin: 0 0 1.5rem; }

/* The `main form button.btn-auth[type=submit]` twins guard the Google
   submit button against the global `main form button[type=submit]` rule
   ((0,2,3)/(0,3,3) vs (0,1,3)), which otherwise painted it as a filled
   primary button whose label vanished on hover — 1.08:1 light / 1.10:1
   dark once only `.btn-auth:hover`'s background survived (#205). */
.btn-auth,
main form button.btn-auth[type=submit] {
  display: flex; align-items: center; justify-content: center; gap: .6rem;
  width: 100%; padding: .65rem 1rem; font: inherit; font-size: .95rem;
  border: 1px solid var(--rule); border-radius: 6px; background: var(--panel);
  color: var(--ink); cursor: pointer;
}
.btn-auth:hover,
main form button.btn-auth[type=submit]:hover { background: var(--panel-hover); }
.btn-auth svg { flex-shrink: 0; }

.auth-divider {
  display: flex; align-items: center; gap: .75rem; margin: 1rem 0;
  color: var(--faint); font-size: .8rem;
}
.auth-divider::before, .auth-divider::after {
  content: ""; flex: 1; border-top: 1px solid var(--rule);
}

.auth-email summary { list-style: none; }
.auth-email summary::-webkit-details-marker { display: none; }
.auth-email[open] summary { border-color: var(--accent); }
.auth-email-form input[type=email] {
  width: 100%; font: inherit; font-size: .95rem; padding: .6rem .75rem;
  border: 1px solid var(--field-border); border-radius: 6px; margin-bottom: .6rem;
  background: var(--panel); color: var(--ink);
}
.auth-email-form .go { width: 100%; padding: .6rem 1rem; }

.auth-alt {
  text-align: center; font-size: .85rem; color: var(--muted);
  margin: 1.5rem 0 0; padding-top: 1rem; border-top: 1px solid var(--rule);
}

/* ---- rich comments (v0.6) ---- */
.comments { margin: 0; padding: 0; list-style: none; }
.comments.nested { margin-top: .75rem; }
.comment { margin: 0 0 1.75rem; }
.comments.nested .comment { margin-bottom: 1rem; }

/* xtd-style thread bracket: a line down the left of each top-level thread,
   from the vote gutter to the reply box */
.comments:not(.nested) > .comment {
  border-left: 2px solid var(--accent-soft);
  border-bottom: 2px solid var(--accent-soft);
  border-bottom-left-radius: 10px;
  padding: 0 0 .9rem .9rem;
}

/* Comment row = a narrow left rail (avatar over the ▲/score/▼ stack) beside
   the content (issue #153). The rail is exactly one avatar wide and both of
   its items are centred in it, so the score badge and the avatar share one
   vertical centre line — side-by-side columns, as this was before, can never
   line up (their centres were 47px apart). Grid, not flex, because the order
   is a layout concern: the markup keeps the gutter first (votes stay the
   first tab stop of a comment) while the avatar renders on top, level with
   the author name it belongs to.
   `auto 1fr` rows matter: the content spans both rows, and without a flexible
   second row its height would be shared out and drag the vote stack down away
   from the avatar. */
.c-row {
  display: grid;
  grid-template-columns: 2.7rem minmax(0, 1fr);
  grid-template-rows: auto 1fr;
  grid-template-areas: "avatar main"
                       "votes  main";
  column-gap: 1.2rem; row-gap: .3rem;
  align-items: start;
}
.c-row > .c-avatar { grid-area: avatar; }
.c-row > .c-gutter { grid-area: votes; }
.c-row > .c-main   { grid-area: main; }

/* vote gutter: ▲ score ▼, stacked (centred under the avatar) */
.c-gutter {
  display: flex; flex-direction: column; align-items: center; gap: .1rem;
  min-width: 1.6rem;
}
/* Tight flex wrappers: without this, baseline alignment leaves the glyph
   low in its line box, making the gap above the badge smaller than below. */
.c-gutter .inline-form { display: flex; }
.c-gutter .vote {
  border: none; background: none; cursor: pointer; padding: 0;
  color: var(--vote-idle); font-size: .72rem; line-height: 1; height: .72rem;
}
.c-gutter .vote:hover:not(:disabled) { color: var(--accent); }
.c-gutter .vote.on { color: var(--accent); }
.c-gutter .vote:disabled { cursor: default; opacity: .35; }
.c-gutter .score {
  /* neutral at score 0; green/red only once votes move it */
  font-size: .78rem; font-weight: 700; color: var(--muted);
  min-width: 1.55em; text-align: center; padding: .12rem .3rem;
  border-radius: 5px; background: var(--score-bg);
}
.c-gutter .score.pos { background: #2f6b34; color: #fff; }
.c-gutter .score.neg { background: #8a2020; color: #fff; }

/* initials avatar */
.c-avatar {
  flex: none; width: 2.7rem; height: 2.7rem; border-radius: 50%;
  display: flex; align-items: center; justify-content: center;
  font-size: .95rem; font-weight: 700; letter-spacing: .02em;
  border: 1px solid rgba(0,0,0,.06); user-select: none;
}
.c-avatar-gone { background: var(--avatar-gone-bg); color: var(--faint); }

.c-main { flex: 1; min-width: 0; }

/* header: stacked author over date (xtd-style), quiet ops right */
.c-head { display: flex; justify-content: space-between; align-items: flex-start; gap: 1rem; }
.c-who { display: flex; flex-direction: column; line-height: 1.35; }
.c-author { font-size: .97rem; }
.c-date { font-size: .78rem; color: var(--faint); }
.c-date em { font-style: normal; color: var(--faint); }
.c-ops { display: flex; gap: .6rem; align-items: baseline; opacity: 0; transition: opacity .15s; }
.c-row:hover .c-ops, .c-ops:focus-within { opacity: 1; }
.c-ops .linkish { font-size: .78rem; color: var(--faint); }
.c-ops .linkish:hover { text-shadow: 0.4px 0 0 currentColor; text-decoration: none; }
/* Plain red text, not a boxed button: must out-specify the global
   `main form button[type=submit]` rule (delete is a real submit button). */
.c-ops .c-delete {
  background: none; border: none; padding: 0; cursor: pointer;
  font: inherit; font-size: .78rem; color: var(--danger);
}
.c-ops .c-delete:hover {
  color: var(--danger); text-shadow: 0.4px 0 0 currentColor; text-decoration: none;
}

.c-text { margin: .25rem 0 .3rem; font-size: .95rem; overflow-wrap: break-word; }
.c-text .katex-display { overflow-x: auto; overflow-y: hidden; padding: .25rem 0; }
.c-deleted { margin: .3rem 0; color: var(--faint); }

/* reactions below the comment body: small, borderless, flushed right
   (xtd-style "14 👍 3 😀 · React") */
.c-foot { display: flex; align-items: center; gap: .5rem; min-height: 1.2rem; }
/* breathing room after a reactions row before whatever follows */
.c-foot.has-reactions { margin-bottom: 1.1rem; }
.c-reactions {
  margin-left: auto; display: flex; align-items: center; gap: .5rem;
  font-size: .72rem; color: var(--faint);
}
.c-reactions .react-pill {
  font: inherit; font-size: .72rem; cursor: pointer;
  padding: 0; border: none; background: none; color: var(--muted);
  line-height: 1.4; white-space: nowrap;
}
.c-reactions .react-pill:hover:not(:disabled) { text-shadow: 0.4px 0 0 currentColor; }
.c-reactions .react-pill.mine { color: var(--accent); font-weight: 600; }
.c-reactions .react-pill:disabled { cursor: default; }
.c-dot { color: var(--faint); }
.react-picker { display: inline-block; position: relative; }
.react-picker > summary {
  list-style: none; cursor: pointer; font-size: .72rem; color: var(--accent);
  padding: 0;
}
.react-picker > summary::-webkit-details-marker { display: none; }
.react-picker > summary:hover { text-shadow: 0.4px 0 0 currentColor; text-decoration: none; }
.react-menu {
  position: absolute; z-index: 5; right: 0; top: 1.4rem; white-space: nowrap;
  display: flex; gap: .15rem; padding: .3rem .45rem;
  background: var(--panel); --math-mask: var(--panel); border: 1px solid var(--rule); border-radius: 6px;
  box-shadow: var(--shadow);
}
.react-menu .react-option {
  border: none; background: none; cursor: pointer; font-size: 1rem;
  padding: .1rem .2rem; border-radius: 4px; line-height: 1.2;
  /* >=24px tap target for the emoji-picker options too (issue #89) */
  min-width: 24px; min-height: 24px;
}
.react-menu .react-option:hover { background: var(--accent-soft); --math-mask: var(--accent-soft); }
/* Reply sits beside React, same size and colour */
.c-reactions .c-reply-toggle { font-size: .72rem; color: var(--accent); }
.c-reactions .c-reply-toggle:hover { text-shadow: 0.4px 0 0 currentColor; text-decoration: none; }

/* nested replies sit under the parent's content, no extra line (the thread
   bracket on the top-level comment already frames the whole thread).
   Declared BEFORE the mobile block below: the #187 override there has the
   same specificity (0,2,0), so it only wins by coming later in the file. */
.c-main .comments.nested { padding-left: .25rem; }

/* ---- comment controls: >=24px tap targets on mobile (issue #89) ----
   Desktop keeps its compact, hover-revealed look; on touch widths we grow
   every interactive control to a >=24x24px effective tap box (WCAG 2.5.8),
   let the reaction row wrap so "React" is never clipped off the right edge,
   and space the ▲/▼ vote arrows apart as a mis-vote guard. Glyph sizes are
   unchanged — the extra area comes from padding/min-size, not bigger text. */
@media (max-width: 40rem) {
  /* Vote gutter grows to fit the taller arrows; the wider gap between ▲ and
     ▼ (with the score badge between them) reduces accidental mis-votes. */
  .c-gutter { height: auto; gap: .4rem; }
  .c-gutter .vote {
    height: auto; min-width: 24px; min-height: 24px;
    display: inline-flex; align-items: center; justify-content: center;
  }

  /* Ops row can't rely on :hover on touch — keep it visible and tappable. */
  .c-ops { opacity: 1; gap: .5rem; }
  .c-ops .linkish,
  .c-ops .c-delete {
    min-width: 24px; min-height: 24px; padding: 0 .25rem;
    display: inline-flex; align-items: center; justify-content: center;
  }

  /* Reaction row: allow wrapping so nothing overflows the right edge at
     375px; align left once it can wrap, and give each control a 24px box. */
  .c-foot { flex-wrap: wrap; }
  .c-reactions { margin-left: 0; flex-wrap: wrap; row-gap: .3rem; }
  .c-reactions .react-pill,
  .c-reactions .c-reply-toggle,
  .react-picker > summary {
    min-width: 24px; min-height: 24px; padding: 0 .25rem;
    display: inline-flex; align-items: center; justify-content: center;
  }
  .react-picker { display: inline-flex; }

  /* #145: reaction-picker popup must stay on-screen at 375px. The menu keeps
     its right:0 anchor (base rule) so it hangs under "React" and grows LEFT;
     what was missing was a width bound — the old nowrap ~215px strip grew past
     x=0 and hid 👍/❤️ (a top-level "React" sits only ~135px in). Wrapping into
     a bounded box keeps the left edge >= 0 even at the shallowest comment, and
     staying right-anchored is what keeps it on-screen at EVERY nesting depth:
     deeper comments push "React" further right, giving the leftward menu more
     room, whereas a left-anchored popup would clip off the RIGHT at depth 3
     (DEPTH_MAX).

     The box is sized from its CONTENTS rather than to a round number, so the
     rows are exactly full and the padding stays symmetric: a magic width left
     each row slack, and pushing that slack to one side (flex-end) opened a
     wide empty band down the popup's left edge — worst on the last, partly
     filled row. Instead: pin the tap cell, lay out ceil(7/4) = 2 rows of four,
     and centre the wrapped rows so the trailing short row is inset evenly on
     both sides. */
  .react-menu {
    --react-cell: 1.65rem;  /* one emoji tap cell (>=24px, issue #89) */
    --react-gap: .15rem;    /* matches the base rule's gap */
    --react-pad: .45rem;    /* matches the base rule's side padding */
    flex-wrap: wrap; white-space: normal; justify-content: center;
    gap: var(--react-gap); padding: .3rem var(--react-pad);
    /* exactly four cells wide (+ gaps, side padding, 1px borders): no slack
       for flex to distribute, so both inner margins are var(--react-pad) */
    width: calc(4 * var(--react-cell) + 3 * var(--react-gap)
                + 2 * var(--react-pad) + 2px);
  }
  .react-menu .react-option { width: var(--react-cell); }

  /* #187: nested replies must not re-pay the desktop indent at every level.
     Each level costs gutter (1.6rem) + gap (.8rem) + avatar (2.7rem) + gap
     (.8rem) + nested padding (.25rem) ~= 98px, which a 375px viewport cannot
     afford: a depth-2 reply got a 118px text column and a depth-3 one — the
     `Comment.DEPTH_MAX` ceiling, reachable through the Reply toggle — got
     19px, roughly one character per line, with its author/date/ops clipped
     off the right edge and the WHOLE PAGE gaining horizontal scroll
     (scrollWidth 507 at a 375px viewport). At 320px depth 3 measured 0px.

     The avatar is `aria-hidden="true"` decoration and the author name sits
     inline beside it in `.c-head`, so nested levels drop it and tighten the
     gap, reclaiming ~68px per level. The vote gutter stays — it keeps its
     >=24x24px tap targets from #89, which this must not regress. Desktop is
     untouched: these rules live inside the max-width: 40rem block, and the
     base rules at .c-row/.c-gutter/.c-avatar/.comments.nested are unchanged. */
  .comments.nested .c-row { gap: .45rem; }
  .comments.nested .c-avatar { display: none; }
  .c-main .comments.nested { padding-left: 0; }

  /* De-indenting widens the text column but does not by itself stop the page
     from scrolling sideways: `.c-head` is a space-between flex row and its
     ops cluster (link/edit/delete, ~114px with #89's 24px boxes) has an
     automatic minimum size, so at depth 3 it pushed past the right edge and
     dragged document.scrollWidth to 376px at EVERY narrow viewport. Let both
     rows wrap instead — the same guard #89 already applies to .c-foot /
     .c-reactions just above — and let the author/date cluster shrink. The
     controls keep their >=24x24px boxes; they only move to a new line. */
  .c-head { flex-wrap: wrap; gap: .25rem .6rem; }
  .c-who { min-width: 0; }
  .c-ops { flex-wrap: wrap; }
}

/* reply + composer boxes */
.reply-box { margin: .5rem 0 .25rem; }
.thread-reply { margin: 1rem 0 0; }
.thread-reply textarea {
  min-height: 3rem; height: 3rem;
}
/* collapsed state must never show scroll arrows */
.thread-reply textarea:not(:focus) { overflow: hidden; }
/* Expand whenever focused OR holding a draft — a non-empty box must not
   collapse back to one row and hide the text the member typed (issue #93). */
.thread-reply textarea:focus,
.thread-reply textarea:not(:placeholder-shown) { height: auto; min-height: 4.5rem; }
.thread-reply .c-reply-actions { display: none; margin: .4rem 0 0; }
/* Show the Reply button on focus AND whenever the box holds a draft. The
   draft-based rule (not focus alone) is load-bearing: on WebKit a <button>
   does not take focus on mousedown, so a focus-only rule `display:none`s the
   button between mousedown and mouseup and the click never lands; clicking
   away likewise stranded a draft with no submit control (issue #93). */
.thread-reply form:focus-within .c-reply-actions,
.thread-reply form:has(textarea:not(:placeholder-shown)) .c-reply-actions { display: block; }
.reply-box textarea, .thread-reply textarea, .c-composer textarea, .c-edit-area {
  width: 100%; font: inherit; font-size: .92rem; padding: .55rem .7rem;
  border: 1px solid var(--field-border); border-radius: 6px; background: var(--panel);
  color: var(--ink); resize: vertical;
}
.reply-box textarea:focus, .thread-reply textarea:focus, .c-composer textarea:focus,
.c-edit-area:focus {
  outline: none; border-color: var(--accent);
  box-shadow: 0 0 0 3px var(--accent-soft);
}
/* Editing uses the same size as composing a new comment. */
.c-edit-area { min-height: 9.5rem; }
.c-reply-actions { margin: .4rem 0 0; }
.c-composer {
  margin-top: 1.25rem; padding: 1rem 1.1rem;
  background: var(--panel); --math-mask: var(--panel);
  border: 1px solid var(--rule); border-radius: 6px;
}
/* Tall enough that the 5 placeholder-height rows never overflow — no
   scrollbar on an empty box. */
.c-composer textarea { min-height: 9.5rem; }
.c-composer .hint { margin: .4rem 0 0; }
.c-composer-actions { margin: .6rem 0 0; }
.c-sort { margin: 0 0 1rem; }

/* The live preview is a --bg island INSIDE the --panel .c-composer, so it must
   put the mask back to --bg or the composer's override would paint two lighter
   blocks over exactly the equation the member is typing. */
.comment-preview {
  border: 1px dashed var(--rule); border-radius: 6px;
  padding: .5rem .75rem; margin-top: .4rem;
  background: var(--bg); --math-mask: var(--bg); font-size: .92rem;
  /* `pre-wrap` only creates break opportunities AT WHITESPACE, so an
     unbreakable run — a pasted URL, a DOI, a hash — has none: it escapes the
     dashed box and at 375px takes the whole document into horizontal scroll.
     Same trio as `.prompt-block`, the other pre-wrap box that holds pasted
     machine text: break inside the run, and keep a scroll of last resort for
     what still cannot break. `.c-text` already guards the POSTED body, so
     without this the preview and the comment it previews wrap differently. */
  white-space: pre-wrap; overflow-wrap: break-word; overflow-x: auto;
}

/* permalink arrival highlight */
.comments li:target > .c-row > .c-main > .c-body {
  background: var(--accent-soft); --math-mask: var(--accent-soft);
  border-radius: 6px; padding: .4rem .6rem; margin: -.4rem -.6rem .3rem;
}

details.more-replies > summary {
  cursor: pointer; color: var(--accent); font-size: .85rem; margin: .5rem 0;
  list-style: none;
}
details.more-replies > summary::before { content: "▸ "; }
details.more-replies[open] > summary::before { content: "▾ "; }
details.more-replies > summary::-webkit-details-marker { display: none; }

/* ---- Solve with AI (v0.22, UI phase) ---- */

/* Entry button for the Solve card. Since it#397 that card is a centered
   `.frame` in the MAIN column, immediately below the problem statement — it
   is no longer in the aside and no longer sits under the ownership card.
   Conformed to the incumbent button.go idiom (same size and fill), with one
   shared .solve-cta size so the owner link and the non-owner locked state are
   visually the same button — only the affordance differs. It stays
   horizontally centered in its frame (v0.22 feedback); the surrounding text
   stays left-aligned. The `aside .box button.go` half of the selector below
   still applies to the ownership card's own CTA, which did not move. */
aside .box button.go,
.solve-cta {
  display: block; width: fit-content; margin-inline: auto;
}
/* it#456: the CTA and its label are 1.5x the incumbent `.go` metrics — the
   maintainer asked for "50% larger". Both font-size and padding scale, so the
   button keeps its proportions rather than becoming a tall thin pill:
   .9rem -> 1.35rem, .4rem 1rem -> .6rem 1.5rem. It deliberately no longer
   matches `button.go`; this one control is meant to outweigh the page's
   ordinary buttons. Checked at 375px: it is the widest thing in the frame but
   does not overflow it, so there is no mobile-only climb-down. */
.solve-cta {
  font: inherit; font-size: 1.35rem;
  padding: .6rem 1.5rem; border-radius: 4px; text-align: center;
}
a.solve-cta {
  background: var(--accent); color: var(--accent-contrast);
  border: 1px solid var(--accent);
}
a.solve-cta:hover { background: var(--accent-strong); text-shadow: none; }
/* Non-owner/logged-out: same-sized inert button (span, not a link), grayed
   like button.go:disabled. */
.solve-cta.locked {
  background: var(--score-bg); color: var(--faint);
  border: 1px solid var(--rule); cursor: not-allowed; user-select: none;
}
/* it#456: centered within the frame on desktop/laptop, left-aligned on
   phones. Uses the site's existing 40rem phone breakpoint (no new value); the
   centered form reads as a caption under a centered button, which stops
   working once the text wraps to three or four lines in a narrow column. */
.solve-box .hint { margin: .6rem 0 0; text-align: center; }
@media (max-width: 40rem) { .solve-box .hint { text-align: left; } }

/* Step 1: model tile grid. The real checkbox is visually hidden; the tile is
   its label, with :has(input:checked) carrying the selected look (the same
   :has() dependency form.stack already relies on) and :focus-within keeping
   keyboard focus visible. */
.model-grid {
  display: grid; grid-template-columns: repeat(auto-fill, minmax(9.5rem, 1fr));
  gap: .9rem; margin: 1rem 0 2rem; max-width: 46rem;
}
.model-tile {
  position: relative; display: flex; flex-direction: column; align-items: center;
  gap: .55rem; padding: 1.15rem .75rem .95rem; text-align: center;
  background: var(--panel); --math-mask: var(--panel); border: 1px solid var(--rule); border-radius: 8px;
  cursor: pointer; transition: background .12s, border-color .12s;
}
.model-tile input {
  position: absolute; opacity: 0; pointer-events: none;
}
.model-tile:hover { background: var(--panel-hover); --math-mask: var(--panel-hover); border-color: var(--accent-border); }
/* At the max-models cap the tiles you may no longer pick withdraw their
   affordance (#239): no pointer, no hover lift, grayed like button.go:disabled.
   The graying uses palette variables rather than `opacity` on the tile —
   opacity would fade the :focus-within ring with it, and these tiles stay
   keyboard-reachable on purpose (the JS marks them aria-disabled, never
   input.disabled, so a keyboard user can still tab past/into them and a
   checked tile can always be reached to deselect). */
.model-tile.capped,
.model-tile.capped:hover {
  background: var(--score-bg); --math-mask: var(--score-bg); border-color: var(--rule);
  color: var(--faint); cursor: not-allowed;
}
.model-tile.capped .model-icon { background-color: var(--faint); }
/* The real checkbox is opacity:0 (above), so the UA focus ring is transparent
   and this rule is the ONLY keyboard-focus affordance on these tiles — it has
   to meet WCAG 2.1 SC 1.4.11 (>=3:1) on its own. It cannot paint in
   --accent-soft: that token is also the *selected* tile's background (see
   :has(input:checked) below), so a focused, selected tile drew a 1.00:1 ring
   against the fill it sat on, and only 1.12:1 (light) / 1.32:1 (dark) against
   the page background the offset exposes. --accent clears 3:1 against all
   three backdrops in both themes. Locked by
   problems/test_solve_tile_focus_css.py. The .model-grid gap (.9rem) leaves
   room for the 2px offset. */
.model-tile:focus-within { outline: 3px solid var(--accent); outline-offset: 2px; }
.model-tile:has(input:checked) {
  background: var(--accent-soft); --math-mask: var(--accent-soft); border-color: var(--accent);
  box-shadow: inset 0 0 0 1px var(--accent);
}
/* Corner check as a second selected cue beside the inverted icon. */
.model-tile::after {
  content: "✓"; position: absolute; top: .35rem; right: .6rem;
  font-weight: 700; color: var(--accent); opacity: 0;
}
.model-tile:has(input:checked)::after { opacity: 1; }
/* Provider marks: normalized single-silhouette SVGs, committed under
   static/img/models/ (never hotlinked). Painted as CSS masks so the color is
   a site variable — one file reads correctly in both themes and inverts with
   the selected tile, keeping the tiles one designed set. The mask uses the
   SVG's alpha only, so the files' internal fill is irrelevant. The lineup is
   DB-backed (v0.22 feedback), so --model-mask is set inline per tile from
   the row's committed icon path — no per-provider CSS class. */
.model-icon {
  width: 4.5rem; height: 4.5rem; background-color: var(--ink);
  -webkit-mask: var(--model-mask) center / contain no-repeat;
  mask: var(--model-mask) center / contain no-repeat;
}
.model-tile:has(input:checked) .model-icon { background-color: var(--accent); }
.model-name { font-weight: 600; font-size: .92rem; }

/* Step 2: one harness chooser per selected model (hidden until its tile is
   picked; the JS toggles the hidden attribute). */
.harness-for {
  max-width: 46rem; margin: 0 0 1rem; padding: .6rem .9rem .8rem;
  background: var(--panel); --math-mask: var(--panel); border: 1px solid var(--rule); border-radius: 8px;
}
.harness-for legend { font-weight: 600; font-size: .95rem; padding: 0 .35rem; }
.harness-option {
  display: flex; gap: .65rem; align-items: flex-start;
  padding: .55rem .6rem; border-radius: 6px; cursor: pointer;
}
.harness-option:hover { background: var(--panel-hover); --math-mask: var(--panel-hover); }
.harness-option:has(input:checked) { background: var(--accent-soft); --math-mask: var(--accent-soft); }
.harness-option input { margin-top: .3rem; flex: none; }
.harness-body { display: flex; flex-direction: column; gap: .15rem; font-size: .92rem; }
.harness-tldr { color: var(--muted); }
.harness-body a { font-size: .85rem; }

/* Large wizard buttons (Continue / Run AIs), grayed while gated. */
button.go.big { font-size: 1.05rem; padding: .65rem 1.6rem; }
button.go:disabled,
button.go:disabled:hover {
  background: var(--score-bg); border-color: var(--rule); color: var(--faint);
  cursor: not-allowed;
}

/* Review-and-confirm: the chosen pairs re-use the tile look, smaller, with
   "Harness: <name>" and the harness TLDR beneath (v0.22 feedback) — the
   tiles are a little wider than the picker's so the TLDR wraps readably. */
.confirm-grid { display: flex; flex-wrap: wrap; gap: .9rem; margin: 1.25rem 0 1.5rem; }
.confirm-tile {
  display: flex; flex-direction: column; align-items: center; gap: .35rem;
  width: 14rem; padding: .9rem .85rem; text-align: center;
  background: var(--panel); --math-mask: var(--panel); border: 1px solid var(--rule); border-radius: 8px;
}
.confirm-tile .model-icon { width: 3.2rem; height: 3.2rem; }
.confirm-harness { font-size: .8rem; color: var(--ink); font-weight: 600; }
.confirm-tldr { font-size: .78rem; color: var(--muted); }
.solve-agree {
  display: flex; gap: .65rem; align-items: flex-start; max-width: 46rem;
  margin: 1.25rem 0 0; padding: .9rem 1rem; font-size: .92rem;
  background: var(--panel); --math-mask: var(--panel); border: 1px solid var(--rule); border-radius: 8px;
  cursor: pointer;
}
.solve-agree input { margin-top: .25rem; flex: none; }

/* ---- Solve with AI (v0.23, execution backend) ---- */

/* Confirm-page notes: the fire-and-forget copy and the daily-quota line
   under the lede — quieter than the lede, same measure. */
.solve-note { color: var(--muted); font-size: .92rem; max-width: 46rem; margin: .35rem 0 0; }

/* Problem-page "AI solve attempts" status chips. The rows reuse
   .problem-card.queue-item + .meta wholesale; Solved/Failed reuse the
   .resolved/.warn pill modifiers. Two solve-specific modifiers (variables
   only — the dark theme redefines them, nothing extra needed here):
   solve-live  — Running / automatic-retry chips, the accent "in flight"
                 palette (same pairing as .pill.imp, without its weight);
   solve-partial — a done task whose best outcome is a partial solution:
                 notable but not final, the info palette. */
.pill.solve-live { background: var(--accent-soft); --math-mask: var(--accent-soft); border-color: var(--accent-border); color: var(--accent); }
.pill.solve-partial { background: var(--info-bg); --math-mask: var(--info-bg); border-color: var(--info-border); color: var(--info-ink); }

/* Harness detail pages: the verbatim prompt in a code-block style — bordered,
   subtly tinted, code-look type, whitespace preserved (content is escaped by
   the template engine as always; KaTeX skips it via ignoredClasses). */
.prompt-block {
  max-width: 52rem; margin: 1rem 0 2rem; padding: 1.1rem 1.3rem;
  background: var(--chip-bg); --math-mask: var(--chip-bg); border: 1px solid var(--rule); border-radius: 8px;
  font-family: ui-monospace, "Cascadia Code", Menlo, Consolas, "DejaVu Sans Mono", monospace;
  font-size: .86rem; line-height: 1.55;
  white-space: pre-wrap; overflow-wrap: break-word; overflow-x: auto;
}

/* ---- theme toggle (v0.6.1) ---- */
/* Hidden until JS wires it up (it can't work without JS). When the nav is
   hidden (logged-out, invite-only), the auto margin right-aligns it alone. */
.theme-toggle {
  border: none; background: none; cursor: pointer; padding: 0;
  font-size: 1.05rem; line-height: 1; color: var(--muted);
  margin-left: auto;
}
/* Comfortable 44×44 tap target (#126). Scoped to :not([hidden]) so the
   display never overrides the UA `[hidden] { display:none }` rule — the
   button must stay hidden until JS reveals it (base.html). Flex-centres the
   ::before glyph inside the box, so the visual position is unchanged. */
.theme-toggle:not([hidden]) {
  display: inline-flex; align-items: center; justify-content: center;
  min-width: 44px; min-height: 44px;
}
header.site nav ~ .theme-toggle { margin-left: 0; }
.theme-toggle:hover { color: var(--accent); }
.theme-toggle::before { content: "☾"; }
html[data-theme="dark"] .theme-toggle::before { content: "☀"; }

/* Title-only resolving references (no url/arXiv/DOI): the search affordance
   is deliberately quieter than a real source link — small, muted, and
   labeled — so it never reads as a claimed source (2026-08-14). */
.ref-search {
  font-size: 0.78em;
  color: var(--faint);
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 0 0.35em;
  margin-left: 0.35em;
  text-decoration: none;
  white-space: nowrap;
}
.ref-search:hover { color: var(--accent); border-color: var(--accent); }

/* ---- owner decision card (SPEC v0.31 §C, #324 redux) ----
   The owner's verdict on a solution attempt, rendered by
   templates/problems/_decision_form.html on BOTH the problem page (inline,
   under the attempt) and the solution page. Every colour goes through a
   palette variable, so it renders in both themes (v0.6.1 rule).

   The four verdict options carry EQUAL visual weight (2026-08-20
   button-emphasis policy): no option is styled as primary or as danger.
   Only the selected one is highlighted, and that is feedback about the
   member's own choice, not a nudge toward it. */
.decision-card {
  margin: .85rem 0 0;
  padding: .9rem 1rem;
  border: 1px solid var(--rule);
  border-radius: 5px;
  background: var(--bg); --math-mask: var(--bg);
}
.decision-verdicts { border: 0; margin: 0; padding: 0; }
.decision-verdicts legend {
  padding: 0; margin-bottom: .45rem;
  font-size: .74rem; text-transform: uppercase; letter-spacing: .05em;
  color: var(--faint);
}
.decision-option {
  display: inline-flex; align-items: center; gap: .4rem;
  margin: 0 .5rem .4rem 0;
  padding: .35rem .65rem;
  border: 1px solid var(--rule); border-radius: 4px;
  background: var(--panel); --math-mask: var(--panel); color: var(--ink);
  font-size: .9rem; cursor: pointer;
}
.decision-option:hover { background: var(--panel-hover); --math-mask: var(--panel-hover); border-color: var(--accent-border); }
.decision-option:has(input:checked) {
  background: var(--accent-soft); --math-mask: var(--accent-soft);
  border-color: var(--accent);
  color: var(--accent-strong);
}
/* Keyboard focus must be visible on the LABEL, not only on the hidden-ish
   radio glyph — the label is what the styling makes look clickable. */
.decision-option:has(input:focus-visible) { outline: 2px solid var(--accent); outline-offset: 1px; }
.decision-card .decision-note { margin: 0 0 .8rem; }
.decision-card textarea {
  width: 100%; font: inherit; font-size: .92rem; padding: .45rem .6rem;
  border: 1px solid var(--field-border); border-radius: 4px;
  background: var(--panel); color: var(--ink);
}
@media (max-width: 40rem) {
  /* Narrow screens: one option per line, so a long label never wraps into
     an unreadable two-line chip next to its neighbour. */
  .decision-option { display: flex; margin-right: 0; }
}

/* ---- v0.33 §C: the per-attempt page's live session panel ----------------
   Scoped to the panel rather than reusing `/crawl/`'s `.bar`, which is itself
   scoped under `.crawl-dash` — two pages drawing progress is not a reason to
   couple this panel's styling to a dashboard that renders from Python. */
.session-meters {
  display: grid; grid-template-columns: 1fr 1fr; gap: 1rem 1.5rem;
  margin: .6rem 0 0;
}
/* The trough is --score-bg, a site.css token defined in BOTH themes.
   `/crawl/`'s bar uses --barbg, which is declared only inside
   crawl_dashboard.py's own inline stylesheet and is therefore undefined on
   every page served from a template — including this one. */
.session-bar {
  height: .5rem; background: var(--score-bg); border-radius: 3px;
  overflow: hidden; margin-top: .3rem;
}
.session-bar > i { display: block; height: 100%; background: var(--accent); }
.session-tail-label {
  font-size: .78rem; letter-spacing: .06em; text-transform: uppercase;
  color: var(--muted); margin: 1rem 0 .35rem; font-weight: 600;
}
.session-tail { list-style: none; margin: 0; padding: 0; font-size: .9rem; }
.session-tail li { padding: .18rem 0; border-bottom: 1px solid var(--rule); }
.session-tail li:last-child { border-bottom: 0; }
.session-tail .verb { color: var(--fg); }

/* One column on a phone: two half-width meters side by side put "not
   reported yet" on three lines each at 375px. */
@media (max-width: 34rem) {
  .session-meters { grid-template-columns: 1fr; gap: .75rem; }
}
