/* ============================================================
   FOLIO — study companion
   Design system carried over from the source Anki collection:
   warm paper, indigo + Chinese vermilion, serif display,
   monospace eyebrows.
   ============================================================ */

/* The webfonts are self-hosted and declared in fonts.css, which index.html links ahead of
   this file. They were an `@import` here until Sep 2026; that single line blocked every
   script on the page behind a third-party request (12,949 ms to first paint with Google
   unreachable, against 437 ms without it). Do not reintroduce one — see the reasoning in
   index.html's head and in .claude/build-fonts.js. */

/* ---------- tokens ---------- */
:root{
  --paper:#F7F6FB; --paper-2:#EBE9F5; --card:#FDFDFF;
  --ink:#1D1B29; --ink-soft:#514D68; --ink-faint:#918CA8; --rule:#DEDAEE;
  /* --ink-quiet: QUIET, BUT NOT BELOW THE BAR. See the CONTRAST block below. --ink-faint is
     3.0-3.5:1 and is right for what it was written for — an eyebrow, a caption, a source line —
     but it had spread to things that are not captions at all: the primary nav labels, a page's
     own 15.5px description, the card counts a reader picks a collection by, and the single
     control that is the only route to the About page. A reader with mild low vision does not
     know the high-contrast mode exists, so those cases take this instead. The value is not a new
     guess: it is the one `body.hc` already solves to, each hue scaled toward the ink until it
     clears 4.6:1 against --paper-2, the darkest of the three papers. */
  --ink-quiet:#69657E;
  /* THE OPAL'S PERIWINKLE, WALKED DOWN TO THE BAR — the same treatment the red below gets, and needed
     for the same reason. `--indigo` is the eyebrows, the nav labels, the glossary links and the FILL of
     every primary button, so as a default it is running text and a control at once. As a collectible
     the stone wore #6E7FC4 / #8A9BDD, which measured 3.19 and 2.24 on `--paper-2` and put white on a
     button at 3.82; promoting it unchanged took `test-a11y.js`'s high-contrast sweep from green to 20
     failures and roughly doubled the default mode's standing backlog. These are those two hues at the
     same chroma, deep enough to clear 4.5 on all three papers: --indigo 6.89 / 6.51 / 5.83 and 7.0 for
     white on it, --indigo-bright 5.48 / 5.18 / 4.64. Night needs none of this — reversed out of a dark
     card the stone's own values are already 8.44 and 10.84 — so it keeps them exactly. */
  --indigo:#45549C; --indigo-bright:#5263B0; --indigo-wash:#E9ECFA; --ochre:#9A7C55;
  /* THE ANSWER TERM'S RED IS A DEEP ROSE, NOT THE STONE'S OWN PINK (Sep 2026, on request: "red text
     should have a deeper red color so there's more contrast"). Opalite wore #C081A8, which is the
     milky pink an opal actually flashes and measures 2.98:1 on --card, 2.52 on --paper-2 — a long way
     under the bar for what is the answer to every card and every footnote marker on the site. This is
     the same hue walked down until it clears 4.5 on all three papers: 6.10 on --card, 5.76 on --paper,
     5.17 on --paper-2, against the OLD default folio vermilion's 4.73 / 4.47 / 4.00. It is measurably
     the highest-contrast red Folio has ever shipped as a default, and still an opal's rose rather than
     a pillar-box red. Keep any replacement above 4.5 on --paper-2, which is the darkest of the three. */
  --zh:#AE3350; --zh-wash:#F9E9EC; --geo:#5FA5A8; --good:#4E9B7E;
  /* an UNDISCOVERED glossary term (Aug 2026, on request). This teal began life as --bknote, the Library
     books' own note colour, and the two roles were SWAPPED: a book's markers now take the card's
     vermilion (--zh) like every other citation on the site, and the hue that distinguished them moved
     to the one mark that had been borrowing --ochre — the gold of a card's blank. The reasoning that
     chose the hue holds either way: every neighbouring token is spoken for (--zh is a citation marker
     and the answer term, --indigo a glossary link, --good completion, --ochre the blank), and this teal
     is none of them, far enough round the wheel from --good's forest green (hue ~186 against ~152) to
     read as another thing rather than as a shade of the same one. */
  --newterm:#1E7A85;
  /* An ADMIN-ONLY tab in the nav (Aug 2026, on request: "make it so that pages that are visible to admin
     only appear with a different color in the menu bar than public pages"). A magenta-purple at hue ~305,
     which is the quarter of the wheel nothing else here holds — --indigo is ~258, --newterm ~186, --good
     ~152, --ochre/--geo ~35, --zh ~5 — so it reads as another KIND of destination rather than as a shade
     of the active state. Measured against every paper the nav sits on rather than eyeballed: 5.6–7.2 in
     light, 6.4–7.6 at night, so it needs no `body.hc` row of its own (that block re-tones the tokens that
     fall SHORT, and this one does not). */
  --admin:#8E2F86;
  --han:"Noto Sans SC","PingFang SC","Hiragino Sans GB","Microsoft YaHei",sans-serif;
  --serif:"Lora", Georgia, "Times New Roman", serif;
  --display:"Nunito Sans", system-ui, sans-serif;
  --mono:"IBM Plex Mono", ui-monospace, Menlo, Consolas, monospace;
  --sans:"Nunito Sans", ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
  --shadow-sm:0 1px 2px rgba(29,27,41,.05);
  --shadow-md:0 1px 2px rgba(29,27,41,.05), 0 8px 28px rgba(29,27,41,.06);
  --shadow-lg:0 2px 4px rgba(29,27,41,.06), 0 18px 50px rgba(29,27,41,.10);
  --bar-h:60px;
  /* The two bars the Atlas has to sit above. --tabbar-h is 0 until the phone's bottom tab bar appears and
     --timebar-h grows when the timeline goes to two rows, so .globe-stage and .atlas-timebar can each be
     written ONCE against them instead of being re-stated per breakpoint (and drifting apart). */
  --tabbar-h:0px;
  --timebar-h:96px;
  /* Reading-text size (Settings → Appearance → Text size, Aug 2026). A multiplier, applied ONLY by the
     rules that size the prose a reader actually reads — a card's question and background, a glossary
     popup, a place panel — each written `calc(<its own px> * var(--fs))` so every one keeps its own
     relative size. Deliberately NOT a whole-site zoom: 522 font sizes in this file are px and the shell
     is laid out against them, so scaling the lot would break the chrome to enlarge the text. Search
     `var(--fs)` for the full set; the setting's wording names exactly those surfaces. */
  --fs:1;
  --ease:cubic-bezier(.2,.7,.2,1);
}
body.night{
  --paper:#131220; --paper-2:#0C0B16; --card:#1C1A2C;
  --ink:#EFEDFA; --ink-soft:#B6B1D0; --ink-faint:#827CA0; --rule:#302C48;
  --ink-quiet:#8C87A8;   /* see the light value — night's own hc figure, 4.98:1 on --card */
  --indigo:#A6B4F0; --indigo-bright:#C2CCFF; --indigo-wash:#20203A; --ochre:#C3A47A;
  /* the red walks the OTHER way at night — see the light value. Reversed out of a dark card the
     stone's #E4A9CC already measured 8.79:1, so deepening it would have COST contrast; what it
     lacked was red, sitting at hue ~320 where the light value is ~345. This is that hue at the
     same weight: 8.89:1 on --card, so it reads as a red and is a shade clearer than before. */
  --zh:#EFA8B8; --zh-wash:#32202A; --geo:#7FCACD; --good:#75C6A4;
  --newterm:#5FC2CE; --admin:#CF93DC;
  --shadow-sm:none; --shadow-md:0 10px 30px rgba(0,0,0,.35);
  --shadow-lg:0 20px 60px rgba(0,0,0,.5);
}

/* ============================================================
   CONTRAST (Aug 2026, on request: "check whether colour contrasts have a ratio of at least 4.5:1, and if
   they don't, add a high contrast mode in the settings")
   ============================================================
   Measured rather than assumed — every foreground token against every paper it is ever set on, by the WCAG
   2.x relative-luminance formula. What came back:

     LIGHT   ink 15.9 · ink-soft 8.4 · indigo 9.9 · indigo-bright 6.5   — comfortable
             ink-faint 3.0–3.5 · ochre 3.3–3.9 · geo 3.0–3.5           — BELOW the bar everywhere
             zh 4.1–4.8 · good 4.1–4.8                                 — below it on paper-2, above on card
     NIGHT   everything 4.6+ except ink-faint on --card, at 4.19

   So the quiet tokens are the whole of the problem, and they are quiet ON PURPOSE — an eyebrow, a caption,
   a source line. Re-toning them for everyone would flatten the typographic hierarchy the design is built
   on, which is exactly the trade a high-contrast MODE exists to let the reader make for themselves. The
   values below are solved, not eyeballed: each is its own hue scaled toward the ink until it clears 4.6:1
   against `--paper-2`, the darkest of the three papers, so it clears the bar on all three.

   One failure was NOT left to the mode, because it is a primary control rather than a caption: in night
   mode `.btn` was #FFF on the light-lavender --indigo, at 2.29:1. That is fixed outright below.

   …AND SIX MORE WERE PROMOTED IN SEP 2026, on the same principle (found in the site review, where
   test-a11y.js reported 110 shortfalls in the DEFAULT mode — it only ever FAILED on the hc sweep, so
   the number was being printed and not read). The mode's own reasoning holds for a caption and does
   not hold for these, because a reader with mild low vision does not know the mode exists and these
   are not captions:

       .tab .tab-label   the primary nav labels, on every page
       .page-head p      a page's own 15.5px description — running body text
       .home-about       the ONLY route to the About page anywhere on the site
       .node-num / .node-count / .group-count   the card counts a collection is chosen by

   They take `--ink-quiet` (declared at the top of this file), which is not a new guess: it is the
   value `body.hc` already solves to. `--ink-faint` keeps every genuine caption, so the hierarchy the
   design rests on is intact and the mode still has work to do. */
/* The ink is for a FILLED button — on night's light-lavender --indigo it reads 7.9:1 against #fff's 2.29.
   A .ghost button is transparent, so the same dark ink puts near-black on the dark card and the label
   disappears altogether: found on the new Common Thread board's Shuffle/Deselect, and it was every ghost
   button on the site in dark mode (the True or False and Timeline results' "Home" among them). The :hover
   line below has always carried the :not(.ghost) — the exemption was intended here too and was missed.
   A ghost keeps .btn.ghost's own var(--indigo), which is that same light lavender ON the dark card. */
body.night .btn:not(.ghost){color:var(--paper);}
body.night .btn:not(.ghost):hover{color:var(--paper);}
/* RE-SOLVED Sep 2026 WHEN THE DEFAULT PALETTE CHANGED. These are one set of values applied to every
   theme, and they always were the DEFAULT theme's own solved values — so when Opalite replaced Folio
   the row had to move with it, or the mode would have gone on imposing the old warm palette's greys and
   golds on a cool one (and `--zh` at #BA4038 now measures 4.51 on the new --paper-2, which is under the
   bar the row exists to clear, and BELOW the base red it replaces — a high-contrast mode that lowers
   contrast). Each is its own hue scaled toward the ink until it clears 4.6:1 on --paper-2, the darkest
   of the three papers: ink-faint 4.65, ochre 4.84, geo 4.80, zh 6.05, good 4.89, newterm 6.18. */
body.hc:not(.night){
  --ink-faint:#69657E; --ochre:#7A6142; --geo:#3A6E71; --zh:#9E2B49; --good:#376F58; --newterm:#155E68;
}
body.hc.night{ --ink-faint:#8C87A8; }   /* 4.98:1 on the new night --card */
/* …and the borders, which are not text and so are not in the table above, but are what tells one card from
   the next. A hairline at 1.2:1 is invisible to a reader who needs this mode at all. */
body.hc:not(.night){ --rule:#B5B0C9; }
body.hc.night{ --rule:#4E4A6E; }
body.hc .btn,body.hc .card,body.hc .set-card{border-width:1.5px;}
/* the gold collection icon is 2.6:1 on card — decorative, but it is the only thing naming the subject */
body.hc:not(.night) .coll-ic{color:#81661E;}
/* A QUIET TOKEN OVER A TINTED BACKGROUND IS NOT COVERED BY THE RE-TONE ABOVE, and the crossword is the
   site's only case (Aug 2026, found the day test-a11y.js learned to read a `color(srgb …)` computed
   value — until then it could not parse a `color-mix` BACKGROUND at all, walked past it to the plain
   paper behind, and reported these three as clean).
   `--ink-faint` is solved against `--paper-2`, the darkest of the three papers. The SELECTED clue and
   the SELECTED square are neither: each is that paper with 12–14% indigo mixed into it, which is enough
   to take the clue number and the enumeration to 3.78 and the square's number to 4.24. They are the
   clue's own text rather than furniture once a reader is in this mode, so they take `--ink-soft`, which
   is a solved ink rather than a quiet one and clears the bar on every surface the grid uses. */
body.hc .xw-cn, body.hc .xw-len, body.hc .xw-n{color:var(--ink-soft);}
/* THE DIFFICULTY ROW'S OWN TEXT is the same case one surface over (Aug 2026, with the word and the
   community figure — the stars themselves are SVG and so are not measured at all, which is why that row
   had nothing here until it gained words). Both are `--indigo` held down by opacity so they label the row
   rather than competing with the question above it, and measured over all six themes in both modes that
   is 1.77–3.28 for the word and 2.06–4.70 for the figure — quiet on purpose, and short of the bar in
   every one of the twelve. Opacity alone does not rescue them either: at full strength the indigo is
   still 3.32 on gazette's dark card and 4.70 on marble's light one. So in this mode they stop being a
   quiet colour and become ordinary ink, which is what the re-tone above does for every other one. */
body.hc .card-stars .cs-lbl, body.hc .card-stars .cs-pct{color:var(--ink); opacity:1;}
/* ...and the successive-relearning caption beside them, which took the same indigo at the same half
   strength in Sep 2026 and therefore inherits the same failure. Its `.done` state is the gold, which is
   re-toned by the rules beside it rather than here. */
body.hc .crit-row:not(.done) .crit-lbl{color:var(--ink); opacity:1;}
/* THE GRADE BAR IS THE SAME CASE AGAIN AND IS THE SITE'S MOST-USED CONTROL (Aug 2026, found the day
   `test-a11y.js` first reached a study card — the route list deliberately has no `study`, so every one
   of these had been failing since the bar was built and nothing had ever measured them). All three of
   its text runs are WHITE, at 1, .82 and .6, over four saturated backgrounds, and measured over both
   modes not one of the twelve combinations clears 4.5: the label 2.25–4.56, the interval 1.97–3.64, the
   key 1.66–2.69.
   THE FIX IS THE BACKGROUND RATHER THAN THE INK, because the ink is what carries the meaning here: the
   four colours ARE the four answers, so re-toning them to a common dark would take the grade bar's whole
   language away. With all three runs at full-strength white the darkening needed is gentle — a factor of
   .67 to .99 — so each button keeps its own hue and simply stops being a pastel. Solved per colour and
   per mode against white; every one lands 4.61–4.71.
   Written `body.hc:not(.night)` / `body.hc.night` (0,4,0) rather than `body.hc` alone, since `.night
   .grade.again` is (0,3,0) and sits a thousand lines BELOW this block — at equal specificity it would
   win on source order and the night bar would be unchanged. */
body.hc .grade .gl, body.hc .grade .gi, body.hc .grade .gk{color:#fff;}
body.hc:not(.night) .grade.again{background:#C54C45;}
body.hc:not(.night) .grade.hard{background:#A2672B;}
body.hc:not(.night) .grade.good{background:#428255;}
body.hc:not(.night) .grade.easy{background:#4E73C0;}
body.hc.night .grade.again{background:#B7574F;}
body.hc.night .grade.hard{background:#966B3A;}
body.hc.night .grade.good{background:#45805C;}
body.hc.night .grade.easy{background:#5974AD;}
/* a focus ring the mode can't leave to chance: thicker, and always the bright indigo rather than the theme's */
body.hc :focus-visible{outline:3px solid var(--indigo-bright) !important; outline-offset:2px;}

*{box-sizing:border-box;}
html,body{margin:0;padding:0;}
/* ---------- skip link (index.html's first element) ----------
   Off screen until it takes focus, then a real button at the top-left. Positioned rather than
   `display:none`, which would take it out of the tab order and defeat the whole point of it. */
.skip-link{
  position:fixed; left:12px; top:-70px; z-index:9900;
  background:var(--indigo); color:#fff; font-family:var(--mono); font-size:calc(12.5px * var(--fs));
  letter-spacing:.06em; text-transform:uppercase; text-decoration:none;
  padding:11px 18px; border-radius:9px; box-shadow:var(--shadow-md);
  transition:top .18s var(--ease);
}
.skip-link:focus{top:12px;}
body.night .skip-link{color:var(--paper);}
#view:focus{outline:none;}   /* it takes focus only as the skip link's target, never by tabbing to it */
body{
  background:var(--paper); color:var(--ink);
  font-family:var(--serif); font-size:calc(17px * var(--fs)); line-height:1.6;
  -webkit-font-smoothing:antialiased; text-rendering:optimizeLegibility;
  transition:background .4s var(--ease), color .4s var(--ease);
}
/* ---------- Text size (Settings → Appearance) ----------
   `--fs` multiplies EVERY px font-size in this file (Aug 2026, on request — it reached only the reading
   prose before: a card's question and background, a glossary popup, an Atlas panel). It is applied by
   rewriting each declaration as `calc(<its own px> * var(--fs))`, so every element keeps its size relative
   to its neighbours and only the text scales — the boxes are still laid out in px, which is what keeps the
   shell from coming apart at Large. The two settings away from Medium are the only rules needed; the
   default is the token above.
   What this deliberately does NOT reach is the ATLAS's canvas labels: those are `ctx.fillText` at computed
   sizes, and their spacing arithmetic (the collision grid, the leader lines) is written against those
   numbers. CSS cannot see them and scaling them would rearrange the map, not enlarge it. */
/* Five stops (Aug 2026, on request — "very small" and "very large" were added at the ends). The steps
   are deliberately not even: the middle three keep the values they have always had, so nobody's chosen
   size moves under them, and the two new ends sit a little further out than that spacing would give —
   a reader who asks for "very large" is asking for a real difference, not one more nudge. */
body[data-fs="tiny"]{--fs:.8;}
body[data-fs="small"]{--fs:.9;}
body[data-fs="large"]{--fs:1.14;}
body[data-fs="huge"]{--fs:1.32;}
/* ON A PHONE THE WHOLE SCALE SITS ONE STOP LOWER (Oct 2026, on request: "the 'Small' text size should be
   the new default; shift the other sizes accordingly"). Medium — the default, and what every reader who
   has never touched the slider is on — renders at what Small was, and each other stop moves down one
   with it: Small takes Very small's value, Very small steps one more .1 below, Large is the old Medium,
   Very large the old Large. The STORED value and the slider's five names are untouched, so nothing
   migrates and a phone and a desktop sharing one account each show the setting at their own size. The
   breakpoint is the phone's, the one the bottom tab bar appears at. */
@media (max-width:640px){
  :root{--fs:.9;}
  body[data-fs="tiny"]{--fs:.7;}
  body[data-fs="small"]{--fs:.8;}
  body[data-fs="medium"]{--fs:.9;}
  body[data-fs="large"]{--fs:1;}
  body[data-fs="huge"]{--fs:1.14;}
}
body::before{
  content:""; position:fixed; inset:-15%; z-index:-1; pointer-events:none;
  background:
    radial-gradient(38% 46% at 16% 8%, color-mix(in srgb,var(--indigo) 20%, transparent), transparent 70%),
    radial-gradient(42% 50% at 88% 16%, color-mix(in srgb,var(--zh) 15%, transparent), transparent 72%),
    radial-gradient(46% 52% at 78% 96%, color-mix(in srgb,var(--ochre) 15%, transparent), transparent 72%),
    radial-gradient(40% 48% at 6% 90%, color-mix(in srgb,var(--indigo-bright) 12%, transparent), transparent 72%);
  filter:blur(70px) saturate(112%); opacity:.85;
  transition:opacity .4s var(--ease);
}
body.night::before{opacity:.5;}
/* Light mode: more pronounced gradient (stronger color, saturation, opacity); dark mode unchanged */
body:not(.night)::before{
  background:
    radial-gradient(40% 48% at 16% 8%, color-mix(in srgb,var(--indigo) 36%, transparent), transparent 72%),
    radial-gradient(44% 52% at 88% 16%, color-mix(in srgb,var(--zh) 30%, transparent), transparent 74%),
    radial-gradient(48% 54% at 78% 96%, color-mix(in srgb,var(--ochre) 30%, transparent), transparent 74%),
    radial-gradient(42% 50% at 6% 90%, color-mix(in srgb,var(--indigo-bright) 26%, transparent), transparent 74%);
  filter:blur(64px) saturate(126%); opacity:.96;
}
a{color:var(--indigo-bright);}
::selection{background:rgba(110,127,196,.18);}
.night ::selection{background:rgba(166,180,240,.22);}
/* DEFAULT THEME SIGNATURE — the opal (Sep 2026, on request: "replace the default Folio theme with the
   Opalite theme, which should be renamed to Folio"). What used to sit here was a bookbinder's ribbon, a
   hairline of indigo → vermilion → gold under the nav, and it went with the palette it belonged to: the
   default's signature is now the stone's, which is the iridescence below and the cabochon corners with it.
   THE IRIDESCENCE IS THE POINT: the ground shifts through the whole opal spectrum rather than sitting on
   one hue, which is what separated this from Amethyst when it was a gemstone theme. Static, like every
   other theme's ambient.
   ⚠ IT MUST STAY BELOW `body:not(.night)::before`. Both selectors are (0,1,2) — an attribute and a `:not`
   weigh the same — so this wins on SOURCE ORDER alone, and moving it above that rule silently gives the
   default back the four-blob gradient it is replacing. */
body[data-theme="folio"]::before{
  background:linear-gradient(135deg,#F4F0FB 0%, #EAF3FB 22%, #EFF8F2 44%, #FBF3F7 68%, #F2EFFC 100%);
  opacity:1; filter:none;
}
body[data-theme="folio"].night::before{
  background:linear-gradient(135deg,#181630 0%, #101C2A 24%, #0F1F1E 48%, #23142A 72%, #14122A 100%);
}
/* the roundest corners on the shelf — an opal is a cabochon, never faceted */
body[data-theme="folio"] .card, body[data-theme="folio"] .collection,
body[data-theme="folio"] .study-card, body[data-theme="folio"] .banner,
body[data-theme="folio"] .game-tile, body[data-theme="folio"] .placard{border-radius:20px;}
body[data-theme="folio"] .btn, body[data-theme="folio"] .grade{border-radius:999px;}
body[data-theme="folio"] .page-head h1{font-family:var(--display); font-weight:700; letter-spacing:-.01em;}

/* utility eyebrow */
.eyebrow{
  font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.2em;
  text-transform:uppercase; color:var(--indigo); font-weight:600;
}

/* ============================================================
   TOP BAR
   ============================================================ */
.topbar{
  position:sticky; top:0; z-index:50; height:var(--bar-h);
  display:flex; align-items:center; gap:24px;
  padding:0 clamp(16px,4vw,40px);
  background:color-mix(in srgb, var(--paper) 86%, transparent);
  backdrop-filter:saturate(140%) blur(10px);
  -webkit-backdrop-filter:saturate(140%) blur(10px);
  border-bottom:1px solid var(--rule);
}
/* THE TAGLINE SITS UNDER THE WORDMARK (Sep 2026, on request: "move the tagline to right below the
   logo"). It was a baseline-aligned row, which is why the two carry no shared alignment rule between
   them any more: as a column the mark's own left edge is the tagline's, and `align-items:flex-start`
   is what holds that at every text size rather than a hand-written offset. The gap is 4px because the
   mark is set `line-height:1` — it has no descender space of its own to stand in for one. */
.brand{display:flex; flex-direction:column; align-items:flex-start; gap:4px; flex:none; user-select:none; cursor:pointer; appearance:none; border:0; padding:0; margin:0; background:none; font:inherit; color:inherit; text-align:left;}
.brand:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px; border-radius:5px;}
.brand .mark{
  font-family:var(--display); font-weight:900; font-size:calc(23px * var(--fs)); letter-spacing:-.01em;
  color:var(--ink); line-height:1;
}
.brand .mark .dot{color:var(--zh);}
/* The tracking comes down with the stacking. At .28em the tagline was half again as wide as the
   wordmark above it, which as a ROW was invisible and as a COLUMN reads as a line that has escaped
   the block it belongs to; at .16em the two are close enough in width to read as one mark. */
.brand .tag{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.16em; color:var(--ink-faint); text-transform:uppercase;}
/* WHICH OF THE TWO LOGOS IS ON SCREEN, AND WHY THE SPLIT IS AT 1180px (Sep 2026, with the masthead).
   The obvious rule is "bar logo wherever the bar is shown, home logo below 640px" — and it does not fit.
   MEASURED, in a browser, with the seven tabs in: STACKED, the brand costs the bar 88px at Medium text
   and 186px at Very large, and the bar has that much to spare only from 1180px up. Below it the tabs are
   pushed off the end — 259px of overflow at 901px, 164px at 1000px and 64px at 1100px at Very large —
   which takes a destination away to make room for an ornament, exactly backwards. (The band rules further
   down already claw back what they can between 641 and 900px, and that is spent on the tabs.)
   THE THRESHOLD CAME DOWN FROM 1280px WHEN THE TAGLINE WENT UNDER THE WORDMARK: a row 171px wide became
   a column 88px wide, which is most of a hundred pixels back and two more classes of screen inside the
   bar. It is a MEASUREMENT, so re-measure it if either half of the brand changes.
   So the BAR's logo is shown from 1180px up and the HOME page's from 1179px down, and exactly one of the
   two is on screen at every width — including a portrait tablet, which gets it at the head of the home
   page rather than in the bar. `.home-brand` is a block of its own above the greeting; it takes no
   pointer, being a heading on the page it would have linked to. */
.topbar .brand{display:none;}
@media (min-width:1180px){ .topbar .brand{display:flex;} }
.home-brand{display:flex; cursor:default; margin:0 0 18px;}
@media (min-width:1180px){ .home-brand{display:none;} }
/* ...AND A PHONE GETS NEITHER (Sep 2026, on request: "on mobile, remove the logo and tagline from the
   top of the home page"). This reverses the rule above at the narrow end and is deliberate: on a 390px
   screen the wordmark and its tagline are the first 60px of a page whose whole job is the day's study, and
   a masthead is worth its room only where there is room. So the split is now three-way rather than two —
   nothing below 640px, the home page's logo from 641 to 1179, the bar's from 1180 up — and the centring
   rule this replaced is gone with the block it centred. */
@media (max-width:640px){ .home-brand{display:none;} }

.nav{display:flex; align-items:center; gap:4px;}
.nav.right{margin-left:auto;}
.tab{
  position:relative; appearance:none; border:0; background:none; cursor:pointer;
  display:inline-flex; align-items:center;
  font-family:var(--mono); font-size:calc(11.5px * var(--fs)); letter-spacing:.16em; text-transform:uppercase;
  color:var(--ink-quiet); font-weight:500; padding:10px 13px; border-radius:7px;
  transition:color .18s var(--ease), background .18s var(--ease);
}
.tab .tab-ic{width:16px; height:16px; flex:none;}
/* THE LABEL IS ALWAYS OPEN (Aug 2026, on request). These tabs used to be icon-only, the name unfolding
   beside the icon on hover / keyboard focus and staying open on the page currently shown — which meant a
   reader had to point at four icons to find out what they were, and the one name on screen belonged to the
   page they were already on. There are only four destinations and the bar has room for all four, so the
   name is simply there; `.active` still says which page you are on, in the indigo and the underline.
   The colour transition is kept and the width one goes with the collapse: nothing animates any more. */
.tab .tab-label{
  display:block; overflow:hidden; white-space:nowrap; margin-inline-start:8px;
}
.tab:hover{color:var(--ink-soft); background:color-mix(in srgb, var(--ink) 5%, transparent);}
.tab.active{color:var(--indigo);}

/* ---------- SEVEN TABS, AND THE BAR IS NARROWER THAN THEY ARE BETWEEN 641 AND 900px ----------
   (Sep 2026, with the Collections tab.) The top bar is desktop-only — below 640px it is `display:none`
   and the bottom bar takes over — so the band this has to answer for is 641px to about 900px, a small
   laptop or a tablet in landscape. Measured with the seventh tab in: 0px of overflow at 900px and above,
   76px at 760 and 134px at 700, all of it in the tabs' own padding and tracking rather than in the words.
   SO THE PADDING AND THE TRACKING COME IN, AND EVERY NAME STAYS. That order is deliberate and is the
   Aug 2026 request's own: the labels were put there because a row of unnamed icons is a row a reader has
   to point at one by one, so the last thing to give is the word. Padding 13px -> 7px and tracking .16em ->
   .06em across seven tabs, with the bar's own gap and side padding in with them.
   The `--fs` scale is what makes this a band rather than a breakpoint - at Very large text the words are a
   third wider - so it is written to hold at the largest of them rather than at Medium. */
@media (min-width:641px) and (max-width:900px){
  .topbar{gap:12px; padding:0 clamp(10px,2vw,20px);}
  .nav{gap:2px;}
  .topbar .tab{padding:10px 7px; letter-spacing:.06em;}
  .topbar .tab .tab-label{margin-inline-start:6px;}
  /* …AND THE EIGHTH TAB GIVES UP ITS WORD, WHICH IS THE ONE THAT CAN (Sep 2026, with Search).
     Measured with eight tabs in: the tightening above holds to 768px and then runs out — 28px of
     overflow at 700 and 86px at 641 — so something has to go, and the rule above says the last thing
     to give is a name. A MAGNIFIER IS THE EXCEPTION AND ONLY THE MAGNIFIER: it is the one icon in the
     bar that names itself, universally, without a word beside it — which is exactly what could not be
     said of a house, a globe or a stack of cards, and why the other seven keep theirs. The name is
     CLIPPED rather than removed, so it still reaches a screen reader; `aria-label` would be the
     alternative and would mean the markup carrying the word twice. */
  .topbar .tab-search .tab-label{
    position:absolute; width:1px; height:1px; overflow:hidden;
    clip-path:inset(50%); white-space:nowrap; margin:0;
  }
  .topbar .tab-search{padding-inline:9px;}
}
/* THE NARROW END OF THE BAND NEEDED ONE MORE TURN (Sep 2026, measured at every width from 641 to 1440).
   Two things were found there and only one of them is new. With the Search tab in, 641px overflowed by
   39px; WITHOUT it, by 3 — so the bar has been three pixels too wide at its narrowest supported width
   all along, and nobody could see it because until the fonts were self-hosted a sandbox run had no
   webfonts at all and the fallback stack is narrower. (It is what `test-layout`'s Atlas-sheet assertion
   was reporting as `horiz: 3`.)
   Both are closed by the same turn of the screw, and it stays in the padding rather than the words:
   7px -> 4px each side across eight tabs is 48px, against 39 needed. Measured after: 0 at every width
   tested, with the Admin tab shown, which is the widest the bar ever gets. */
@media (min-width:641px) and (max-width:720px){
  .topbar{gap:8px; padding:0 8px;}
  .topbar .tab{padding:10px 4px;}
  .topbar .tab .tab-label{margin-inline-start:5px;}
}
/* …and an admin-only tab wears --admin instead, in every state. It is the colour that says so and not a
   badge: the nav is icons and one word each, and a second mark on two of six tabs would be more furniture
   than fact. `applyMode` still HIDES these for everyone else — this is what an editor sees, not a gate. */
/* The Admin tab ships `hidden` and applyMode reveals it for an admin; the author `display` on `.tab`
   would otherwise beat the UA's [hidden] rule and show it to everybody. */
.tab[hidden]{display:none;}
.tab.tab-admin{color:color-mix(in srgb, var(--admin) 62%, var(--ink-faint));}
.tab.tab-admin:hover{color:var(--admin); background:color-mix(in srgb, var(--admin) 8%, transparent);}
.tab.tab-admin.active{color:var(--admin);}
.tab.tab-admin .ink{background:var(--admin);}
.tab.tab-admin:focus-visible{outline-color:var(--admin);}
/* the underline sits under the unfolded page NAME only, never the icon:
   --ink-start = horizontal padding + icon width + the open label's margin */
.tab .ink{
  position:absolute; inset-inline-start:var(--ink-start, 37px); inset-inline-end:var(--ink-end, 13px);
  bottom:4px; height:2px; border-radius:2px;
  background:var(--indigo); transform:scaleX(0); transform-origin:left; opacity:0;
  transition:transform .28s var(--ease), opacity .2s var(--ease);
}
.tab.active .ink{transform:scaleX(1); opacity:1;}
.tab:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}

/* in first-time-visitor preview, the Admin entry point disappears */
body.visitor-mode .tab-admin{display:none;}

/* language picker — Settings → Language (it was a top-bar dropdown until Aug 2026; a preference
   belongs on the preferences page, and on a phone the top bar it lived in is gone entirely) */
.lang-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:10px;}
.lang-opt{display:flex; align-items:center; gap:10px; width:100%; text-align:left; appearance:none; cursor:pointer; padding:11px 13px; border-radius:10px; border:1.5px solid var(--rule); background:var(--card); color:var(--ink); transition:border-color .15s, box-shadow .15s, background .14s var(--ease);}
.lang-opt:hover{border-color:var(--ink-faint);}
.lang-opt.on{border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash); color:var(--indigo);}
.lang-opt:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.lo-code{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.08em; font-weight:700; margin-inline-start:auto; flex:none; color:var(--ink-faint);}
.lang-opt.on .lo-code{color:var(--indigo);}
.lo-label{font-size:calc(14.5px * var(--fs));}
/* SVG country flags (emoji flags render as bare letter pairs on Windows) */
.lo-flag{width:21px; height:14px; flex:none; border-radius:2.5px; overflow:hidden; box-shadow:0 0 0 1px color-mix(in srgb, var(--ink) 18%, transparent); display:inline-flex;}
.lo-flag svg{width:100%; height:100%; display:block;}
/* RTL (Arabic): flip the reading direction; flex/grid layouts follow the dir attribute on <html> */
html[dir="rtl"] .lang-opt{text-align:right;}
html[dir="rtl"] .page-head{text-align:right;}

/* mobile nav condense */
.nav-toggle{display:none;}

/* ============================================================
   PAGE SHELL + TRANSITIONS
   ============================================================ */
.stage{max-width:880px; margin:0 auto; padding:clamp(22px,4vw,42px) clamp(16px,4vw,40px) 90px; position:relative;}
/* touch-action is what makes every horizontal SWIPE on the site possible, and without it none of them
   worked on a real phone (Aug 2026, on a report that the book's chapter swipe did nothing — the page swipe
   between home/collections/library/account/settings was broken in exactly the same way and had been since
   it shipped). Under the default `auto` the browser hands a touch to its scroll machinery the moment it
   passes the slop and fires POINTERCANCEL at the page: pointerup never arrives, so a handler that measures
   the gesture at pointerup can never see one. It is invisible to the tests because a synthesised
   PointerEvent bypasses that arbitration entirely and completes every time.
   `pan-y` says this box scrolls vertically and nothing else, so a horizontal drag is no longer anyone's
   scroll and the pointer stream survives to pointerup; a vertical one still scrolls and still cancels,
   which is correct — that is a scroll and not a swipe. `pinch-zoom` keeps the reader able to zoom, which
   bare `pan-y` would take away. It costs double-tap-to-zoom, which the book already suppresses for its own
   double tap. Nested horizontal scrollers are UNAFFECTED (measured): the intersection that decides a pan
   stops at the element that will scroll, so the chapter bar, the Atlas sheet's pager and the heatmap still
   pan sideways, and every draggable that declares touch-action:none only ever narrows this further.

   IT HAS TO BE ON THE PAGE'S ANCESTORS TOO, and it was on `.page` alone for a fortnight (Aug 2026, on a
   report that swiping left from the Library did nothing when the finger started in the empty space below
   the content). A short page leaves the bottom of the screen covered by `.stage`'s 90px of padding and
   then by the body itself, and neither of those is `.page` — so a finger landing there met the default
   `auto`, the browser took the gesture for a scroll, and pointerup never arrived. The gesture worked or
   did not depending on WHERE on the page it began, which reads as the swipe being unreliable rather than
   as a rule with a hole in it. `body` is the honest place for it: the document scrolls vertically and
   nothing else, at every width and on every route. Nested horizontal scrollers are still unaffected for
   the reason above — the intersection stops at the element that will scroll. */
body{touch-action:pan-y pinch-zoom;}
.stage,#view{touch-action:pan-y pinch-zoom;}
.page{animation:pageIn .42s var(--ease) both; touch-action:pan-y pinch-zoom;}
@keyframes pageIn{from{opacity:0; transform:translateY(10px) scale(.995);} to{opacity:1; transform:none;}}
/* The outgoing page (see makePageGhost in app.js): a CLONE of it, laid over the stage to fade while the new
   one renders underneath — render() is synchronous and cannot wait for an exit animation, so the exit
   happens beside it rather than before it. Never interactive, never in the layout, and never in the
   accessibility tree. #view has to be positioned for it, so the copy lands exactly over the new page. */
#view{position:relative;}
.page-ghost{position:absolute; left:0; right:0; top:0; pointer-events:none; z-index:2; animation:pageOut .26s var(--ease) both;}
@keyframes pageOut{from{opacity:1; transform:none;} to{opacity:0; transform:translateY(-8px) scale(.994);}}

/* ============================================================
   MOTION (Aug 2026, on request: "animated transitions between pages and for actions")
   ============================================================
   Everything here is decoration over layout that already works, and every rule is an animation or a
   transition — so the stylesheet's global prefers-reduced-motion killswitch at the foot of this file
   covers the lot with no per-rule handling. Two standing constraints:
     · nothing may change an element's LAYOUT (no margins, no display), only its paint and its transform,
       so a mid-animation measurement (test-layout.js, computeCityLayout, cpMinH) reads the settled value;
     · the entrance stagger opts out exactly where the page-level animation already does — the Atlas
       (a canvas being drawn into) and the editor (a repaint per keystroke).
   ------------------------------------------------------------ */
/* A page arrives as a sequence rather than a slab: its top-level blocks follow the page in, a beat apart.
   Capped at eight — past that the last block would arrive noticeably late, and a long page is scrolled to
   rather than read from the top. */
.page > *{animation:sectIn .42s var(--ease) both;}
.page > *:nth-child(1){animation-delay:.02s;}
.page > *:nth-child(2){animation-delay:.06s;}
.page > *:nth-child(3){animation-delay:.10s;}
.page > *:nth-child(4){animation-delay:.14s;}
.page > *:nth-child(5){animation-delay:.17s;}
.page > *:nth-child(6){animation-delay:.20s;}
.page > *:nth-child(n+7){animation-delay:.22s;}
@keyframes sectIn{from{opacity:0; transform:translateY(7px);} to{opacity:1; transform:none;}}
/* …and the same three opt-outs the page animation itself takes. The ghost must not re-run the entrance on
   its own children either, or the outgoing page flickers back in as it fades out. */
.page:has(.atlas) > *,body.admin-mode .page > *,.page-ghost > *{animation:none;}
/* A QUIET REPAINT (renderInPlace in app.js) is a page rebuilt UNDER the reader rather than navigated to —
   a downloaded deck's row turning into the deck itself is the case it was written for — so nothing may
   fade, slide or stagger: the whole point is that the reader cannot tell the list was rebuilt. It reaches
   the page, its blocks and the deck rows' own entrance, which is a separate animation one level down.
   Deliberately a class on the PAGE element rather than on the body, like `.page-next`, so it dies with the
   page it belongs to and can never be left behind on the next ordinary navigation. */
.page.page-quiet,.page-quiet > *,.page-quiet .active-deck{animation:none;}

/* A swiped navigation comes in from the side the finger came from, and the outgoing page leaves the other
   way — so the gesture and the transition tell the same story (see wirePageSwipe in app.js). The class is
   set on the page element itself rather than on the body, so it dies with that page and can never be left
   behind on a subsequent tab-bar navigation.

   It is a full CROSS-SLIDE (Aug 2026, on a report that it was "a hard cut"). It used to be a 26px nudge
   under a cross-fade, which at that distance is a fade with a lean in it: the finger had just dragged a
   page sideways and what arrived barely moved. Both halves now travel a whole page width — the two exactly
   adjacent, with a 24px gutter so their edges never abut — at ONE duration and ONE easing, which is what
   makes it read as a single sheet moving rather than as two things happening at once. Hence no opacity
   here at all: a page that is off the side of the screen needs no fading, and fading it would only make
   the sheet look like it were dissolving as it went. PAGE_GHOST_MS in app.js must stay in step with the
   .34s, or the outgoing page is removed part-way across.

   The stage clips horizontally for the length of the move (body.stage-sliding — see clipStageFor); without
   that, a page a whole screen wide overflows the document while it travels. */
.page.page-next{animation:pageInNext .34s var(--ease) both;}
.page.page-prev{animation:pageInPrev .34s var(--ease) both;}
@keyframes pageInNext{from{transform:translateX(calc(100% + 24px));} to{transform:none;}}
@keyframes pageInPrev{from{transform:translateX(calc(-100% - 24px));} to{transform:none;}}
.page-ghost.ghost-next{animation:pageOutNext .34s var(--ease) both;}
.page-ghost.ghost-prev{animation:pageOutPrev .34s var(--ease) both;}
@keyframes pageOutNext{from{transform:none;} to{transform:translateX(calc(-100% - 24px));}}
@keyframes pageOutPrev{from{transform:none;} to{transform:translateX(calc(100% + 24px));}}
/* The staggered entrance is for a page ARRIVING in place; on a page that is sliding in as one slab it
   would hold each block invisible until the slab had nearly landed, and they would then pop in behind it.
   Same opt-out as the Atlas and the editor take, two rules above. */
.page.page-next > *,.page.page-prev > *{animation:none;}
/* overflow-x ALONE — never the shorthand and never `hidden`. `overflow-x:clip` beside an untouched
   `overflow-y:visible` is the one pairing that clips without making the stage a scroll container, so the
   book's sticky chapter bar and every other sticky thing on the site keep resolving against the viewport.
   The clip is at the stage's PADDING box, which on a phone is the screen edge — which is why this is on the
   stage and not on #view, that sitting inside the padding. `overflow-clip-margin` then gives the paint a
   little slack past it, for the desktop, where the stage is 880px centred in a wider window and the book's
   chapter slide would otherwise be cut off at the column edge with room to spare either side. It buys
   nothing on a phone (past the screen edge is past the window) and costs nothing where it is unsupported:
   clipping is what stops the overflow being scrollable, and the margin only moves where it is painted. */
body.stage-sliding .stage{overflow-x:clip; overflow-clip-margin:80px;}

/* Overlays: the backdrop washes in and the box rises to meet it — and, since Aug 2026, leaves the same way.
   These had no entrance at all once, and then an entrance and no exit, which left the dismissal as the one
   abrupt half of a control that is otherwise entirely animated. `.closing` is applied by deckSheet's close
   (app.js), which also stops hit-testing the overlay at once, so the gesture is finished the instant it is
   made whatever the paint is still doing. DECK_SHEET_OUT_MS must stay in step with the duration here. */
.inline-prompt,.deck-menu{animation:overlayIn .18s var(--ease) both;}
.ip-box,.dm-box{animation:dialogIn .26s var(--ease) both;}
@keyframes overlayIn{from{opacity:0;} to{opacity:1;}}
@keyframes dialogIn{from{opacity:0; transform:translateY(12px) scale(.97);} to{opacity:1; transform:none;}}
.deck-menu.closing{pointer-events:none; animation:overlayOut .19s var(--ease) both;}
.deck-menu.closing .dm-box{animation:dialogOut .19s var(--ease) both;}
@keyframes overlayOut{from{opacity:1;} to{opacity:0;}}
@keyframes dialogOut{from{opacity:1; transform:none;} to{opacity:0; transform:translateY(8px) scale(.98);}}

/* Press feedback on the buttons that had none. `.btn` already lifts and settles; these are the quieter
   controls — the account page's ghost buttons, the editor's mini buttons, the sheet rows — where a click
   previously produced no acknowledgement of its own at all. */
.ghost-btn,.mini-btn{transition:background .15s var(--ease), border-color .15s var(--ease), transform .1s var(--ease);}
.ghost-btn:active,.mini-btn:active,.dm-item:active{transform:scale(.975);}
/* A tile is big enough that a scale reads as a shove; it dips instead. */
.game-tile:active{transform:translateY(1px);}
/* The global killswitch at the foot of this file collapses DURATIONS, not delays — and `both` holds an
   element at its start state (invisible) through its delay, so without this a reduced-motion reader would
   watch a page arrive in blank steps over a fifth of a second. Delays go to zero there. */
@media (prefers-reduced-motion:reduce){ .page > *{animation-delay:0s !important;} }

/* The shipped version and the moment it was released, in the top-left corner of the home page — quiet
   enough to be ignored and precise enough to quote in a bug report (see versionLineHTML in app.js).
   `--ink-faint` is the site's own quiet token, so the high-contrast mode re-tones it along with every
   caption and source line; don't hard-code a grey here. It stays LEFT-aligned at every width: below
   640px the page head under it is centred, and a centred version number reads as a title rather than
   as a stamp. */
.site-ver{
  font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em;
  color:var(--ink-faint); line-height:1; margin:0 0 8px; text-align:left;
}
html[dir="rtl"] .site-ver{text-align:right;}

.page-head{margin:2px 0 22px;}
.page-head h1{font-family:var(--display); font-weight:600; font-size:calc(clamp(28px,5vw,38px) * var(--fs)); margin:6px 0 0; letter-spacing:-.015em;}
.page-head p{color:var(--ink-quiet); margin:8px 0 0; font-size:calc(15.5px * var(--fs)); max-width:60ch;}
/* a slim accent hairline under every page title (centred fade on mobile, where the head is centred) */
.page-head::after{content:""; display:block; width:190px; max-width:38%; height:2px; margin-top:14px; border-radius:2px;
  background:linear-gradient(90deg, color-mix(in srgb, var(--indigo) 55%, transparent), transparent);}

/* ============================================================
   BUTTONS
   ============================================================ */
.btn{
  appearance:none; cursor:pointer; font-family:var(--mono); font-weight:600;
  font-size:calc(12.5px * var(--fs)); letter-spacing:.08em; text-transform:uppercase;
  border:1px solid var(--indigo); background:var(--indigo); color:#fff;
  padding:13px 22px; border-radius:9px; line-height:1;
  transition:transform .12s var(--ease), box-shadow .2s var(--ease), background .2s var(--ease), opacity .2s;
  box-shadow:var(--shadow-sm);
}
.btn:hover{transform:translateY(-1px); box-shadow:var(--shadow-md);}
.btn:active{transform:translateY(0) scale(.98);}
.btn:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.btn.ghost{background:transparent; color:var(--indigo); border-color:var(--rule);}
.btn.ghost:hover{border-color:var(--indigo); background:var(--indigo-wash);}
.btn.zh{background:var(--zh); border-color:var(--zh);}
.btn.block{display:block; width:100%; text-align:center;}
.btn[disabled]{opacity:.45; cursor:not-allowed; transform:none; box-shadow:none;}

/* ============================================================
   PROGRESS BAR
   ============================================================ */
.prog{display:flex; align-items:center; gap:12px;}
.prog .track{flex:1; height:7px; border-radius:99px; background:var(--paper-2); overflow:hidden; border:1px solid var(--rule);}
.prog .fill{height:100%; border-radius:99px; background:linear-gradient(90deg, var(--indigo), var(--indigo-bright)); width:0; transition:width .9s var(--ease);}
.prog .count{font-family:var(--mono); font-size:calc(11.5px * var(--fs)); color:var(--ink-faint); letter-spacing:.04em; white-space:nowrap; font-weight:500;}
.prog.zh .fill{background:linear-gradient(90deg, var(--zh), #E0786F);}
/* ---------- XP / level bars (replace progress bars on collections + the daily-review banner) ---------- */
.xp{display:flex; flex-direction:column; gap:6px;}
.xp-head{display:flex; align-items:baseline; justify-content:space-between; gap:12px;}
.xp-lvl{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--indigo); font-weight:600;}
.xp-count{font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-faint); letter-spacing:.04em; white-space:nowrap; font-weight:500;}
.xp-track{height:8px; border-radius:99px; background:var(--paper-2); overflow:hidden; border:1px solid var(--rule);}
.xp-fill{height:100%; border-radius:99px; background:linear-gradient(90deg, var(--indigo), var(--indigo-bright)); width:0; transition:width .9s var(--ease);}
/* ---------- A COLLECTION WORKED ALL THE WAY THROUGH (Aug 2026, on request) ----------
   The name and the full bar both go GOLD — #C39A2E, the achievement gold the earned badge, the
   collection icons and the daily-study banner's own bar already use, so a finished collection reads as
   the same kind of thing as a badge rather than as a new colour.
   The class sits on the BAR (deckProgMarkup / adProg both write `prog-done`), and the title is reached
   from it with `:has()` — one flag, so the two halves of "displayed in gold" cannot come apart, and
   every surface that draws one of these bars is covered without a rule apiece. `:has()` is used for the
   crossword squares and the study card already.
   The collection HUE still wins on the bar of an unfinished one: these rules are `.prog-done` only.
   `!important` is not used anywhere here — each of these outranks the rule it replaces on CLASS COUNT
   rather than on source order, which matters because the rules it has to beat are scattered through the
   file and two of them come after this one: `.collection-row .xp-fill`, `.cl-row .xp-fill` and
   `.dp-row .prog .fill` are two or three classes, so the finished state is written `.xp.prog-done
   .xp-fill` / `.prog.prog-done .fill` — three, on the same two elements. */
.xp.prog-done .xp-fill,
.prog.prog-done .fill{background:linear-gradient(90deg, #C39A2E, #E6C765);}
body.night .xp.prog-done .xp-fill,
body.night .prog.prog-done .fill{background:linear-gradient(90deg, #D4AA3C, #F2DC96);}
/* the count beside the bar goes with it — a deeper gold, because at 10px #C39A2E on the card is 3.6:1
   (the same reasoning, and the same value, as `.banner .xp-lvl`) */
.prog-done .xp-lvl,.prog-done .xp-count{color:#8A6A1E;}
/* ---------- HOW LONG THIS READER HAS SPENT IN THIS BOOK (Aug 2026, on request) ----------
   At the top of a book's front matter, above the essay: the one thing on that page that is about the
   reader rather than about the book. It is set as a figure with a caption beside it rather than as a
   sentence, so it reads at a glance and does not compete with the prose under it — the same shape the
   daily-study footer's timer uses, and deliberately the same face and size, since they are the same
   clock. Drawn only when there is something to say (see bookIntroChapter), so there is no empty state. */
.bk-clock{display:flex; align-items:baseline; gap:8px; flex-wrap:wrap; margin:0 0 18px; padding-bottom:14px;
  border-bottom:1px solid var(--rule);}
.bkc-fig{font-family:var(--display); font-weight:600; font-size:calc(19px * var(--fs)); color:var(--ink); line-height:1;}
.bkc-lbl{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint);}

body.night .prog-done .xp-lvl,body.night .prog-done .xp-count{color:#E0C270;}
/* …and the collection's NAME, on all three surfaces that pair a title with one of these bars */
.collection-main:has(.deck-prog.prog-done) .collection-title,
.dk-body:has(.dk-prog.prog-done) .dk-title{color:#8A6A1E;}
body.night .collection-main:has(.deck-prog.prog-done) .collection-title,
body.night .dk-body:has(.dk-prog.prog-done) .dk-title{color:#E6C765;}
/* Library: each collection's bar fills in its own identity hue */
.collection-row .xp-fill{background:linear-gradient(90deg, var(--coll-bg, var(--indigo)), color-mix(in srgb, var(--coll-bg, var(--indigo)) 62%, white));}
/* A collection / deck PROGRESS bar (deckProgMarkup) borrows the .xp shape so every theme rule, hue and
   animateProgs above applies unchanged. Only its head differs: "Studied" is a caption rather than a level,
   so it takes the quiet ink instead of the indigo the level label wears. */
.deck-prog .xp-lvl{color:var(--ink-faint); font-weight:500;}
/* home banner stats: Anki's three piles in Anki's colours — blue new, red learning, green review. The same
   three run down the deck rows below the banner as .dkc-new / .dkc-learn / .dkc-rev, and the study bar's
   own .cnt row has always used exactly these tokens; keep all three sites on the same palette. */
.banner .stat.st-new b{color:var(--indigo-bright);}
.banner .stat.st-learn b{color:var(--zh);}
.banner .stat.st-rev b{color:var(--good, #4F9D67);}
.banner .stat.stat-zero b{color:var(--ink-faint);}
/* THE LEVEL BADGE IS GONE ENTIRELY (Aug 2026). `.level-badge` / `.lb-num` / `.lb-lbl` dressed a large
   numeral in two places: the home Daily-study banner, which gave its numeral up earlier that month on
   request, and the COLLECTION banners, which gave theirs up with collection levels themselves. Nothing
   was left rendering one, so the rules went with `levelBadgeMarkup` — along with the per-script variants
   (.level-badge.zh / .num-ja / .num-roman / .num-greek / .num-cyrillic / .num-devanagari) that counted a
   collection's level in its own numerals. Folio's level is still spelled out in words by the banner's xp
   bar (.xp-lvl). A collection carries a subject icon instead: */
/* ---- a collection's subject icon (Aug 2026, on request) ----
   It stands where the level numeral stood, at the same width, so nothing around it had to move. GOLD
   rather than the collection's own hue: the banner wash IS that hue, and the gold is the one colour
   already proven to read over all ten of them in all eight themes, light and dark. */
.coll-ic{flex:none; display:flex; align-items:center; justify-content:center; min-width:56px; color:#C39A2E;}
.coll-ic svg{width:calc(34px * var(--fs)); height:calc(34px * var(--fs)); display:block;}
body.night .coll-ic{color:#E6C765;}
.cl-row .coll-ic, .udeck .coll-ic{min-width:46px;}
.cl-row .coll-ic svg{width:calc(28px * var(--fs)); height:calc(28px * var(--fs));}
/* A PNG a reader has uploaded (Aug 2026). It cannot take the gold — that is currentColor on a stroke,
   and a picture has colours of its own — so it is an <img> rather than an inline <svg>, sized to the same
   box and CONTAINED in it, since a reader's file has whatever proportions it has and cropping one would
   silently show them something other than what they chose. */
.coll-ic img{width:calc(34px * var(--fs)); height:calc(34px * var(--fs)); object-fit:contain; display:block; border-radius:4px;}
.cl-row .coll-ic img, .udeck .coll-ic img{width:calc(28px * var(--fs)); height:calc(28px * var(--fs));}
/* …and on a review row, where the width belongs to the deck's name (see adIcon). No min-width: the mark
   sits in the flex line rather than reserving a column, so a row without one loses nothing. */
.active-deck .coll-ic.dk-ic{min-width:0; margin-inline-end:2px;
  /* …AND IT RUNS IN THE DECK'S OWN HUE, not the collection gold (Sep 2026, on request). The row already
     carries that hue as `--coll-bg` (see hueStyle), and the mark is `currentColor` on a stroke, so this is
     one declaration rather than a colour per row. It is MIXED TOWARDS `--ink` rather than used raw: the
     hues are signature washes chosen to sit UNDER a banner, and several of them (Second World War's near
     black, World History's mid brown) are a wash on paper by day and all but invisible on a dark card by
     night. Mixing towards the ink raises the contrast in BOTH directions with no per-theme table, which is
     the ratio `.elab-box`'s tab is solved the same way. The plain hue is declared first for a browser with
     no `color-mix`. */
  color:var(--coll-bg, #C39A2E);
  color:color-mix(in srgb, var(--coll-bg, #C39A2E) 80%, var(--ink));}
body.night .active-deck .coll-ic.dk-ic{color:color-mix(in srgb, var(--coll-bg, #C39A2E) 55%, var(--ink));}
/* High contrast drops the hue and keeps the SHAPE, which is what carries the information: the mark says
   what kind of cards are in the deck, and the colour only repeats the row's own wash. */
body.hc .active-deck .coll-ic.dk-ic{color:var(--ink);}
.active-deck .coll-ic.dk-ic svg, .active-deck .coll-ic.dk-ic img{width:calc(22px * var(--fs)); height:calc(22px * var(--fs));}
/* The Daily-review banner carried a large plain golden numeral of its own — the day's whole pile — and it
   went in Aug 2026 on request, along with pileBadgeMarkup, the tick it showed on a cleared day and the
   sizing rules that dressed it. `.banner` is that one button; the Library's collection banners carry an
   icon rather than a numeral (.coll-ic, a few lines above). */
/* breathing room between the desc line above and the blue "Level X / X of X cards" row */
.banner .xp{margin-top:12px;}
/* the Daily-review banner's xp bar is GOLD, in the same metal as the level numeral beside it — downstairs the
   Library bars run in each collection's own hue and the account's in indigo, and one blue bar looked like
   another. The earned .done / .won fills override this with their own on-fill colour (gold on gold reads as
   nothing), which their higher specificity already handles. */
.banner .xp-fill{background:linear-gradient(90deg, #C39A2E, #E6C765);}
body.night .banner .xp-fill{background:linear-gradient(90deg, #D4AA3C, #F2DC96);}   /* brighter, like the numeral's night gold */
.banner .xp-lvl{color:#8A6A1E;}   /* a deeper gold than the bar: at 10px this text needs the contrast, and #C39A2E on the card is only 3.6:1 */
body.night .banner .xp-lvl{color:#E6C765;}
/* profile: how far through each collection the reader has got */
.coll-levels{display:flex; flex-direction:column; gap:12px; margin-bottom:16px;}
.cl-row{display:flex; align-items:center; gap:16px; background:var(--card); border:1px solid var(--rule); border-radius:12px; padding:12px 18px;}
.cl-main{flex:1; min-width:0; display:flex; flex-direction:column; gap:8px;}
.cl-name{font-family:var(--display); font-weight:600; font-size:calc(16px * var(--fs)); letter-spacing:-.01em;}
.cl-empty{color:var(--ink-faint); font-size:calc(14px * var(--fs)); padding:4px 2px;}

/* ============================================================
   HOME — banners
   ============================================================ */
.banners{display:flex; flex-direction:column; gap:14px;}   /* matches .game-grid's tile gap, so the review banner + tiles read as one grid */
.banner{
  /* LIGHT BLUE is the Daily-study tile's own hue, mixed through --tile exactly like a game tile's colour.
     (It was a bronze, #9A6634, until Aug 2026 and was changed on request.) `.banner` is the review banner
     and nothing else on the site, so this and `.review-group`'s copy — which is what the deck rows under it
     fall back to — are the same colour and must be kept in step; the group sets its own because a value set
     on the banner element itself would otherwise outrank an inherited one.
     The idle wash is declared here on .banner itself rather than on a state selector, so a theme that gives
     the banner a surface of its own (marble's veining, academy's stationery) still wins — the earned .done /
     .won fills below outrank both. */
  --tile:#5AA9DC;
  position:relative; overflow:hidden; cursor:pointer; text-align:left;
  background:linear-gradient(105deg, color-mix(in srgb, var(--tile) 22%, transparent), transparent 64%),
             linear-gradient(150deg, var(--card) 56%, color-mix(in srgb, var(--tile) 13%, var(--card)));
  border:1px solid var(--rule); border-radius:14px;
  padding:26px 28px; box-shadow:var(--shadow-md);
  transition:transform .2s var(--ease), box-shadow .25s var(--ease), border-color .2s;
  display:flex; align-items:center; gap:24px;
}
/* the wash is a whisper on dark themes, as on the game tiles — a daylight-strength mix glares over dark card stock */
body.night .banner{
  background:linear-gradient(105deg, color-mix(in srgb, var(--tile) 11%, transparent), transparent 64%),
             linear-gradient(150deg, var(--card) 56%, color-mix(in srgb, var(--tile) 16%, var(--card)));
}
.banner:hover{transform:translateY(-2px); box-shadow:var(--shadow-lg); border-color:color-mix(in srgb,var(--tile) 34%, var(--rule));}
/* a coloured left bar echoing the minigame tiles */
.banner, .banner:hover{border-left:4px solid var(--tile);}
.banner:active{transform:translateY(0) scale(.995);}
.banner:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px;}
.banner .body{flex:1; min-width:0;}
.banner h2{font-family:var(--display); font-weight:600; font-size:calc(25px * var(--fs)); margin:7px 0 6px; letter-spacing:-.01em; color:var(--ink);}
/* THE TITLE HAS THE WHOLE CARD AGAIN (Sep 2026). It carried a `padding-right` reserving the banner's top
   right — first for the Ordered/Random pill, then for the deck list's Edit control — and BOTH have left
   that corner: the Edit button is at the foot of the list now, on request (see `.rv-foot`). What is still
   in the corner is the completion mark, which is 29px and sits above the title's own line rather than in
   it, so it needs no reservation. The phone keeps its smaller title, which was never about the corner. */
.banner h2.review-title{font-size:calc(39px * var(--fs)); margin:0 0 11px; line-height:1.0;}
@media (max-width:640px){
  .banner h2.review-title{font-size:calc(32px * var(--fs));}
}
.banner .desc{color:var(--ink-soft); font-size:calc(15px * var(--fs)); margin:0; line-height:1.5;}
.banner .meta{display:flex; gap:16px; margin-top:14px; flex-wrap:wrap; align-items:center;}
/* each figure sits CENTRED over its own label rather than left-aligned against it: three columns of two
   lines read as three columns only if both lines share a centre */
.banner .stat{display:flex; flex-direction:column; align-items:center; gap:2px; text-align:center;}
.banner .stat b{font-family:var(--display); font-weight:600; font-size:calc(21px * var(--fs)); line-height:1; color:var(--indigo);}
.banner .stat span{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; color:var(--ink-faint);}
/* (the day's time on cards was a fourth `.stat` here until Aug 2026 — it is `.rv-time` under the deck
   list now, on request; see the note there) */
.banner .cta{margin-left:auto;}   /* sits on the SAME line as the three piles, pushed to the right edge */
/* ---------- the footer row under the DECK LIST ----------
   The day's timer at one end and the deck list's Edit control at the other (Sep 2026, on request: "move
   it to the bottom right just below the active decks list, vertically centered opposite the study
   timer"). The row has held, at various times, "+ New group", the "+ Add decks" lip and the Edit button;
   the first two are gone for good and the third is back, having spent a fortnight in the banner's corner.
   `justify-content:space-between` with `align-items:center` is the whole of "opposite, centred": the
   timer keeps the left, the control keeps the right, and the two share one line box whatever either is.
   The timer's span is emitted EVEN WHEN EMPTY, so the button stays at the right edge on a morning with
   nothing studied yet rather than sliding across to the left. It WRAPS, which is what a phone in the
   editor mode needs: four controls and a sentence do not fit on one 313px line, so the controls drop to
   their own line under the time rather than being squeezed to nothing. */
/* …AND `.rv-foot` IS NOT DRAWN AT ALL SINCE OCT 2026 (on request): the timer is a Today section of the
   This week box (`.home-week`), and the Edit control — which then stood in the collections header for a
   day — is gone with its whole editor mode (Oct 2026, on request). */
/* THE ABSOLUTE CORNER ROW (`.rv-topacts`) STOOD HERE AND IS GONE (Sep 2026, on request). It laid the
   Edit control over the banner's top right, which had to be an overlay rather than markup — the banner is
   a `<button>` and cannot contain one — with `pointer-events:none` on the row and `auto` on its children,
   a height computed from the title's own line box, an inset that dodged the completion mark, and two
   media queries shrinking the three editor buttons so they and the word "Daily" could share a 313px line.
   All of that existed to put a button somewhere a button cannot go. In `.rv-foot` it is an ordinary
   button in an ordinary row and none of it is needed. */
/* THE DECK LIST'S EDITOR MODE (Aug 2026 – Oct 2026) STOOD HERE AND IS GONE, on request ("remove that
   button and its system entirely"): the Edit pill (`.rv-edit`), the four controls it became
   (`.rv-editacts`, Icons / Undo / Revert / Done), the `.rv-editing` state that showed the handles and
   stepped the chevron aside, the red cross (`.dk-del`) and the in-place rename field (`.dk-rename`).
   See the note beside `adOwnTitle` in app.js. */
/* ---------- the handles ----------
   They sat at `.32` on every row at rest until Aug 2026, when the request was that they not be there at
   all until asked for — and the editor mode that then showed them is gone (Oct 2026), so they are hidden
   at rest and nothing shows them. Hidden with `opacity` rather than `display:none` so the column they
   occupy is still reserved — the row's `padding-left` allows for it — and with `visibility`, which is
   what takes them out of hit-testing, or an invisible handle would still swallow the press that should
   be studying the deck. The row's `padding-left` is written as `calc(<depth>px + var(--dk-grip-w))`,
   so the depth arithmetic stays in app.js and the handle's own width stays here, where it is declared. */
.active-decks{--dk-grip-w:8px;}
.active-deck .dk-grip{opacity:0; visibility:hidden;}
/* THE DAY'S TIME ON CARDS, at the left end of the group's own bottom edge (Aug 2026, on request — it was
   a stat in the banner's meta row beside the three piles, and it briefly went up to the banner's corner
   with the Edit button before being asked back the same day). Out here it is on the page's own paper
   rather than on the card, and the row it joins is one small tab high, so it is ONE line rather than the
   piles' figure over a word: the figure in the display face at the caption's own size, the words in the
   mono caption style, and the word LEADING it
   ("studied 13m today", on request) so it reads as a sentence rather than as a labelled statistic. The
   inset and the top padding are what put its line where the row's other items used to sit — measured on
   the rendered page, not derived. */
/* (`.rv-time`, the timer's own rule, is gone with the timer — Oct 2026.) */
/* WHAT THE NEXT ARTEFACT NEEDS (Sep 2026), set exactly as the day's timer beside it: the same mono
   caption, the same baseline, the same quiet ink. It is a small pleasure rather than a target, so it
   is a caption and not a meter — and the FIGURE leads here where the word leads in the timer, because
   what the reader is being told is how many, not what of. On the narrowest phones it gives way to the
   timer, the timer being about work already done. */
.rv-chestnext{
  display:inline-flex; align-items:baseline; gap:6px; flex:0 1 auto; min-width:0;
  margin-inline-start:20px; padding:7px 0 0;
  font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.12em; text-transform:uppercase;
  color:var(--ink-faint); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;
}
.rv-chestnext span{font-family:var(--display); font-weight:600; font-size:calc(13px * var(--fs)); line-height:1; letter-spacing:0; text-transform:none; color:var(--ink-soft);}
@media (max-width:430px){ .rv-chestnext{display:none;} }
/* (the "Short on time?" chips under the deck list lasted a day — Oct 2026, removed on request) */
/* a first visitor's way to try ten cards without choosing anything (Oct 2026). */
.hero-sample{margin:12px 2px 0; font-size:calc(14px * var(--fs)); color:var(--ink-soft);}
.hero-sample button{font:inherit; color:var(--indigo); background:none; border:0; padding:0; cursor:pointer; text-decoration:underline; text-underline-offset:3px;}
@media (max-width:640px){ .hero-sample{text-align:center;} }
.banner .glyph{
  position:absolute; right:-10px; bottom:-26px; font-family:var(--han); font-weight:700;
  font-size:calc(150px * var(--fs)); line-height:1; color:var(--indigo); opacity:.05; pointer-events:none; user-select:none;
}
.banner .glyph-svg{bottom:-22px; right:-14px;}
.banner .glyph-svg svg{display:block; width:168px; height:168px;}

/* ---------- completion is a MARK IN THE CORNER, not a filled tile (Aug 2026, on request) ----------
   The Daily-review banner and every game tile used to FILL with their colour once played, and turn gold on a
   perfect score. That is a lot of surface to change for one fact, and it fought every theme's own treatment
   of the card. What marks completion now is a small mark in the TOP-RIGHT corner — the tile underneath is
   left exactly as it was, so a played page still reads as a page of tiles rather than a page of blocks of
   colour. `.done` / `.won` stay on the element (the mark is a child, and the classes are what the tests and
   the achievements read); all they do now is carry the mark.
   TWO SHAPES, ONE ANCHOR (Aug 2026, on a second request). Having played is a green circled check; a perfect
   score is a gold WAX SEAL with the same check impressed in it, in the same corner and the same size class,
   where it used to be a diagonal ribbon reading "Perfect!" across the whole corner. The two answer one
   question, so they belong in one place and differ in the thing that differs — green circle against gold
   wax — rather than in shape, position and surface all at once.
   Both are real elements with an aria-label rather than pseudo-elements: a patch of colour with no name is
   invisible to a screen reader whatever shape it is. */
.gt-check{
  position:absolute; top:9px; right:9px; z-index:3; pointer-events:none;
  width:21px; height:21px; border-radius:50%;
  background:var(--good, #4F9D67); color:#fff;
  display:inline-flex; align-items:center; justify-content:center;
  box-shadow:0 1px 4px rgba(0,0,0,.24);
}
.gt-check svg{width:12px; height:12px; display:block;}
/* the review banner is a bigger surface, so its mark is too — the ribbon's own pair of sizes, one line up */
.review-group .banner .gt-check{top:12px; right:12px; width:25px; height:25px;}
.review-group .banner .gt-check svg{width:14px; height:14px;}
body[data-theme="arcade"] .gt-check{border-radius:0; box-shadow:none; border:2px solid var(--ink);}
body[data-theme="gazette"] .gt-check{box-shadow:none;}
/* A PERFECT SCORE IS A GOLD WAX SEAL (Aug 2026, on request), stamped in the corner the check sits in.
   THE SHAPE IS A CLIP-PATH AND NOT A BORDER-RADIUS, because what makes a wax seal read as one at 24px is
   the FLUTED RIM — the scalloped edge a seal press leaves — and a rounded rectangle cannot have one at any
   radius. The polygon is 22 points alternating between r=50% and r=43% of the box, which is one flute per
   32° and the coarsest scallop that still reads as a scallop rather than as a cog.
   The DROP SHADOW is a `filter`, not a `box-shadow`: a clip-path clips the box shadow away with the box,
   and a filter follows the clipped silhouette, which is what makes the wax look pressed onto the tile
   rather than cut out of it.
   The GLINT is the ribbon's own `gt-gold-shine` sweep, unchanged — the same gold and the same 3.4s rest
   between passes, so a seal and a gold XP bar are the same material.
   The CHECK is `.gt-seal-face`, a child rather than the seal itself, because the sweep lives on the seal's
   own background and a check drawn on that element would be swept with it. It is set in the deep gold the
   ribbon's word used, with a soft white shadow under it so it reads as impressed into the wax. */
.gt-seal{
  position:absolute; top:8px; right:8px; z-index:3; pointer-events:none;
  width:24px; height:24px;
  display:inline-flex; align-items:center; justify-content:center;
  clip-path:polygon(50.0% 0.0%, 62.1% 8.7%, 77.0% 7.9%, 82.5% 21.8%, 95.5% 29.2%, 92.6% 43.9%, 99.5% 57.1%, 89.1% 67.9%, 87.8% 82.7%, 73.2% 86.2%, 64.1% 98.0%, 50.0% 93.0%, 35.9% 98.0%, 26.8% 86.2%, 12.2% 82.7%, 10.9% 67.9%, 0.5% 57.1%, 7.4% 43.9%, 4.5% 29.2%, 17.5% 21.8%, 23.0% 7.9%, 37.9% 8.7%);
  filter:drop-shadow(0 1px 3px rgba(0,0,0,.34));
  background-image:
    linear-gradient(115deg, rgba(255,255,255,0) 40%, rgba(255,255,255,.75) 50%, rgba(255,255,255,0) 60%),
    radial-gradient(circle at 34% 28%, #F7ECBE 0%, #E5C765 46%, #BE9829 100%);
  background-size:250% 100%, 100% 100%;
  background-repeat:no-repeat;
  background-position:110% 0, 0 0;
  animation:gt-gold-shine 3.4s ease-in-out infinite;
}
.gt-seal-face{
  display:block; color:#4A3708;
  filter:drop-shadow(0 1px 0 rgba(255,255,255,.45));
}
.gt-seal-face svg{width:13px; height:13px; display:block;}
@media (prefers-reduced-motion:reduce){ .gt-seal{animation:none;} }
/* the review banner is a bigger surface, so its seal is too — the check's own pair of sizes */
.review-group .banner{position:relative;}
.review-group .banner .gt-seal{top:11px; right:11px; width:29px; height:29px;}
.review-group .banner .gt-seal-face svg{width:16px; height:16px;}
/* arcade squares every mark off and outlines it in ink; the gazette prints flat, so the wax loses its lift */
body[data-theme="arcade"] .gt-seal{clip-path:none; border:2px solid var(--ink); filter:none;}
body[data-theme="gazette"] .gt-seal{filter:none;}
/* ---------- A PLAYED TILE WEARS ITS STATE (Sep 2026, on request: "implement design 8, but make the
   colour of the card's background gradient + left bar gold or green accordingly") ----------
   Design 8 of eight rendered alternatives, chosen from images: NO BAND AND NO BADGE — one very large
   tick washed across the tile behind the words, and the tile's own hue swapped for the state's.

   WHAT CAME BEFORE IT, and why the request kept coming back. The tile FILLED with its colour when
   played, which was a lot of surface for one fact; then it was a small mark in the corner; then, on a
   phone, a green or gold BAND across the top 60% with a big white tick standing on it — which is the one
   that was rejected twice ("I still don't like how the gold/green completed minigames tiles look"). Each
   of those answered "which state is this tile in" by painting a NEW shape onto the tile. This answers it
   by re-tinting the shape the tile already has: the wash from the left, the left bar, the hover border
   and the title all read a single custom property, so a played tile is the same tile in a different
   colour rather than a tile wearing a badge.

   `--gt-accent` IS WHY THIS IS SMALL. The tile's own hue arrives inline as `--tile`, and an inline
   declaration beats any selector without `!important` — so `.done` cannot simply re-set `--tile`. It sets
   a second property that every treatment reads instead, and which merely DEFAULTS to `--tile`. One line
   per state, and nothing that derives from it had to be found and changed.

   THE TICK IS A MASK, NOT A GLYPH: the same 24-box polyline the corner marks draw, painted as a
   background through `-webkit-mask`/`mask`, so it takes the accent colour with everything else and needs
   no second copy of the SVG in the stylesheet. It sits at z-index 0 with `.gt-body` lifted above it — a
   watermark the words are read OVER, which is what makes it able to be this large.

   THE GLYPH GOES, as it did under the band and for the same reason: the corner ornament names a game
   whose name is written underneath it in words, so on a played tile it is a second mark competing with
   the tick for the same corner and the same colour. `display:none` rather than `opacity:0` — it is
   decoration, and a screen reader gains nothing by keeping it in the tree.

   THE CORNER MARKS ARE CLIPPED, NEVER `display:none`. `.gt-check` and `.gt-seal` are the only thing on
   the tile that STATES the completion in words (`role="img"` + aria-label), so they stay in the
   accessibility tree at 1px — the same trade the folded grade bar makes with its labels. Removing them
   would leave a screen reader with a tile that says nothing about having been played. Scoped to
   `.game-tile`, since the review banner draws the same two marks and still wants them visible. */
.game-tile.done, .game-tile.won{ --gt-accent:var(--good, #4F9D67); }
.game-tile.won{ --gt-accent:var(--gt-gold); }
.game-tile{ --gt-gold:#B8892A; }
body.night .game-tile{ --gt-gold:#D8B355; }
/* WHERE THE TICK SITS FOLLOWS THE TILE'S SHAPE, and that is the one thing here that needed a
   breakpoint. A phone tile is nearly square with the words filling its foot, so the tick is put at the
   bottom-left and the title is read over it — which is the arrangement that was chosen from the eight.
   A tile above the breakpoint is a wide letterbox whose right half is empty (it is where the corner
   ornament used to be), and there the same placement lays the tick straight through the title for no
   reason. So the base rule is the desktop's and the phone keeps its own, in the ≤640px block below. */
.game-tile.done .gt-face::after, .game-tile.won .gt-face::after{
  content:""; position:absolute; z-index:0; pointer-events:none;
  right:-6px; top:50%; transform:translateY(-50%); width:min(44%, 112px); aspect-ratio:1;
  background:var(--gt-accent); opacity:.20;
  -webkit-mask:var(--gt-tick) no-repeat center/contain;
  mask:var(--gt-tick) no-repeat center/contain;
}
.game-tile.won .gt-face::after{ opacity:.30; }
@media (max-width:640px){
  .game-tile.done .gt-face::after, .game-tile.won .gt-face::after{
    right:auto; top:auto; transform:none;
    left:-8px; bottom:-12px; width:min(74%, 104px);
  }
}
.game-tile{ --gt-tick:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>"); }
/* the words are read OVER the watermark — `.gt-body` is positioned already, but its auto z-index would
   put it UNDER a later positioned sibling, which the ::after is */
.game-tile.done .gt-body, .game-tile.won .gt-body{ z-index:2; }
.game-tile.done .gt-glyph, .game-tile.won .gt-glyph{ display:none; }
.game-tile .gt-check, .game-tile .gt-seal{
  width:1px; height:1px; padding:0; border:0; overflow:hidden;
  clip-path:inset(50%); background:none; box-shadow:none; animation:none; filter:none;
}
/* daily challenge accent treatment */
.banner.challenge{background:linear-gradient(135deg, var(--card) 60%, var(--zh-wash));}
.banner.challenge h2{color:var(--ink);}
.banner.challenge .stat b{color:var(--zh);}
.banner.challenge .glyph{color:var(--zh); opacity:.06;}
.live-dot{display:inline-block; width:7px; height:7px; border-radius:99px; background:var(--zh); margin-right:7px; box-shadow:0 0 0 0 rgba(200,69,60,.5); animation:pulse 2s infinite;}
@keyframes pulse{0%{box-shadow:0 0 0 0 rgba(200,69,60,.5);}70%{box-shadow:0 0 0 7px rgba(200,69,60,0);}100%{box-shadow:0 0 0 0 rgba(200,69,60,0);}}

/* ---------- home: first-run hero ---------- */
.banner.hero{padding:34px 32px;}
.hero-eyebrow{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.2em; text-transform:uppercase; color:var(--indigo); display:block; margin-bottom:10px;}
.banner.hero .review-title{font-size:calc(34px * var(--fs)); letter-spacing:-.015em;}
.banner.hero .desc{font-size:calc(16px * var(--fs)); max-width:56ch;}
/* `.hero-alt` — the quiet "or browse the collections" link beside the hero's button — is GONE (Aug 2026,
   on request), with the markup that carried it. The hero offers ONE way in; the collections are reached
   from the lip under the review group, which is the route the home page advertises. */

/* ---------- home: the way to the collections ----------
   THREE SHAPES, and the third is on request (Aug 2026). It was a full-width banner of its own — a second
   block spending a heading and a sentence on what one word says — then a small "+ Add decks" tab hanging
   off the review group's bottom edge (`.rv-lip`), and is now an UNATTACHED button standing under the
   group. The tab said two things at once and only one was true: stuck to the block, "+ Add" reads as an
   action performed on it, where pressing it goes to a page. Detached, it is named for where it leads.
   It ships at EVERY width, the Collections tab having left the desktop's top bar too: this is the ONLY
   route to the collections anywhere on the site.
   It keeps the lip's INDIGO and its mono capitals — the blue is the site's own primary-button indigo, so
   it matches Start directly above it — and gives up the tab geometry (the negative pull, the flat top, the
   right-hand inset), all of which existed to join it to an edge it no longer touches. It is a block in the
   `.banners` flex column, so the column's own gap is what separates it. */
.home-collections{
  appearance:none; cursor:pointer; align-self:center; position:relative;
  display:inline-flex; align-items:center; justify-content:center; padding:11px 26px;
  border:1px solid var(--indigo); border-radius:12px;
  background:var(--indigo); box-shadow:0 3px 7px rgba(0,0,0,.07);
  font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:#fff;
  transition:background .16s var(--ease), border-color .16s var(--ease);
}
.home-collections:hover{background:var(--indigo-bright); border-color:var(--indigo-bright);}
.home-collections:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px; z-index:1;}
/* ---------- …AND IT CLOSES THE DAILY-STUDY CARD (Aug 2026, on request) ----------
   It stood free under the group and ended up between the last deck row and the Minigames heading with a
   gap either side and nothing to belong to — which on a phone with several decks is the middle of the
   page. Inside the group it is the last row of the stack the banner opens: full width, square at the top
   where it meets the row above, and carrying the card's own bottom corners.
   The row above therefore gives those corners UP — `.dk-last` and `:last-child` both round the last
   visible deck row, and with a row beneath them they would round into it. The rule is written on the
   GROUP (`.review-group .active-deck…`) rather than as a `:has()` on the row, since the button is
   unconditional here and a state selector would be describing a state that never varies.
   With NO decks (a first run) the banner keeps its own rounded foot and the button stands off it
   instead: `#b-review + .home-collections`. */
.review-group .home-collections{
  align-self:stretch; width:100%; margin-top:0;
  border-radius:0 0 14px 14px; border-top-color:color-mix(in srgb, #000 12%, var(--indigo));
}
.review-group .active-deck:last-child,
.review-group .active-deck.dk-last{border-bottom-left-radius:0; border-bottom-right-radius:0;}
.review-group > #b-review + .home-collections{margin-top:10px; border-radius:12px; border-top-color:var(--indigo);}
/* ---------- home: the way to the About page on a phone ----------
   About left the bottom tab bar (Aug 2026, on request) and this took its place: a quiet grey line at the
   foot of the page rather than a fifth of a row four readers share. Every width, like the button above. */
.home-about{
  appearance:none; border:0; background:none; cursor:pointer; width:100%;
  padding:20px 0 16px; text-align:center;
  font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-quiet);
}
/* …and on a desktop it stands further off the game grid above it (Aug 2026, on request). The padding is a
   PHONE figure — at 390px the page ends a thumb's width below the last tile and more air there is just
   scrolling — where a wide window has room to let the last line of the page be plainly the end of it. It is
   `padding-top` rather than a margin so it stays part of the button's own target. */
@media (min-width:641px){ .home-about{padding-top:48px;} }
.home-about:hover{color:var(--ink-soft);}
.home-about:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px; border-radius:6px;}

/* ---------- home: the Minigames section ----------
   The games are the last thing on the home page now — the discovery row that used to follow them is not
   built at any width — so they get a heading rather than arriving as an unannounced grid of six coloured
   squares. Phone-only for a fortnight, then at every width, on request, with the row's removal.
   NOT `.mg-*`: that prefix is the MAP GAME's (`.mg-card` / `.mg-head` / `.mg-score`, further down), and
   naming this heading `.mg-head` silently gave it that card's `display:flex` — which beats `text-align`
   outright, so the heading rendered left — while pushing this font and colour onto the game's score row. */
.games-sec{display:flex; flex-direction:column; gap:10px;}
/* CENTRED (Aug 2026, on request — it was centred, then moved left for a fortnight, and is centred again).
   It names the block of six tiles beneath it rather than starting a column of prose, and the tiles are a
   centred grid; the left edge it used to share is the edge of the review's text above, which is a different
   thing entirely. `text-align` is enough because the h2 is a block spanning the column — do NOT reach for
   `display:flex` here, and do NOT rename the class to `.mg-head`: that is the MAP GAME's card header
   further down the file, whose `display:flex` beats text-align outright and once left this heading
   rendering hard left with a computed text-align of "center". */
.games-head{
  margin:6px 0 0; text-align:center;
  font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.16em; text-transform:uppercase;
  font-weight:600; color:var(--ink-faint);
}

/* ---------- home: the day's nine, and the chest they earn (Sep 2026, on request) ----------
   A miniature of the grid below it — three rows of three small rounded bars in DAILY_GAMES order, so the
   lit bar in the middle of the top row IS the tile in the middle of the top row — the count in figures,
   and the chest that finishing all nine unlocks. See sweepRowHTML in app.js.
   THE GAP IS 3px AND THAT IS THE REQUEST ("but closer together"): at 4px and up the nine bars read as
   nine separate marks, and what they have to read as is one meter with nine segments. The bar itself is
   sized off --fs like everything else, so the block grows with the reader's text size rather than
   staying a fixed speck beside larger type. */
.sweep-row{
  display:flex; align-items:center; justify-content:center; gap:14px;
  margin:2px 0 2px;
}
.sweep-meter{display:flex; align-items:center; gap:9px;}
.sweep-chips{
  display:grid; grid-template-columns:repeat(3, auto); gap:3px;
}
.sw-chip{
  display:block; width:calc(15px * var(--fs)); height:calc(4.5px * var(--fs)); border-radius:99px;
  background:var(--rule); transition:background .25s var(--ease), box-shadow .25s var(--ease);
}
.sw-chip.on{background:var(--good, #4F9D67); box-shadow:0 0 0 1px color-mix(in srgb, var(--good, #4F9D67) 35%, transparent);}
/* A PERFECT RUN'S CHIP IS GOLD, the same pair of stops the tile's wax seal is mixed from (`.gt-seal`),
   so "perfect" is one colour on this page rather than two. It is written AFTER `.on` and at the same
   specificity, so the later rule wins and a won chip needs no extra weight. */
.sw-chip.won{background-image:linear-gradient(100deg, #E5C765 0%, #BE9829 100%); background-color:#D2AE47;
  box-shadow:0 0 0 1px color-mix(in srgb, #BE9829 45%, transparent);}
.sweep-count{
  font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-faint); letter-spacing:.04em;
}
.sweep-count b{color:var(--ink); font-weight:600;}
/* The chest is a real button in every state (never a div promoted later), so `disabled` is what says
   "locked" to the keyboard and the screen reader as well as to the eye. */
.sweep-chest{
  position:relative; display:inline-flex; align-items:center; justify-content:center;
  width:calc(38px * var(--fs)); height:calc(32px * var(--fs)); padding:0;
  background:none; border:0; cursor:default; color:var(--ink-faint); opacity:.45;
  transition:opacity .25s var(--ease), color .25s var(--ease), transform .18s var(--ease);
}
.sweep-chest .chest-svg{width:100%; height:100%;}
.sweep-chest .lock-svg{
  position:absolute; right:calc(-2px * var(--fs)); bottom:calc(-1px * var(--fs));
  width:calc(13px * var(--fs)); height:calc(13px * var(--fs));
  background:var(--paper); border-radius:50%; padding:1px;
}
.sweep-chest.ready{
  opacity:1; cursor:pointer; color:var(--gold, #C08A2E);
  animation:sweepChestWait 2.4s var(--ease) infinite;
}
.sweep-chest.ready:hover{transform:translateY(-1px) scale(1.05);}
.sweep-chest.claimed{opacity:.62; color:var(--good, #4F9D67);}
@keyframes sweepChestWait{
  0%, 68%, 100%{transform:none;}
  76%{transform:translateY(-2px) rotate(-3deg);}
  86%{transform:translateY(-1px) rotate(3deg);}
}

/* ---------- home: how-it-works strip — REMOVED (Aug 2026, on request) ----------
   `.howit` / `.hi-step` / `.hi-num` / `.hi-body` were the three-beat first-run strip under the review
   banner: study a card, grade yourself, it comes back. The walkthrough offered directly above it now
   says all three properly, with the forgetting curve and a real card, so the strip was a first visit
   spending its attention twice. Its markup went with these rules — see PAGES.home. */

/* ---------- home: the walkthrough offer (until it is answered, or the first card is graded) ---------- */
/* It sits with the first-run hero, and for the same reason: a reader who has already studied knows what
   the site is. Either button retires it — see tourOfferHTML in app.js. */
.tour-offer{
  display:flex; align-items:center; gap:15px; flex-wrap:wrap;
  background:var(--card); border:1px solid var(--rule); border-radius:12px; padding:14px 17px;
  box-shadow:var(--shadow-sm);
  border-left:3px solid var(--indigo);
}
.to-chip{flex:none; width:34px; height:34px; border-radius:99px; display:grid; place-items:center; background:var(--indigo-wash); color:var(--indigo);}
.to-chip svg{width:18px; height:18px;}
.to-body{flex:1 1 240px; min-width:0; display:flex; flex-direction:column; gap:2px;}
.to-body b{font-family:var(--display); font-weight:600; font-size:calc(14.5px * var(--fs)); color:var(--ink);}
.to-body span{font-size:calc(12.5px * var(--fs)); line-height:1.45; color:var(--ink-soft);}
.to-acts{flex:none; display:flex; align-items:center; gap:12px;}
/* --ink-soft, not the --ink-faint the site's captions use: this is one of only two CONTROLS on a
   first-time reader's first screen, and at 12px the faint token reads 3.54:1 (measured by test-a11y).
   A caption may be quiet; the button that declines the tutorial may not be. */
.to-no{appearance:none; border:0; background:transparent; cursor:pointer; padding:4px 2px; color:var(--ink-soft); font-family:inherit; font-size:calc(12px * var(--fs)); text-decoration:underline; text-underline-offset:3px;}
.to-no:hover{color:var(--ink);}

/* ---------- the guided walkthrough (see THE GUIDED TOUR in app.js) ---------- */
/* z-index: above the gloss scrim (9590) so a popup left open cannot sit over the lesson, and below the
   fullscreen media viewer (9800), which nothing here opens. The scrim is DARK rather than a paper wash —
   the Atlas's coach marks lighten their globe, and this one has to darken a page of prose in eight themes,
   so it is deliberately theme-independent. */
/* NO `backdrop-filter` HERE, and that is the point rather than an omission (Sep 2026, on a bug report:
   on mobile the tutorial "blurs the background while also trying to highlight elements from it"). The
   scrim covers the whole page, targets included, so a blur on it blurs the very element the dashed ring
   is drawn around — the step says look at this and then makes it unreadable. It bites hardest on a phone,
   where the card docks to the bottom and the ringed element is the only other thing on screen, but the
   contradiction is not a phone one and neither is the fix. The 55% black wash does the separating on its
   own; `.page-help` KEEPS its blur, because a coach mark rings nothing. */
.folio-tour{position:fixed; inset:0; z-index:9700; display:flex; align-items:center; justify-content:center; padding:20px;
  background:color-mix(in srgb, #000 55%, transparent);
  animation:tourIn .22s var(--ease) both;}
@keyframes tourIn{from{opacity:0}to{opacity:1}}
/* the arrow layer: no viewBox, so its user units are CSS pixels and the rects tourPlace() measures need no
   conversion. pointer-events:none — it is drawn over the card as well as beside it. */
.tour-draw{position:absolute; inset:0; width:100%; height:100%; pointer-events:none; overflow:visible;}
.tour-ring{stroke:var(--ochre); stroke-width:2.5; opacity:.95; stroke-dasharray:7 6;}
.tour-line{stroke:var(--ochre); stroke-width:2.5; stroke-linecap:round;}
.tour-head{stroke:var(--ochre); stroke-width:2.5; stroke-linecap:round; stroke-linejoin:round;}

.tour-card{position:relative; z-index:1; width:min(460px, 100%); max-height:calc(100vh - 40px); overflow-y:auto;
  background:var(--card); border:1px solid var(--rule); border-radius:15px; padding:20px 24px 18px;
  box-shadow:0 22px 54px -18px rgba(0,0,0,.65); display:flex; flex-direction:column; gap:11px;
  /* the nudge that keeps the card off its own target (tourPlace) — eased, so the popup is followed rather
     than found again on each step. The global reduced-motion killswitch covers it. */
  transition:transform .28s var(--ease);}
.tour-card:focus{outline:none;}
.tour-count{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; color:var(--ink-faint);}
.tour-card h3{font-family:var(--display); font-weight:600; font-size:calc(20px * var(--fs)); letter-spacing:-.01em; margin:0 22px 0 0;}
.tour-body{font-size:calc(13.5px * var(--fs)); line-height:1.55; color:var(--ink-soft);}
.tour-body p{margin:0 0 9px;}
.tour-body p:last-child{margin-bottom:0;}
.tour-body b{color:var(--ink); font-weight:600;}
/* The word naming a colour is PAINTED in that colour, so the step and the page agree. --newterm is what
   an undiscovered glossary term wears (see .ttip[data-new]); it was --ochre until Aug 2026, when the
   Library's note markers gave the teal up and the two roles were swapped, and this said "gold" for a
   fortnight after the mark had stopped being gold. Read the token, never a remembered hue. */
.tour-body b.tour-newterm{color:var(--newterm);}
.tour-x{position:absolute; top:10px; right:11px; appearance:none; border:0; background:transparent; color:var(--ink-faint); font-size:calc(17px * var(--fs)); width:26px; height:26px; border-radius:7px; cursor:pointer; display:flex; align-items:center; justify-content:center;}
.tour-x:hover{background:var(--rule); color:var(--ink);}
.tour-nav{display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:3px;}
.tour-nav-main{display:flex; align-items:center; gap:9px;}
.tour-skip{appearance:none; border:0; background:transparent; cursor:pointer; padding:4px 2px; color:var(--ink-soft); font-family:inherit; font-size:calc(12px * var(--fs)); text-decoration:underline; text-underline-offset:3px;}
.tour-skip:hover{color:var(--ink);}
.tour-back[disabled]{opacity:.4; cursor:default;}
.tour-next[hidden],.tour-back[hidden]{display:none;}
/* THE HIGHLIGHTED THING IS NOT DARKENED (Sep 2026, on request). With a target the overlay's own wash is
   switched off and the HOLE casts it instead, as a box-shadow reaching past every edge of the screen, so
   the target sits in the one clear window. It is under the card and never takes the pointer. */
.folio-tour.has-hole{background:transparent;}
.folio-tour[hidden]{display:none;}
.tour-hole{position:absolute; z-index:0; border-radius:12px; pointer-events:none;
  box-shadow:0 0 0 200vmax color-mix(in srgb, #000 55%, transparent);
  transition:left .22s var(--ease), top .22s var(--ease), width .22s var(--ease), height .22s var(--ease);}
.tour-hole[hidden]{display:none;}
.tour-draw{z-index:0;}
/* A LIVE step uses the page: the scrim stops catching the pointer, and only the card does. On a
   `live: "hole"` step the four blocks catch it everywhere but the target. */
.folio-tour.tour-live{pointer-events:none;}
.folio-tour.tour-live .tour-card{pointer-events:auto;}
.folio-tour.tour-live:not(.has-hole){background:transparent;}
.tour-blocks[hidden]{display:none;}
.tour-blocks i{position:absolute; pointer-events:auto; background:transparent;}
/* …and the study page's steps sit top right, off the study card. A phone docks them to the foot as ever,
   lifted above the grade bar when the step is about it (--tour-above, measured in tourPaint). */
.folio-tour.tour-corner{align-items:flex-start; justify-content:flex-end; padding-top:calc(var(--bar-h) + 14px);}
.folio-tour.tour-corner .tour-card{width:min(340px, 100%);}
/* the pile words painted as their counts are on the page (.dkc-new / .dkc-learn / .dkc-rev) */
.tour-body b.tour-pile-new{color:var(--indigo-bright);}
.tour-body b.tour-pile-learn{color:var(--zh);}
.tour-body b.tour-pile-rev{color:var(--good, #4F9D67);}
.tour-demo .msn-curve{margin:0; padding:12px 10px 10px;}
.tour-demo .msn-curve figcaption{font-size:calc(9px * var(--fs)); margin-top:7px;}
.td-rar{display:grid; grid-template-columns:repeat(2, minmax(0,1fr)); gap:8px;}
.td-rar-row{display:flex; align-items:center; justify-content:space-between; gap:8px; padding:8px 10px; border-radius:9px;
  background:color-mix(in srgb, var(--rar, var(--rule)) 10%, var(--paper-2)); border:1px solid color-mix(in srgb, var(--rar, var(--rule)) 35%, var(--rule));}
.td-rar-row i{font-style:normal; font-family:var(--mono); font-size:calc(10px * var(--fs)); color:var(--ink-faint);}

/* the illustrations. A study card is never dealt for real during the walkthrough — the reader's schedule is
   not a demonstration surface, and the grade bar is pinned to the bottom of the viewport under the scrim —
   so the card, its blank and the four grades are drawn here instead. */
.tour-demo[hidden]{display:none;}
.td-card{background:var(--paper-2); border:1px solid var(--rule); border-radius:11px; padding:14px 16px; display:flex; flex-direction:column; gap:11px; align-items:flex-start;}
.td-q{font-family:var(--display); font-size:calc(15px * var(--fs)); line-height:1.45; color:var(--ink);}
.td-blank{display:inline-block; width:3.4em; vertical-align:baseline; border-bottom:2px solid var(--ochre); margin:0 3px;}
.td-btn{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:#fff; background:var(--indigo); border-radius:8px; padding:7px 14px;}
.td-grades{display:grid; grid-template-columns:repeat(4,1fr); gap:7px;}
.td-g{border-radius:9px; padding:8px 4px; display:flex; flex-direction:column; align-items:center; gap:2px; color:#fff;}
.td-g b{font-family:var(--display); font-weight:600; font-size:calc(12.5px * var(--fs));}
.td-g i{font-family:var(--mono); font-style:normal; font-size:calc(9.5px * var(--fs)); opacity:.85;}
.td-g.again{background:#D9544C;} .td-g.hard{background:#DB8B3A;} .td-g.good{background:#4F9D67;} .td-g.easy{background:#4F74C2;}
.night .td-g.again{background:#E26B62;} .night .td-g.hard{background:#E0A057;} .night .td-g.good{background:#5FB07E;} .night .td-g.easy{background:#6E8FD6;}
.td-marker{display:flex; align-items:center; gap:14px; background:var(--paper-2); border:1px solid var(--rule); border-radius:11px; padding:13px 16px;}
.td-pen{flex:none; width:42px; height:42px; border-radius:99px; display:grid; place-items:center; background:var(--indigo); color:#fff; box-shadow:var(--shadow-sm);}
.td-pen svg{width:20px; height:20px;}
.td-scribble{flex:1 1 auto; height:34px; color:var(--ochre);}

/* THE REVEAL BUTTON WHILE THE CARD'S BACK IS STILL COMING (Sep 2026 — see CARD_EXTRA_FIELDS).
   A card's heavy half is fetched per collection, and in the ordinary case it is already in before the
   reader has read the question. When it is not — a cold deck on a slow link — the button says so
   rather than sitting dead: the label is replaced by a pulsing ellipsis, and `aria-busy` says the
   same thing to a screen reader. The pulse is a plain animation, so the global reduced-motion
   killswitch lands it on its resting state with no rule of its own. */
#reveal-btn.waiting{position:relative; color:transparent; pointer-events:none;}
#reveal-btn.waiting::after{
  content:"\2026"; position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  color:var(--paper); font-size:calc(19px * var(--fs)); letter-spacing:.12em;
  animation:revealWait 1.1s ease-in-out infinite;
}
@keyframes revealWait{0%,100%{opacity:.45;}50%{opacity:1;}}

/* THE QUIET FORM OF A FIRST-VISIT CARD (Sep 2026 — see pageHelp in app.js). A strip at the top of the
   page rather than a modal over it: the heading, the first tip, and a control that unfolds the rest in
   place. It is in the page's own flow, so it scrolls away and dies with the page. Used by the Library
   and the Atlas; the marker keeps the modal, being the one genuinely undiscoverable control. */
.page-help-quiet{
  display:flex; align-items:flex-start; gap:12px;
  margin:0 0 18px; padding:13px 14px;
  border:1px solid var(--rule); border-left:3px solid var(--indigo); border-radius:8px;
  background:var(--card);
}
.page-help-quiet .phq-main{flex:1 1 auto; min-width:0;}
.page-help-quiet .phq-label{
  display:block; font-family:var(--mono); font-size:calc(10px * var(--fs)); font-weight:600;
  letter-spacing:.14em; text-transform:uppercase; color:var(--indigo); margin-bottom:5px;
}
.page-help-quiet .phq-first,
.page-help-quiet .ah-tip{font-size:calc(14px * var(--fs)); line-height:1.5; color:var(--ink-soft);}
.page-help-quiet .phq-rest{margin-top:9px; display:flex; flex-direction:column; gap:8px;}
.page-help-quiet .phq-rest[hidden]{display:none;}
.page-help-quiet .phq-more{
  appearance:none; border:0; background:none; cursor:pointer; padding:7px 0 0;
  font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase;
  font-weight:600; color:var(--indigo); text-decoration:underline; text-underline-offset:3px;
}
.page-help-quiet .phq-close{
  appearance:none; border:0; background:none; cursor:pointer; flex:none;
  width:26px; height:26px; border-radius:6px; font-size:calc(17px * var(--fs)); line-height:1;
  color:var(--ink-quiet);
}
.page-help-quiet .phq-close:hover{color:var(--ink); background:var(--paper-2);}
.page-help-quiet .phq-more:focus-visible,
.page-help-quiet .phq-close:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}

/* …and the Atlas's own card in that same form. It is a static element inside the globe stage, so the
   compact state is a class rather than a second element: the full-bleed dimmer becomes a strip pinned
   to the top of the stage, and the globe stays visible and draggable underneath it. */
.atlas-help.ah-compact{
  inset:auto 0 auto 0; display:block; background:none; backdrop-filter:none; -webkit-backdrop-filter:none;
  /* the top padding clears .atlas-tabs, which is fixed at bar-h + 12px and is the same 34px-tall,
     centred row this strip would otherwise be drawn straight on top of */
  padding:58px 12px 10px; pointer-events:none;
}
.atlas-help.ah-compact .ah-card{
  pointer-events:auto; max-width:min(560px, calc(100% - 24px)); margin:0 auto;
  max-height:none; padding:13px 40px 13px 16px; text-align:left;
}
.atlas-help.ah-compact .ah-card h3{font-size:calc(14px * var(--fs)); margin:0 0 6px;}
/* everything past the first tip, and the go button, fold away — the "?" reopens the full card.
   The button is `#ahGo` and carries only `.btn`, so it is named by id: a `.ah-go` selector (the class
   the pageHelp modal uses) matches nothing here and left the Explore button sitting under one tip. */
.atlas-help.ah-compact .ah-tip:not(:first-of-type){display:none;}
.atlas-help.ah-compact #ahGo{display:none;}
.atlas-help.ah-compact .ah-tip{font-size:calc(13px * var(--fs));}

/* ---------- a page's own first-visit coach marks (the Library; the Atlas has .atlas-help) ---------- */
/* The same card as the Atlas's, in a wrapper fixed to the VIEWPORT rather than absolute inside a
   full-bleed stage — an ordinary page has no such stage to sit inside. */
.page-help{position:fixed; inset:0; z-index:9700; display:flex; align-items:center; justify-content:center; padding:20px;
  background:color-mix(in srgb, #000 48%, transparent); backdrop-filter:blur(2px); -webkit-backdrop-filter:blur(2px);}
.page-help[hidden]{display:none;}
.page-help .ah-card{max-height:calc(100vh - 40px); overflow-y:auto;}
.ah-go{align-self:flex-start; margin-top:3px;}
.lib-help-btn{flex:none;}

/* The discovery row — .explore-grid with its card of the day, gloss of the day and Atlas teaser — was
   REMOVED in Aug 2026 on request, from the phone first and then from the desktop, so that the two home
   pages read the same. Its ~90 rules (.exp-tile / .exp-card / .cod-* / .term-* / .atlas-* / .mini-globe)
   went with it: PAGES.home no longer BUILDS any of it at any width, so nothing they described can exist.
   The tile-press transform they shared with .game-tile is the one line kept, up in the BUTTONS block. */

/* ============================================================
   DECKS
   ============================================================ */
.collection-list{display:flex; flex-direction:column; gap:16px;}
.collection{background:var(--card); border:1px solid var(--rule); border-radius:14px; box-shadow:var(--shadow-md); overflow:hidden; transition:box-shadow .25s var(--ease);}
.collection.open{box-shadow:var(--shadow-lg);}
.collection-row{
  display:flex; align-items:center; gap:20px; padding:22px 24px; cursor:pointer;
  transition:background .18s var(--ease);
}
.collection-row:hover{background:color-mix(in srgb,var(--ink) 3%, transparent);}
.collection-row:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}
/* banner theme: the colour spans the whole banner, fading from a light shade (top-left, where the
   title sits) to a dark shade (bottom-right). A sparse golden motif drifts up to the right at ~30°
   and fades out toward the left; it brightens on hover. */
.collection-deco{
  position:absolute; inset:0; z-index:0; pointer-events:none; overflow:hidden;
  background:linear-gradient(to bottom right, color-mix(in srgb, var(--coll-bg, #888) 46%, white), color-mix(in srgb, var(--coll-bg, #888) 76%, black));
}
.night .collection-deco{ /* flip so the (light) title text stays on the dark end */
  background:linear-gradient(to bottom right, color-mix(in srgb, var(--coll-bg, #888) 78%, black), color-mix(in srgb, var(--coll-bg, #888) 50%, white));
}
/* NOTE: collection banners are deliberately STATIC — no drifting motifs, no animated pattern layers
   (a motif-drift system lived here once and was removed on request; don't reintroduce it). */
.collection-row > .collection-main, .collection-row > .collection-actions, .collection-row > .coll-ic{position:relative; z-index:1;}
/* Default (folio) theme — the opal: the hue SHIFTS rather than fading, the play of colour being the whole
   of the stone. This replaced the old default's "bookplate" (a wash of the collection's hue into the card
   under a fine inner rule) when Opalite became the default; the inner rule went with it, a cabochon having
   no facet to draw. A theme MUST override `.collection-deco` — the base rule washes the banner in its own
   hue at 46–76%, which leaves the quiet text on it unreadable. */
body[data-theme="folio"] .collection-deco{
  background:linear-gradient(100deg, color-mix(in srgb,var(--coll-bg, #888) 17%, var(--card)) 0%,
    color-mix(in srgb,var(--coll-bg, #888) 6%, var(--card)) 38%, color-mix(in srgb,#BFD9FF 22%, var(--card)) 68%, var(--card) 100%);
}
body[data-theme="folio"].night .collection-deco{
  background:linear-gradient(100deg, color-mix(in srgb,var(--coll-bg, #888) 40%, var(--card)) 0%,
    color-mix(in srgb,var(--coll-bg, #888) 16%, var(--card)) 40%, color-mix(in srgb,#3A4980 34%, var(--card)) 70%, var(--card) 100%);
}
/* the studied/total count sits in a small solid box so it stays legible over the colour wash */
.collection-row .prog .count{background:var(--coll-bg, var(--card)); color:#fff; padding:2px 9px; border-radius:6px; box-shadow:0 0 0 1px rgba(0,0,0,.08);}
.collection.placeholder{background:var(--paper-2); box-shadow:var(--shadow-sm);}
/* a ghost of the collection's identity colour shows through — a shelf awaiting opening, not an empty carton.
   The row no longer carries a level badge or an XP bar (nothing here can be studied), so the whole opacity
   fade no longer has to cover a meter: it is dialled back to .78, since at .62 the title and its pill sat
   under a tinted wash and fell close to the contrast floor. */
.collection.placeholder .collection-row{opacity:.78; cursor:default; padding-top:14px; padding-bottom:14px;}
.collection.placeholder .collection-row:hover{background:none;}
/* The title row's 9px bottom margin separates it from the XP bar underneath — and a coming-soon
   collection has no XP bar, so that margin was 9px of nothing below the title, sitting inside a flex
   item the row centres as a whole. The title therefore rode ~4.5px above the middle of the banner.
   (A flex item establishes its own formatting context, so the margin cannot collapse away by itself.) */
.collection.placeholder .collection-title-row{margin-bottom:0;}
.collection.placeholder .collection-title{color:var(--ink-soft);}
/* (the gold collection seals were removed on request) */
.collection-main{flex:1; min-width:0;}
.collection-title-row{display:flex; align-items:center; gap:10px; margin-bottom:9px;}
.collection-title{font-family:var(--display); font-weight:600; font-size:calc(23px * var(--fs)); letter-spacing:-.01em; line-height:1;}
.collection-span{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.02em; color:var(--ink-faint); white-space:nowrap;}
.pill{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; color:var(--ink-faint); border:1px solid var(--rule); border-radius:99px; padding:3px 9px; font-weight:500;}
.pill.soon{color:var(--ochre); border-color:color-mix(in srgb,var(--ochre) 35%, var(--rule));}
.chev{
  flex:none; width:40px; height:40px; border-radius:50%; border:1px solid var(--rule);
  background:var(--paper); color:var(--ink-soft); display:grid; place-items:center; cursor:pointer;
  transition:transform .34s var(--ease), background .2s, border-color .2s, color .2s;
}
.chev:hover{background:var(--indigo-wash); color:var(--indigo); border-color:color-mix(in srgb,var(--indigo) 30%,var(--rule));}
.chev.open{transform:rotate(180deg);}
.chev svg{width:18px; height:18px;}
.chev:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}

/* subdecks expand */
.node-children{display:grid; grid-template-rows:0fr; transition:grid-template-rows .42s var(--ease);}
.node-children.open{grid-template-rows:1fr;}
.node-children-inner{overflow:hidden;}
.node-children-pad{padding:4px 16px 18px 16px; display:flex; flex-direction:column; gap:9px;}
/* align-items is FLEX-START rather than centre because every row is two lines now — a title with the
   count and the size under it — so the number, the + and the chevron line up with the NAME rather than
   with the middle of the block. */
.node{
  display:flex; align-items:flex-start; gap:16px; padding:15px 18px; cursor:pointer;
  background:var(--paper); border:1px solid var(--rule); border-radius:11px;
  border-left:3px solid var(--indigo);
  transition:transform .16s var(--ease), background .18s, box-shadow .2s, border-color .2s;
  opacity:0; transform:translateY(-6px); animation:subIn .4s var(--ease) forwards;
}
.node-children.open .node:nth-child(1){animation-delay:.04s;}
.node-children.open .node:nth-child(2){animation-delay:.10s;}
.node-children.open .node:nth-child(3){animation-delay:.16s;}
@keyframes subIn{to{opacity:1; transform:none;}}
.node:hover{transform:translateX(3px); box-shadow:var(--shadow-sm); background:var(--card);}
.node:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.node.placeholder{border-left-color:var(--ink-faint); cursor:default; background:var(--paper-2); filter:opacity(.5);}
.node.placeholder:hover{transform:none; background:var(--paper-2);}
/* deck rows take their collection's identity hue for the left hairline (--coll-bg inherits from the
   .collection root); branches keep ochre and placeholders grey so the hierarchy still reads */
.collection .node{border-left-color:var(--coll-bg, var(--indigo));}
.collection .node.branch{border-left-color:var(--ochre);}
.collection .node.placeholder{border-left-color:var(--ink-faint);}
/* nested branches (sub-subdecks) */
.node-group{display:flex; flex-direction:column;}

/* admin: drag handle + drop indicators on the library
   IT RESTS VISIBLE, AND THAT IS THE WHOLE OF THE Aug 2026 "I can no longer drag collections" REPORT.
   The grip has always been rendered and `draggable`, and it was `opacity:0` until the row was hovered —
   so on the live collections the one control that says the rows can be reordered was invisible until you
   happened to point at it, while the Coming-soon rows showed theirs at rest (their `.placeholder`
   overrides below exist to fight the ancestor's `filter:opacity(.5)`, and the side effect was a resting
   grip on those rows and nowhere else). Reordering worked throughout; nothing said so. It rests at .32 —
   present enough to be found, quiet enough that a reader-facing page is not a page of handles — and the
   hover states are unchanged. Only an admin ever sees one: `wireLibraryDnd` returns before the grips are
   built for anybody else. */
.collection-row, .node{position:relative;}
/* `.lib-grip`, `.lib-dragging`, `.lib-drop-before` and `.lib-drop-into` — the admin's drag over this page — are
   gone (Oct 2026, on request); see `wireLibraryDnd`'s note in app.js. `position:relative` above stays: the
   row's fill (`.collection-deco`) is laid out against it. */
.lib-empty{padding:22px; text-align:center; border:1.5px dashed var(--rule); border-radius:14px; color:var(--ink-quiet); font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.06em; text-transform:uppercase;}
.lib-empty::before{content:"✦"; display:block; font-size:calc(18px * var(--fs)); margin-bottom:6px; opacity:.55; color:var(--indigo);}
.node.branch{cursor:pointer; border-left-color:var(--ochre); background:var(--card);}
.node.branch:hover{transform:translateX(2px);}
.node.branch.placeholder{border-left-color:var(--ink-faint);}
.node.branch .node-title{font-family:var(--display); letter-spacing:-.01em;}
.chev-sm{width:32px; height:32px;}
.chev-sm svg{width:15px; height:15px;}
.node-children .node-children .node-children-pad{padding-top:10px;}
.node-num{font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-quiet); font-weight:500; flex:none; width:18px;}
.node-main{flex:1; min-width:0;}
.node-title-row{display:flex; align-items:center; gap:9px; flex-wrap:wrap; row-gap:4px;}
/* THE FIGURES ARE A LINE OF THEIR OWN, on both shelves and at every width. They used to sit on the
   title's line and wrap onto a second one when they would not fit, so whether a deck stated its size
   under its name or beside it was a function of the window and of how long that deck happened to be
   called — which is what made "the same way history decks do" impossible to point at. */
.node-meta{display:flex; align-items:center; gap:9px; flex-wrap:wrap; row-gap:4px; margin-top:5px;}
/* A LANGUAGE DECK'S OWN LINE — what it is and when to study it (Sep 2026). Quiet ink and a measure that
   stops well short of the row's width: it is guidance under a title, not a paragraph, and a line that
   ran the full width of a wide row would read as the deck's description rather than as a caption. */
.node-sub{
  margin-top:4px; font-family:var(--sans); font-size:calc(11.5px * var(--fs));
  line-height:1.45; color:var(--ink-quiet); max-width:52ch;
}
.node-meta:empty{display:none;}
.node-title{font-family:var(--serif); font-weight:600; font-size:calc(17px * var(--fs)); line-height:1;}
.node-span{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.02em; color:var(--ink-faint); white-space:nowrap;}
/* how many cards the deck holds — the collection banner's own `.collection-count`, a size down to sit on
   a deck row rather than a banner. Same figure, same words, same place on the line, so the two levels of
   the hierarchy cannot be read as saying different things. */
.node-count{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.02em; color:var(--ink-quiet); white-space:nowrap;}
/* how much of the download the deck is — the same setting as the count it sits beside, a shade quieter,
   because it is the second figure on the line and the count is the one a reader came for. A language
   deck's row uses the same span (it is the size of a file that WILL be fetched); one fact, one place, on
   both shelves.

   THE TITLE ROW WRAPS RATHER THAN THE FIGURES BEING DROPPED. It is a nowrap flex line, so a third item
   on it would push the deck's NAME — the one part of the row with no shorter form — off the end at 390px;
   and a size hidden on a phone is a size hidden from most readers, which on a 21 MB download is exactly
   the wrong half to lose. Wrapping costs a row nothing at any width the figures fit. */
.node-size{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.02em; color:var(--ink-quiet); opacity:.72; white-space:nowrap;}

/* WHAT A DECK TEACHES, beside how big it is (Sep 2026, on request). Set as the count and the size are —
   same face, same size, same quiet ink — because these are figures on the same line and a fourth
   typographic treatment on one row is noise. They are drawn only where they VARY: every deck on the
   shelf is asked both ways, so that fact says nothing; audio is the norm, so only its absence is
   printed, and `.node-fact-no` gives that one the ochre the site already uses for a fact a reader
   should notice without being warned by it. */
.node-fact{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.02em; color:var(--ink-quiet); opacity:.72; white-space:nowrap;}
.node-fact-no{color:var(--ochre); opacity:.9;}

/* ============================================================
   STUDY / FLASHCARD  (mirrors the source collection card CSS)
   ============================================================ */
.study-shell{max-width:720px; margin:0 auto;}
.study-bar{display:flex; align-items:center; gap:14px; margin-bottom:20px; flex-wrap:wrap;}
.backbtn{
  appearance:none; border:1px solid var(--rule); background:var(--card); cursor:pointer;
  font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-soft);
  padding:9px 14px; border-radius:8px; display:inline-flex; align-items:center; gap:7px;
  transition:transform .12s var(--ease), background .18s, border-color .18s;
}
.backbtn:hover{background:var(--indigo-wash); color:var(--indigo); border-color:color-mix(in srgb,var(--indigo) 30%,var(--rule));}
.backbtn:active{transform:scale(.97);}
/* the step-back-a-card button. It only exists once something has been graded, so it never sits there greyed
   out; it reads quieter than Exit because it is the rarer of the two. */
.undobtn{margin-left:-6px; border-style:dashed; color:var(--ink-faint);}
/* Since Aug 2026 this names the CARD's own deck rather than the session's, which in the pooled review is
   a full path ("Mandarin Chinese — HSK 3.0 Level 1 · Chinese → English") instead of the one word "Review".
   The bar already wraps, so a long label costs no layout; what it must not do is push the three counts off
   the end of the line, hence `min-width:0` and the ellipsis. The counts keep their own place because they
   are `flex:none` in the row, and this is the only part of the bar with a shorter form. */
.study-where{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint);
  min-width:0; flex:0 1 auto; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.counts{margin-left:auto; display:flex; gap:14px; align-items:center;}
.suspendbtn{
  display:inline-flex; align-items:center; gap:6px; appearance:none;
  border:1px solid var(--rule); background:var(--card); border-radius:8px; padding:5px 11px; cursor:pointer;
  font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; font-weight:600;
  color:var(--ink-faint); transition:color .18s, border-color .18s, background .18s, transform .12s;
}
.suspendbtn svg{width:14px; height:14px;}
.suspendbtn:hover{color:var(--zh); border-color:color-mix(in srgb,var(--zh) 40%,var(--rule)); background:var(--zh-wash);}
.suspendbtn:active{transform:scale(.97);}
.suspendbtn:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.cnt{font-family:var(--mono); font-size:calc(12.5px * var(--fs)); font-weight:600; display:flex; align-items:center; gap:5px;}
.cnt::before{content:""; width:8px; height:8px; border-radius:50%; display:inline-block;}
.cnt.new{color:var(--indigo-bright);} .cnt.new::before{background:var(--indigo-bright);}
.cnt.learn{color:var(--zh);} .cnt.learn::before{background:var(--zh);}
.cnt.due{color:var(--good);} .cnt.due::before{background:var(--good);}

.cardwrap{position:relative;}
.cardwrap.swap{animation:cardSwap .34s var(--ease);}
@keyframes cardSwap{0%{opacity:0; transform:translateY(14px) scale(.98);}100%{opacity:1; transform:none;}}

/* the card itself — copied tokens from the collection so it renders identically */
.study-card{
  position:relative;
  background:var(--card); border:1px solid var(--rule); border-radius:12px;
  padding:30px 32px 26px; box-shadow:var(--shadow-md); text-align:left;
  font-family:var(--serif); font-size:calc(18px * var(--fs)); line-height:1.62; color:var(--ink);
}
.study-card .card-head{display:flex; align-items:flex-end; gap:15px; border-bottom:1px solid var(--rule); padding-bottom:15px; margin-bottom:20px;}
.study-card .num{font-family:var(--han); font-weight:700; font-size:calc(30px * var(--fs)); line-height:.92; color:var(--indigo); letter-spacing:.01em; flex:none;}
.study-card .head-text{flex:1; min-width:0; padding-bottom:3px;}
.study-card .tag{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.18em; text-transform:uppercase; color:var(--ink-faint); font-weight:500; display:block;}
.study-card .label{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.2em; text-transform:uppercase; color:var(--indigo); font-weight:600; margin:0 0 7px; display:block; opacity:.5;}
/* the difficulty rating, top right of a study card (Aug 2026, on request). It takes the QUESTION/ANSWER
   label's own --indigo at the same .5, so the corner is the same voice as the two headers under it; an
   unearned star is the same colour at a fraction of the opacity, which reads as an outline without
   needing a second glyph. It is absolutely positioned so it costs the question no width, and it yields
   to the read-aloud mute button, which holds this corner when read-aloud is on. */
/* ---------- THE QUESTION HEADER ROW (Aug 2026, on request) ----------
   The difficulty rating sat absolutely in the corner at top:14px while the QUESTION label began at the
   card's own 30px padding, so the two were never level and could not be made level by an offset — the
   label's height follows the reader's text size and the corner's does not. In one flex row they are level
   at every size, and neither has to know where the other is. The row is what carries the label's old
   bottom margin, so the space under it is unchanged.

   The stars therefore stop being absolute HERE and stay absolute nowhere else; `.card-stars` is emitted
   at exactly one site, so there is no other layout to keep in step. Where read-aloud is on, its mute
   button still holds the true corner, so the row keeps clear of it. */
/* IT IS A THREE-COLUMN GRID SINCE SEP 2026, and that is what centres the successive-relearning row the
   request asked for: `1fr auto 1fr` gives the two OUTER columns an equal share of whatever is left, so
   the middle one sits at the card's own centre — exactly, at every text size, with no offset written
   down. A flex row cannot do it, because the state dot is a fourth item on the left whose width is
   `calc(8px * var(--fs))`: flexing the label and the stars equally leaves the middle pushed right by half
   the dot and its gap, which is a number that moves when the reader changes text size. Wrapping the dot
   and the label in `.q-lead` makes the left column ONE item, which is what makes the grid possible. */
.study-card .q-head{display:grid; grid-template-columns:1fr auto 1fr; align-items:center; gap:12px; margin:0 0 7px;}
.study-card .q-head .q-lead{display:flex; align-items:center; gap:12px; min-width:0; overflow:hidden;}
/* ...AND THE LABEL IS A FLEX ROW OF ITS OWN (Aug 2026, on a bug report that the dot, the word, the
   phrasing counter, the DIFFICULTY label and the stars sit at four different heights). The row above
   centres the two BOXES; inside the label the parts were still inline, so each aligned by its own rule —
   the dot on the BASELINE (a 7px circle sitting on it has its centre well above the text's), the counter
   on `middle`, the play triangle on the baseline again — and the stars, being a box rather than text,
   centred against none of them. Made a flex row with `align-items:center`, every part of the label
   centres on one line and that line centres against the stars, at every text size and with no offset
   anywhere. The per-part `vertical-align` rules below are inert in flex and are left as they are for
   any other context that ever renders them. */
.study-card .q-head .label{margin:0; flex:1 1 auto; min-width:0; display:flex; align-items:center;}
.study-card:has(.tts-mute) .q-head{padding-right:38px;}
.study-card .card-stars{position:static; flex:none; z-index:2; display:inline-flex; gap:2px; align-items:center;}
/* the stars and the three-day dots are PRESSABLE since Sep 2026 — each opens a bubble saying what it
   means (openInfoBubble in app.js), which a tooltip could never do on a phone */
[data-info]{cursor:pointer; -webkit-tap-highlight-color:transparent; border-radius:6px;}
[data-info]:focus{outline:none;}
[data-info]:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px;}
.info-bubble{position:fixed; z-index:9700; width:min(280px, calc(100vw - 16px)); background:#1B1A17; color:rgba(255,255,255,.88);
  padding:11px 13px; border-radius:9px; font-family:var(--sans); font-size:calc(12px * var(--fs)); line-height:1.5; text-align:left;
  box-shadow:var(--shadow-md); animation:ibIn .16s var(--ease) both;}
.night .info-bubble{background:#34343C;}
.info-bubble::after{content:""; position:absolute; top:100%; left:var(--ib-arrow, 50%); margin-left:-6px; border:6px solid transparent; border-top-color:#1B1A17;}
.night .info-bubble::after{border-top-color:#34343C;}
.info-bubble.ib-below::after{top:auto; bottom:100%; border-top-color:transparent; border-bottom-color:#1B1A17;}
.night .info-bubble.ib-below::after{border-bottom-color:#34343C; border-top-color:transparent;}
.info-bubble .ib-title{display:block; font-weight:600; color:#fff; margin-bottom:7px; font-size:calc(10.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; opacity:.82;}
.info-bubble .ib-row{display:block;}
.info-bubble .ib-row + .ib-row{margin-top:6px;}
.info-bubble .ib-row b{color:#fff; font-weight:700;}
.info-bubble .ib-quiet{color:rgba(255,255,255,.62); font-size:calc(11px * var(--fs));}
@keyframes ibIn{from{opacity:0; transform:translateY(4px);} to{opacity:1; transform:none;}}
/* WHERE THIS CARD STANDS, in the three colours the study bar's own counts use (Aug 2026, on request) —
   blue for a card never studied, red for one being learned, green for one up for review. It is a mark
   rather than a control, so it takes no pointer; its name is on the element, since three colours say
   nothing to a reader who cannot see them.
   IT IS A SIBLING OF THE "Question" LABEL RATHER THAN A CHILD OF IT (Sep 2026, on a bug report that it
   "should be at full opacity, same color as the three dots in the top right"). It was inside `.label`,
   which carries `opacity:.5` to set the word quietly over the question — and OPACITY ON AN ANCESTOR IS A
   GROUP: it composites the element and everything under it, so the dot's own `opacity:1` could not
   escape it and no colour, `!important` or otherwise, could have. Standing outside the label it takes
   `--indigo-bright` / `--zh` / `--good` at full strength, which is exactly the ink the study bar's own
   three counts (`.cnt::before`) are drawn in. The size is bumped 7px → 8px with them for the same reason. */
/* ABOVE A PHONE IT IS A CHIP THAT SAYS WHICH (Sep 2026, on request): NEW, REVIEW or REPEAT, in the same
   three inks. A coloured dot asks the reader to have learned a key; a word does not, and a tablet or a
   desktop has the room. The wash is mixed from the state's own colour and the WORD is mixed towards the
   ink, since `--good` and `--indigo-bright` are quiet tokens that fall under 4.5:1 as 10px text on their
   own; the plain colour is declared first for a browser without `color-mix`. Below 640px the word is
   removed and the chip goes back to being the dot, so the phone's row keeps its width. */
.study-card .q-dot{--qd:var(--indigo-bright); display:inline-flex; align-items:center; flex:none; opacity:1;
  margin-right:8px; padding:calc(2px * var(--fs)) calc(8px * var(--fs)); border-radius:999px;
  border:1px solid var(--qd); background:transparent;
  background:color-mix(in srgb, var(--qd) 14%, transparent);
  color:var(--qd); color:color-mix(in srgb, var(--qd) 72%, var(--ink));
  font-size:calc(10px * var(--fs)); font-weight:700; letter-spacing:.09em; line-height:1.3;
  text-transform:uppercase; white-space:nowrap;}
.study-card .q-dot.q-new{--qd:var(--indigo-bright);}
.study-card .q-dot.q-learn{--qd:var(--zh);}
.study-card .q-dot.q-review{--qd:var(--good);}
@media (max-width:640px){
  .study-card .q-dot{display:inline-block; width:calc(8px * var(--fs)); height:calc(8px * var(--fs));
    padding:0; border:0; border-radius:50%; background:var(--qd); vertical-align:baseline;}
  .study-card .q-dot .q-dot-t{display:none;}
}
.card-stars .cs-star svg{width:calc(12px * var(--fs)); height:calc(12px * var(--fs)); display:block; fill:var(--indigo); opacity:.13;}
.card-stars .cs-star.on svg{opacity:.5;}
/* THE WORD, beside the stars (Aug 2026, on request): five small marks in a corner say that something is
   being rated and not what. Small and thin, so it labels the row rather than competing with the question
   it sits above; aria-hidden because the row's own aria-label already says "Difficulty N of 5" in words. */
.card-stars .cs-lbl{font-family:var(--sans); font-size:calc(8.5px * var(--fs)); font-weight:400; letter-spacing:.09em;
  text-transform:uppercase; color:var(--indigo); opacity:.5; margin-right:5px; white-space:nowrap;}
/* the community figure, where a card has enough answers behind it to have one. Mono, because it is a
   measurement rather than a word, and it must not be mistaken for part of the label. */
.card-stars .cs-pct{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.02em;
  color:var(--indigo); opacity:.62; margin-left:5px;}
.card-stars.cs-community .cs-star.on svg{opacity:.62;}
/* ---------- stepping through a card's phrasings (Aug 2026, on request) ----------
   Every card carries three ways of asking the same thing, and until now which one a reader met was a coin
   toss they could not appeal. The chevrons sit on the Question label because that is what they qualify —
   the counter between them is what tells a reader there is more than one at all. */
.study-card .q-cycle{display:inline-flex; align-items:center; gap:1px; margin-left:9px; vertical-align:middle; letter-spacing:0;}
.study-card .qc-btn{appearance:none; background:none; border:0; padding:0; width:17px; height:17px; cursor:pointer; color:inherit; display:inline-flex; align-items:center; justify-content:center; border-radius:5px; opacity:.75; transition:opacity .14s var(--ease), background .14s var(--ease);}
.study-card .qc-btn svg{width:11px; height:11px;}
.study-card .qc-btn:hover{opacity:1; background:color-mix(in srgb,var(--indigo) 14%, transparent);}
.study-card .qc-btn:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:1px;}
/* `nowrap` because the counter is a pair of numbers and a slash: with the header's middle column now
   taken by the successive-relearning row, a narrow phone can squeeze this box to its 30px minimum,
   and "2 / 3" then breaks across two lines — which makes the whole header two lines tall. */
.study-card .qc-n{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; min-width:30px; text-align:center; white-space:nowrap;}
.study-card .question{font-size:calc(18px * var(--fs)); line-height:1.62; margin:0 0 20px; color:var(--ink);}
/* THE WRITING BAND — empty space under the question to work an answer out on, drawn only while the
   marker's pen is down (Sep 2026, on request). Three things decide its shape.
   IT IS NOT DRAWN AT ALL BY DEFAULT: `body.wb-down` is set by applyWBState the moment the pen goes down
   and cleared when it comes up, so a reader who never picks the marker up sees the card they always saw.
   IT STAYS WHEN THE ANSWER IS REVEALED, which is the second half of the request — it sits between the
   question and `.reveal` in the markup, so the answer box opens UNDER it and the working stays on the
   page beside what it was working out.
   ITS HEIGHT IS IN `em`, so it scales with the reader's own text size as every other band on the card
   does; the rule is a hairline at the foot rather than a box, because a boxed area over a canvas the
   reader can draw straight past would promise a boundary the ink does not keep. */
.study-card .scratch{display:none;}
body.wb-down .study-card .scratch{display:block; height:9em; margin:0 0 20px; border-bottom:1px dashed var(--rule);}
@media (max-width:640px){ body.wb-down .study-card .scratch{height:7em;} }
.study-card .blank{display:inline-block; box-sizing:border-box; width:2.7em; color:transparent; overflow:hidden; white-space:nowrap; vertical-align:baseline; border-bottom:2px solid var(--ochre); margin:0 3px; line-height:1;}
/* the golden blank becomes a typing field; on reveal it is replaced by the typed letters, coloured
   green where they directly matched the answer (case-insensitive) and red where they didn't */
/* The width is set in px by setupCloze from a measured sizer span, because a proportional serif makes
   `ch` — the width of "0" — a third wider than the letters actually typed, and the underline then ran well
   past the last word. `min-width` matches the untouched `.blank` above so an empty field looks the same as
   a question nobody has typed in yet. */
.study-card .blank-input{display:inline-block; box-sizing:content-box; width:2.7em; min-width:2.7em; max-width:100%; font:inherit; color:var(--ink); background:none; border:0; border-bottom:2px solid var(--ochre); padding:0 2px; margin:0 3px; line-height:1; vertical-align:baseline; outline:none; transition:border-color .18s;}
/* the hidden ruler the field is measured against: same font by inheritance, out of the flow and out of the
   accessibility tree, never `display:none` (a box with no layout cannot be measured) */
.study-card .blank-sizer{position:absolute; visibility:hidden; white-space:pre; pointer-events:none; left:-9999px; top:0; font:inherit; letter-spacing:inherit; padding:0; border:0;}
.study-card .blank-input:focus{border-bottom-color:var(--indigo);}
.study-card .blank-graded{display:inline-block; border-bottom:2px solid var(--ochre); margin:0 3px; padding:0 2px; line-height:1; font-weight:600; white-space:pre;}
.study-card .blank-graded.empty{color:var(--ink-faint);}
.study-card .ch-ok{color:#15a34a;}
.study-card .ch-bad{color:#e23a2e;}
.night .study-card .ch-ok{color:#46d27f;}
.night .study-card .ch-bad{color:#ff6155;}

.answer{background:var(--indigo-wash); border-radius:8px; padding:15px 20px; margin:0 0 22px; display:flex; justify-content:space-between; align-items:flex-start; gap:14px; flex-wrap:nowrap;}
.answer-main{display:flex; flex-direction:column; align-items:flex-start; gap:4px; flex:0 1 auto; min-width:0;}
.answer-main .label{margin:0; line-height:1.5; padding-top:0;}
.answer-av{min-width:0;}
/* The answer term and, where the card has one, its flag — a row so the flag is centred on the term rather
   than on the block below it. Only emitted on a card that carries a flag (see cardFlagHTML), so nothing
   else in the answer box changes shape. `min-width:0` on the term is what lets a long name ellipsise
   rather than pushing the flag out of the box. */
.av-term{display:flex; align-items:center; gap:12px; min-width:0;}
.av-term .val{min-width:0;}
.av-flag{
  flex:0 0 auto; height:calc(26px * var(--fs)); width:auto; max-width:calc(56px * var(--fs));
  object-fit:contain; border-radius:2px; border:1px solid var(--rule); box-shadow:var(--shadow-sm);
  background:var(--card);
  /* it opens in the fullscreen viewer (see answerFlagHTML) — a flag two centimetres wide hides the very
     charges that tell one from another, so the cursor says there is more of it to see */
  cursor:zoom-in;
}
.av-flag:focus-visible{outline:2px solid var(--accent); outline-offset:2px;}
.av-row{display:flex; align-items:center; gap:14px; margin-top:7px;}
.av-row:empty{margin-top:0;}
/* SUCCESSIVE RELEARNING — the pips under the answer term (see CRIT_DAYS in app.js). Three marks saying
   which separate DAYS this card has been recalled on, which is the distinction the whole thing draws and
   which a count could not make. Quiet by design: it is a record, not a demand, and it sits under a term
   set at 25px, so it must not compete with it. It goes gold once the criterion is met, taking the same
   ink the collection icons and the finished-deck bars use, so "you are done with this" reads the same
   way everywhere on the site. */
/* THE SUCCESSIVE-RELEARNING ROW LIVES IN THE STUDY CARD'S HEADER (Sep 2026, on request) — see
   critPipsHTML in app.js, and the `.q-head` grid above for how it is centred. */
.crit-row{display:flex; align-items:center; gap:7px; margin-top:9px;}
.study-card .q-head .crit-row{margin-top:0; min-width:0; justify-self:center;}
/* THE STARS ARE PLACED IN COLUMN 3 EXPLICITLY, not left to fall there (Sep 2026, on a bug report: "when
   the three dots for three days studied have not yet appeared on the first time a card is shown, the
   difficulty rating jumps to the center of the card when it should stay in the top right"). `critPipsHTML`
   returns NOTHING for a card with no record, so on a card's first sighting the grid has two children
   rather than three and the stars land in the `auto` MIDDLE column — where `justify-self:end` has nothing
   to push against, an auto column being only as wide as its content. Naming the column is what makes the
   corner the corner in every state, this one and the phone's, where the row moves to the study bar and
   the middle child is absent again. */
.study-card .q-head .card-stars{grid-column:3; justify-self:end;}
/* WHERE THE ROW GOES — one declaration, read back by `renderCard` (see `--crit-slot` there). */
body{--crit-slot:head;}
/* …and in the study bar it keeps its label and sits opposite the three counts, which take a full-width
   line of their own at this width (see `.counts` in the phone block). */
.counts .crit-row{margin-top:0; margin-right:auto; min-width:0;}
.crit-pips{display:inline-flex; gap:4px;}
.crit-pip{width:7px; height:7px; border-radius:50%; border:1.5px solid var(--prog-seen); box-sizing:border-box;}
.crit-pip.on{background:var(--prog-seen); border-color:var(--prog-seen);}
/* THE CAPTION IS SET LIKE THE TWO LABELS IT SHARES THE HEADER ROW WITH (Sep 2026, on request:
   "make the 'Recalled on X of 3 days' atop cards the same font and blue color as the Question header
   and difficulty rating"). Both of those are mono-ish small caps in the indigo at half strength —
   `.study-card .label` and `.card-stars .cs-lbl` — and this row sat between them in the serif and the
   faint ink, which read as a third kind of thing on a line that holds only two. The `.done` rules below
   still override the colour with the gold: "Learned" is a state rather than a caption, and the gold is
   how finishing something reads everywhere else on the site. */
.crit-lbl{font-family:var(--mono); font-size:calc(10px * var(--fs)); font-weight:600; letter-spacing:.12em; text-transform:uppercase; color:var(--indigo); opacity:.5; white-space:nowrap;}
/* all three filled: the deep "learned" blue of the deck bar (Sep 2026, on request — it was gold); the DOTS
   alone turn GREEN (Oct 2026, on request) — `--good` is set per theme. The caption keeps the blue: it is
   10px text, and the light theme's green does not clear 4.5:1 on the card. */
.crit-row.done .crit-pip{background:var(--good); border-color:var(--good);}
.crit-row.done .crit-lbl{color:var(--prog-learned); opacity:1;}
body.night .crit-row.done .crit-lbl{color:#8FA2FF;}
/* ...and in the header a PHONE gets the pips alone (Sep 2026, on request: "only display the text on
   desktop and tablet format. on mobile, only show the three dots"). Four things share that line — the
   state dot, the "Question" label with its phrasing counter, this row and the difficulty stars — and
   measured on a 390px screen the sentence alone is wider than the stars beside it. The whole sentence is
   still on the row as its `aria-label` and its tooltip, which is the only form a reader who cannot see
   three dots has ever had, so nothing is said by the words alone.
   AND A TABLET NOW GETS THE PIPS TOO (Sep 2026, on request: "on tablet view, in the 'recalled X/3 days'
   atop cards, remove the text and keep only the dots"), which reverses the request above at that width.
   1024px is the same landmark `--cp-sheet` uses — the iPad's landscape width — so the two places that
   ask "is this a tablet?" answer it the same way rather than each picking a number. The card's own width
   is capped at 680px well before the window is, so on a tablet this line is as tight as a phone's while
   the screen around it is not, which is why the crowding does not stop at 640px. */
@media (max-width:1024px){
  .study-card .q-head .crit-lbl{display:none;}
}
.answer .val{font-family:var(--display); font-weight:600; font-size:calc(25px * var(--fs)); color:var(--zh); line-height:1.18; min-width:0; display:block;}
/* THE ANSWER TERM ALWAYS OPENS ON A CAPITAL (Aug 2026, on request), even where the question and the
   background write it lower-case — here it is a heading naming the thing, not a word in a sentence.
   Done in CSS rather than in `buildBack`, and that is the load-bearing part: the stored `answer` is
   HTML that may open on a <b> or an <a>, so an uppercasing pass in JS would have to walk into the
   markup to find the first letter; and the answer is also read aloud, typed against in the cloze box
   and matched by the glossary, none of which must see a capital the data has not got. ::first-letter
   changes what is painted and nothing else. It reaches the letter through inline descendants, and on a
   hanzi answer it is simply inert, there being no case to change. */
.answer .val::first-letter{text-transform:uppercase;}
/* …and the one exception, set by buildBack: a term whose own lower-case initial is part of its name
   (pH, mRNA, tRNA), recognised by a lower-case letter followed immediately by a capital. */
.answer .val.nocap::first-letter{text-transform:none;}
.answer .val a{color:inherit; text-decoration:none; border-bottom:1px solid rgba(200,69,60,.34);}
.night .answer .val a{border-bottom-color:rgba(240,147,139,.42);}
/* column-gap 16, not the 7 it was: the label is a small-caps mono word and the value beside it is mono
   digits, and at 7px the two read as one string. Aug 2026, on request. */
.dt{display:grid; grid-template-columns:max-content 1fr; column-gap:16px; row-gap:2px; align-items:baseline; min-width:0;}
.dt-solo{grid-column:1 / -1;}
/* The date line is a KEY/VALUE LIST, not a paragraph (Aug 2026, on request — it had grown to a
   sentence or three under a one-word label). Several key/value pairs inside one .dt land in the same
   grid, so their labels align down the left; .dt-sub is a continuation line under a value (the place
   under a birth date), sitting in the value column with no label of its own. */
.dt-sub{grid-column:2; opacity:.72;}
.dt-k{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--indigo); font-weight:600; opacity:.55; white-space:nowrap;}
.dt-v{font-family:var(--mono); font-size:calc(11.5px * var(--fs)); letter-spacing:.01em; color:var(--ink-faint); line-height:1.45; min-width:0;}
/* The collapsible translation column (`.answer-tr`, the 中文 toggle) stood here and is retired (Sep 2026):
   every card with a Chinese name now shows it in the answer box's middle section — see `.ans-cn` below. */
.tr-ov{color:var(--ink-soft); font-family:var(--han); font-weight:400; font-size:calc(16px * var(--fs));}
.tr-rom{font-family:var(--mono); font-size:calc(12px * var(--fs)); letter-spacing:.02em; color:var(--ink-faint);}
.tr-lit{color:var(--ink-soft); font-style:italic; font-family:var(--serif); font-size:calc(13.5px * var(--fs));}
.tr-k{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint); font-weight:500; flex:none; align-self:center;}
.py{font-family:var(--sans); font-style:normal; font-weight:400;}

/* ---------- the figures box beside an answer (map cards; see cardFactsHTML) ----------
   Tiles rather than the date line's two aligned columns, and deliberately so: `.dt` answers "when", in one
   narrow key/value list, and this answers "how many" — a capital, a population, an area — which reads far
   better as small facts than as more rows under the same labels.

   IT IS A GRID ON THE RIGHT OF THE ANSWER BOX, a sibling of `.answer-main` in `.answer`'s flex row — the
   slot `.answer-tr` takes on a Chinese card, and `.answer` is `space-between`, so it goes to the far edge
   with no rule of its own. TWO COLUMNS RATHER THAN `auto-fit`: the tiles are different widths (a capital
   against an area with its conversion), so an auto-fitting track count re-flows between one card and the
   next and the box stops being a shape a reader recognises. `minmax(0,1fr)` because a grid item's
   automatic minimum is its content's, and without it a long value pushes the column past its share and
   squeezes the answer beside it.

   `align-self:center` CENTRES the box against the answer beside it (Aug 2026, on request; it was
   `align-content:start`, which lined the tiles up with the answer term's first line). The two properties
   answer different questions and only the second one can do this: `align-content` distributes the grid's
   own tracks inside the grid's own height, which is its content's, so it moves nothing — the box is a flex
   item of `.answer`, whose `align-items:flex-start` is what was holding it to the top. */
.card-facts{
  display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:6px;
  align-content:start; align-self:center; flex:0 1 auto; min-width:0; max-width:min(58%, 340px);
}
.cf-tile{
  display:flex; flex-direction:column; gap:1px; min-width:0; padding:5px 10px 6px;
  border:1px solid var(--rule); border-radius:7px; background:var(--card);
}
.cf-k{font-family:var(--mono); font-size:calc(8.5px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--indigo); font-weight:600; opacity:.62; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.cf-v{font-family:var(--mono); font-size:calc(12px * var(--fs)); color:var(--ink-soft); line-height:1.35; overflow-wrap:anywhere;}

/* ---------- the answer's Chinese name (see answerNameHTML) ----------
   THE MIDDLE SECTION OF THE ANSWER BOX (Sep 2026, on request): a sibling of `.answer-main` and of the
   figures grid, so on a wide card it stands between the term on the left and the figures on the right,
   ruled off from the term by a hairline. The traditional form where it differs sits on the line above the
   simplified, and the pinyin with its speaker under both; the block is centred against the term, as the
   figures are. It was a line under the term, on a map card only; every card that carries
   a Chinese name gets it now. Below 640px `.answer` stacks (see the phone block), so it goes under the
   term at full width with the rule on its top edge instead. */
/* THE BLOCK IS SET FLUSH RIGHT (Sep 2026, on request). Its three lines are of very different widths — a
   two-character name over a four-character one over a romanisation twice as long as either — and ranged
   left they read as a ragged column drifting away from the rule beside them; ranged right they line up
   against the figures grid, which is the edge the eye is already following down the card. */
.ans-cn{display:flex; flex-direction:column; align-items:flex-end; gap:2px; flex:0 1 auto; min-width:0;
  align-self:center; padding:2px 0 2px 16px; border-left:1px solid color-mix(in srgb, var(--zh) 22%, transparent);
  line-height:1.15; text-align:right;}
.ans-cn .ac-chars{display:flex; flex-direction:column; align-items:flex-end; gap:1px;}
/* the traditional form is written FIRST in the markup and so sits on the line above; it is still the
   quieter of the two, smaller and at half strength, so the simplified form under it reads as the name
   and this as the second script it is also written in. */
.ans-cn .ac-s{font-family:var(--han); font-weight:400; font-size:calc(24px * var(--fs)); color:var(--zh); line-height:1.15;}
.ans-cn .ac-t{font-family:var(--han); font-weight:400; font-size:calc(19px * var(--fs)); color:var(--zh); opacity:.5; line-height:1.15;}
.ans-cn .ac-row{display:flex; align-items:center; gap:7px;}
.ans-cn .ac-p{font-family:var(--mono); font-size:calc(11.5px * var(--fs)); letter-spacing:.02em; color:var(--ink-faint);}
/* the speaker: a small round button in the Chinese ink, hidden outright on a device with no speech engine */
.ans-cn .ac-say{flex:none; width:calc(22px * var(--fs)); height:calc(22px * var(--fs)); border-radius:50%; border:1px solid color-mix(in srgb, var(--zh) 45%, var(--rule));
  background:var(--card); color:var(--zh); display:inline-flex; align-items:center; justify-content:center; padding:0; cursor:pointer;
  transition:background .15s var(--ease), color .15s var(--ease);}
.ans-cn .ac-say svg{width:calc(12px * var(--fs)); height:calc(12px * var(--fs)); display:block;}
.ans-cn .ac-say:hover, .ans-cn .ac-say.tts-playing{background:var(--zh); color:#fff;}
.ans-cn .ac-say:focus-visible{outline:2px solid var(--zh); outline-offset:2px;}
body.no-tts .ans-cn .ac-say{display:none;}

/* ---------- a map card's window (see the MAP CARDS block in app.js) ----------
   The question is a canvas: one place shaded on the globe, turned and zoomed by the reader. `touch-action:
   none` is what makes it work on a phone at all — the same rule the whiteboard canvas needs, and for the
   same reason: under the default the browser claims the drag as a page scroll and fires `pointercancel`
   at the element before it has seen two moves. */
.map-card{
  position:relative; margin:2px 0 16px; border-radius:10px; overflow:hidden;
  border:1px solid var(--rule); background:var(--paper-2); box-shadow:var(--shadow-sm);
}
.mc-canvas{
  display:block; width:100%; height:clamp(210px, 42vh, 340px);
  touch-action:none; cursor:grab; background:var(--paper-2);
}
.mc-canvas:active{cursor:grabbing;}
.mc-canvas:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}
.mc-zoom{position:absolute; right:9px; top:9px; display:flex; flex-direction:column; gap:5px;}
.mc-btn{
  width:29px; height:29px; border-radius:8px; border:1px solid var(--rule); cursor:pointer;
  background:color-mix(in srgb, var(--card) 88%, transparent); color:var(--ink-soft);
  font:600 15px/1 var(--sans); display:flex; align-items:center; justify-content:center;
  transition:background .15s var(--ease), color .15s var(--ease);
}
.mc-btn:hover{background:var(--card); color:var(--ink);}
.mc-btn svg{width:14px; height:14px;}
/* "Discovered!" — top LEFT, opposite the control stack, on a card whose reveal has just earned the
   reader somewhere new (Sep 2026, on request). It takes --newterm, which is the ink the site already
   uses for a term met for the first time, so a discovery looks the same wherever it happens. */
.mc-new{
  position:absolute; left:9px; top:9px; z-index:2; pointer-events:none;
  font-family:var(--mono); font-size:calc(9.5px * var(--fs)); font-weight:600; letter-spacing:.08em;
  text-transform:uppercase; line-height:1.7; padding:0 8px; border-radius:999px; white-space:nowrap;
  color:var(--newterm);
  background:color-mix(in srgb, var(--newterm) 13%, var(--card));
  border:1px solid color-mix(in srgb, var(--newterm) 38%, transparent);
  animation:mcNewIn .34s var(--ease) both;
}
.mc-new[hidden]{display:none;}   /* the author `display` would beat the UA rule and pin it open */
@keyframes mcNewIn{from{opacity:0; transform:translateY(-4px) scale(.96);} to{opacity:1; transform:none;}}
/* The "Drag to turn · zoom to look closer" chip that sat bottom-left is gone (Sep 2026, on request); the
   canvas's aria-label still says what to do, which is the one reader the words were for. */
/* the two lazy bundles behind it. A window that says nothing while it waits reads as one that failed. */
.map-card.mc-loading::after, .map-card.mc-failed::after{
  content:"Loading the map…"; position:absolute; inset:0; display:flex; align-items:center; justify-content:center;
  background:var(--paper-2); color:var(--ink-faint); font:500 calc(12px * var(--fs))/1.4 var(--sans); text-align:center; padding:16px;
}
.map-card.mc-failed::after{content:"This map could not be loaded. The answer is below.";}
.map-card.mc-loading .mc-zoom, .map-card.mc-failed .mc-zoom{display:none;}

/* ---------- an artwork card's picture (see cardArtSpec in app.js) ----------
   The question, on a card whose answer is the work in it. THREE THINGS ARE DELIBERATE.

   IT IS SHOWN WHOLE (`object-fit:contain`) rather than filled into a frame, which every other picture on
   the site is: a painting cropped to 16:9 is a different picture, and on a portrait or a scroll it is a
   detail. The height is capped in `vh` so a tall work still leaves the question and the grade bar on
   screen, and the box takes the paper behind it so the letterboxing reads as mount rather than as a gap.

   IT CARRIES NO CHROME UNTIL THE REVEAL — no zoom glyph, no pointer, no caption — because all three say
   "there is more here", and what is more here is the answer. `.revealed` is added by `cardArtReveal` and
   is what turns it into the enlargeable, credited figure the rest of the site draws by default.

   THE CAPTION IS THE FIGURE'S OWN `figcaption`, so it cannot be laid over the picture and cannot be
   mistaken for part of the work. */
.art-shot{
  margin:2px 0 16px; border-radius:10px; overflow:hidden; border:1px solid var(--rule);
  background:var(--paper-2); box-shadow:var(--shadow-sm);
}
.art-shot img{
  display:block; width:100%; height:clamp(220px, 46vh, 420px);
  object-fit:contain; background:var(--paper-2);
}
/* A DEAD FILE ON THIS FORMAT IS THE WHOLE QUESTION GONE, so it says so rather than hiding (which would
   leave four empty fields under nothing) and rather than painting the alt text at full size in the frame,
   which is what a browser does by default and which reads as a broken page. The sentence is in CSS
   because the frame is built by `cardArtHTML` before anything knows whether the file will arrive, and the
   site ships in English (see MULTILANG); it follows `.ces-img.media-dead`, which says its own line the
   same way. */
.art-shot.media-dead img{display:none;}
.art-shot.media-dead::after{
  content:"This picture could not be loaded."; display:block; padding:34px 16px; text-align:center;
  font:400 calc(13px * var(--fs))/1.5 var(--sans); color:var(--ink-faint);
}
.art-shot.revealed{cursor:zoom-in;}
.art-shot.revealed:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}

/* THE FLAG ON A FLAG CARD'S FRONT (Sep 2026, with the Flags collection; see the FLAG CARDS block in
   app.js). `.art-shot`'s sibling and NOT a reuse of it, because the two frames differ in the two things
   that matter here.

   THE FLAG IS CONTAINED AND NEVER CROPPED, like a painting — flags run from 1:1 (Switzerland, Vatican
   City) through 2:3 and 1:2 to Qatar's 11:28 — so the height is a MAXIMUM and `object-fit:contain` does
   the rest. It is shorter than the artwork frame: a flag is a wide shallow thing and a 46vh box would
   letterbox most of them into a band of paper.

   IT IS DRAWN ON A RULED GROUND, which the artwork frame gets for free and this one genuinely needs.
   Japan, Qatar's hoist, the Nordic crosses and every white-bordered flag have WHITE at the edge, so
   against the card they lose their own outline and against a dark card they are cut in half. The border
   and the paper behind it are what keep the edge visible in both, and Japan and Switzerland at both
   light and dark are the whole test of this rule.

   IT NEVER GAINS CHROME, unlike `.art-shot.revealed`: there is no zoom glyph and no caption in ANY
   state, because a national flag's credit line reads "Government of India, public domain" and the
   viewer's own caption bar would print it. The credited, enlargeable copy is the small `.av-flag` inside
   the answer box, which `buildBack` draws on the reveal. */
/* THE FRAME TAKES THE FLAG'S OWN SHAPE (Sep 2026, on request: "the canvas … should shrink horizontally to
   fit the shape of the flag"). `--ar` is the flag's width over its height, written on the figure by
   `fitFlagShot` once the file has loaded; 1.5 until then, the commonest ratio on the shelf. The frame's
   width is derived from its target height, `min(100%, height × ratio)`, and the picture's height from the
   frame's width through `aspect-ratio` — so a narrow screen shortens a wide flag rather than
   letterboxing it, and no ratio can leave a band of paper beside the flag. It is centred, the one place a
   frame narrower than the card has to sit. `object-fit:contain` stays as the fallback for a file whose
   size never arrives. */
.flag-shot{
  --flag-h:clamp(150px, 30vh, 280px);
  width:min(100%, calc(var(--flag-h) * var(--ar, 1.5))); box-sizing:border-box;
  margin:2px auto 16px; border-radius:10px; overflow:hidden; border:1px solid var(--rule);
  background:var(--paper-2); box-shadow:var(--shadow-sm);
}
.flag-shot img{
  display:block; width:100%; height:auto; aspect-ratio:var(--ar, 1.5);
  object-fit:contain; background:var(--paper-2);
}
/* AND ON THE REVEAL IT BECOMES ENLARGEABLE, which is where its credit lives (Sep 2026, on request: "the
   image box should not show the image source or link on the card, only when it is clicked to enlarge
   should it say the source info"). There is deliberately NO caption class here: a `figcaption` under this
   frame put two lines of Commons URL under every flag, and `openMediaViewer` draws the credit as
   `.iv-credit` under the enlarged picture instead. `.revealed` is added by `cardFlagReveal` and never
   before, since a flag's credit names the country. */
.flag-shot.revealed{cursor:zoom-in;}
.flag-shot.revealed:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}
/* A dead file here is the whole question gone, exactly as on an artwork card, and for the same reason it
   says so rather than letting the browser paint the alt text at full size in the frame. */
.flag-shot.media-dead img{display:none;}
.flag-shot.media-dead::after{
  content:"This flag could not be loaded."; display:block; padding:34px 16px; text-align:center;
  font:400 calc(13px * var(--fs))/1.5 var(--sans); color:var(--ink-faint);
}

/* ---- DRAW CARDS: the pad the reader draws a flag in (Sep 2026, on request) ----
   The pad is its OWN canvas with its OWN menu across the top of it, and has nothing to do with the
   floating marker — see the DRAW CARDS block in app.js for why the first cut (a frame over the page-wide
   whiteboard) could not answer "only be used within that canvas" or hold a fill.
   THE WIDTH IS CAPPED ON THE PAD AND NOT THE FRAME. Capping the FRAME's height instead (`max-height`)
   does shrink it back to 3:2 — but only the frame, so on a desktop it ended short of `.draw-pad`'s own
   right edge while `.dp-answer` below it stayed full width, and the drawing and the flag it is compared
   against were two different widths. One cap on the parent keeps all three edges together. */
.draw-pad{margin:20px auto 18px; max-width:560px;}
/* The menu sits ON the canvas's top edge: one box, ruled across rather than two. */
.dp-tools{
  display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap;
  padding:7px 9px; border:1px solid var(--rule); border-bottom:none;
  border-radius:10px 10px 0 0; background:var(--paper-2);
}
.dp-cols, .dp-acts{display:flex; align-items:center; gap:6px;}
.dp-col{
  width:22px; height:22px; padding:0; border-radius:50%; cursor:pointer;
  border:2px solid transparent; background:var(--dpc); box-shadow:inset 0 0 0 1px rgba(0,0,0,.18);
}
.dp-col.on{border-color:var(--ink); box-shadow:inset 0 0 0 1px rgba(0,0,0,.18), 0 0 0 2px var(--paper-2);}
/* The sixth swatch is the reader's own colour and opens the field below. It wears a small notch so it
   reads as "and any other" rather than as a seventh fixed colour, and the notch inverts while the field
   is open so the swatch says which state it is in without a second control. */
.dp-custom{position:relative;}
.dp-custom::after{
  content:""; position:absolute; right:-1px; bottom:-1px; width:9px; height:9px; border-radius:50%;
  background:var(--paper-2); box-shadow:inset 0 0 0 1px rgba(0,0,0,.28);
}
.draw-pad.dp-picking .dp-custom::after{background:var(--ink);}
/* THE PICKER IS A ROW, NOT A POPOVER — the marker's own rule for its own picker, and the same reason:
   the bar above is already a box with a decided position, and a second floating box inside it would
   have to decide again. `[hidden]` has to be said explicitly because the author `display:flex` beats
   the UA rule (codebase convention, cf. `.wb-pick` and `.ces-imgpanel`). */
.dp-pick{
  display:flex; flex-direction:column; gap:8px; padding:9px;
  border-inline:1px solid var(--rule); background:var(--paper-2);
}
.dp-pick[hidden]{display:none;}
.dp-col:focus-visible, .dp-btn:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.dp-btn{
  width:30px; height:30px; padding:0; display:grid; place-items:center; cursor:pointer;
  border:1px solid var(--rule); border-radius:8px; background:var(--card); color:var(--ink-soft);
}
.dp-btn svg{width:17px; height:17px;}
.dp-btn.on{background:var(--ink); border-color:var(--ink); color:var(--paper);}
.dp-btn:disabled{opacity:.38; cursor:default;}
/* The brush-size slider takes the rest of the menu's row, or a row of its own when the colours and the
   tools have used the width up. The dot is drawn at the next stroke's own width (capped at its box) in
   the pen's colour; for the eraser it is a hollow ring. */
.dp-size{display:flex; align-items:center; gap:8px; flex:1 1 150px; min-width:130px; cursor:pointer;}
.dp-size-well{flex:none; width:26px; height:26px; display:grid; place-items:center;}
.dp-size-dot{display:block; width:4px; height:4px; border-radius:50%; box-shadow:0 0 0 1px rgba(0,0,0,.28);}
.dp-size-dot.dp-size-erase{box-shadow:0 0 0 1.5px var(--ink-soft);}
.dp-size-range{flex:1 1 auto; min-width:0; width:100%; margin:0; accent-color:var(--ink); cursor:pointer; touch-action:pan-y;}
/* `aspect-ratio` ALONE, with no `min-height` beside it: the two together inflated the USED WIDTH past
   the card — 34vh of a tall phone is 300px, which at 3:2 asked for a 450px-wide box inside a 366px card,
   and the pad ran off the right edge taking the card's own border with it. */
.dp-frame{
  position:relative; aspect-ratio:3 / 2; overflow:hidden;
  border:1px solid var(--rule); border-radius:0 0 10px 10px; background:var(--card);
}
/* THE CANVAS IS THE WHOLE FRAME, and `touch-action:none` is what lets a finger draw on it rather than
   having the gesture claimed as a scroll — the `pointercancel` fault this stylesheet records against
   every horizontal swipe on the site, here wanted rather than avoided. */
.dp-canvas{position:absolute; inset:0; width:100%; height:100%; touch-action:none; cursor:crosshair; display:block;}
/* …and the hint sits at the FOOT of the frame rather than its middle: centred, it is under the reader's
   own drawing, which is both something to draw over and something that makes the drawing harder to read.
   It is `pointer-events:none` so it can never take a press meant for the canvas beneath it, and it goes
   the moment anything is drawn. */
.dp-hint{
  position:absolute; left:0; right:0; bottom:10px;
  font:400 calc(12px * var(--fs))/1.5 var(--sans); color:var(--ink-faint);
  letter-spacing:.04em; text-transform:uppercase; pointer-events:none; padding:0 14px; text-align:center;
}
.draw-pad.dp-drawn .dp-hint{opacity:0;}
/* …and it comes BACK at the reveal, with a different word: two frames are then standing one above the
   other and the labels are what say which is which. */
.draw-pad.dp-drawn.dp-revealed .dp-hint{opacity:1;}
/* The answer lands UNDER the pad and never inside it, so nothing above it moves when it arrives. */
/* The revealed flag takes its own shape, exactly as a flag card's front does (see `.flag-shot` and
   `fitFlagShot`), so a flag narrower than the pad sits centred under it with no band of paper beside it.
   That gives up the three-edges-together rule above for the ANSWER alone, deliberately: it was asked for
   by name ("should never have black bars on the side"), and the pad itself keeps its width. */
.dp-answer{
  --flag-h:clamp(150px, 30vh, 280px);
  width:min(100%, calc(var(--flag-h) * var(--ar, 1.5))); box-sizing:border-box;
  margin:14px auto 0; border-radius:10px; overflow:hidden; border:1px solid var(--rule);
  background:var(--paper-2); box-shadow:var(--shadow-sm);
}
.dp-answer[hidden]{display:none;}   /* the author `display:block` would beat the UA rule, as .af-src does */
.dp-answer img{
  display:block; width:100%; height:auto; aspect-ratio:var(--ar, 1.5);
  object-fit:contain; background:var(--paper-2);
}
.dp-answer.revealed{cursor:zoom-in;}
.dp-answer.revealed:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}
/* A draw card has a canvas to work in, so the writing band under the question would be a SECOND blank
   area on one card — and the reader would have no way of telling which of the two the answer belongs in.
   The floating marker itself is untouched: with its pen down it draws OVER the pad, as it does over
   everything else on the page. */
body.wb-down .study-card.draw-card .scratch{display:none;}

.art-cap{
  display:block; padding:9px 12px 11px; border-top:1px solid var(--rule); background:var(--card);
  font:400 calc(12px * var(--fs))/1.5 var(--sans); color:var(--ink-soft); text-align:left;
}
.art-cap-t{display:block; font-weight:600; color:var(--ink);}
.art-cap-d{display:block; margin-top:2px;}
/* The credit's visible text IS a Commons URL (see mediaCreditHTML), which carries no break opportunity
   at all — on a phone it ran past the caption's right edge. Nothing else in the block needs this, so it
   is on the credit line rather than on the caption. */
.art-cap-c{display:block; margin-top:4px; color:var(--ink-faint); font-size:calc(11px * var(--fs)); overflow-wrap:anywhere;}
.art-cap-c a{color:inherit;}

/* ---------- the answer box an artwork card is answered in (Sep 2026, on request) ----------
   "The user must guess the title, artist, date of creation, and current ownership/location in the answer
   box." THREE labelled fields under the picture now, built by `cardArtHTML`: where the work is now came
   off the question side on a second request the same month and is stated in the answer's own figures
   grid instead. They are the only words on the question side, and they are the FORM rather than a clue:
   none of them says anything about the work.

   THE LABEL IS BESIDE THE FIELD, NOT OVER IT, and the label column is a fixed fraction rather than a
   `max-content`, so labels of different lengths keep every field left-aligned with the others, which is
   what makes the block read as one answer rather than three questions. Below 480px it stacks, where a
   fixed column would leave the widest label almost no room for the field beside it. The 7.5em is kept
   from the four-field version deliberately: it is the width a returning "Where it is now" would need,
   and a roomier label column costs the fields nothing at this size.

   GRADED, THE FIELD IS REPLACED RATHER THAN DISABLED (see gradeArtFields), so there is no editable copy
   left to improve an answer in. The mark takes its own colour and the typed value is struck through only
   where it is plainly wrong — a `near` date is not a mistake and must not be drawn as one. */
.art-ask{display:grid; gap:7px; margin:0 0 4px;}
.art-f{
  display:grid; grid-template-columns:minmax(0,7.5em) minmax(0,1fr); align-items:center; gap:10px;
}
.art-lab{
  font:600 calc(11px * var(--fs))/1.3 var(--sans); letter-spacing:.06em; text-transform:uppercase;
  color:var(--ink-faint);
}
.art-input{
  width:100%; min-width:0; padding:7px 10px; border:1px solid var(--rule); border-radius:8px;
  background:var(--paper-2); color:var(--ink); font:400 calc(15px * var(--fs))/1.4 var(--serif);
}
.art-input:focus{outline:none; border-color:var(--indigo-bright); box-shadow:0 0 0 2px color-mix(in srgb, var(--indigo-bright) 26%, transparent);}
.art-graded{
  display:flex; flex-wrap:wrap; align-items:baseline; gap:4px 8px; min-width:0;
  font:400 calc(15px * var(--fs))/1.45 var(--serif); color:var(--ink);
}
/* The two verdict colours are the graded cloze's own (`.ch-ok` / `.ch-bad` above), literal and with a
   night pair, because this is the same thing said about the same kind of answer and two greens for one
   idea drift apart invisibly. `near` takes --ochre, which is the blank's own colour in both modes. */
.art-mark{flex:none; font:700 calc(13px * var(--fs))/1 var(--sans);}
.art-graded.ok .art-mark{color:#15a34a;}
.art-graded.near .art-mark{color:var(--ochre);}
.art-graded.bad .art-mark, .art-graded.empty .art-mark{color:#e23a2e;}
.night .art-graded.ok .art-mark{color:#46d27f;}
.night .art-graded.bad .art-mark, .night .art-graded.empty .art-mark{color:#ff6155;}
.art-said{min-width:0; overflow-wrap:anywhere;}
.art-graded.bad .art-said{text-decoration:line-through; color:var(--ink-faint);}
.art-true{min-width:0; overflow-wrap:anywhere; font-weight:600;}
.art-graded.ok .art-true{font-weight:400; color:var(--ink-soft);}
@media (max-width:480px){
  .art-f{grid-template-columns:1fr; gap:3px;}
  .art-lab{letter-spacing:.05em;}
}

/* ---------- the locator (see cardLocatorHTML in app.js) ----------
   The same window on the back of a card whose answer is a place, saying where it is. It is shorter than a
   map card's, which is the question and deserves the room; this is an annotation, so it takes about the
   height of the card's own illustration. Its failure line has to differ too — a map card's says the answer
   is below, which on the back of a card is nonsense. */
.card-loc{margin-top:20px;}
.card-loc .label{display:block; margin-bottom:7px;}
.map-card.map-loc{margin-bottom:0;}
.map-loc .mc-canvas{height:clamp(180px, 30vh, 260px);}
.map-card.map-loc.mc-failed::after{content:"This map could not be loaded.";}

/* ---------- and who fought, where the card is about a war (see cardWarKeyHTML in app.js) ----------
   The map says two sides are drawn and it cannot say WHICH is which — green and red being the one pair
   a red-green colour-blind reader has no purchase on — so the key under it names them. It carries no
   colours of its own: the swatches are written inline from `TINT_WIN` / `TINT_LOSE`, which the map is
   painted from, so a key and the map it explains cannot drift apart.
   IT WRAPS RATHER THAN SHRINKING. Two side names of any length go side by side on a laptop and stack on
   a phone; a nowrap row would ellipsise the one word on it that matters. */
.war-key{display:flex; flex-wrap:wrap; gap:6px 18px; margin-top:20px;}
/* …tucked up under the map when the map is there, and standing on its own when the Atlas popup has
   taken the globe away (see mountCardBack's `noLocator`). */
.card-loc + .war-key{margin-top:8px;}
.war-key .wk-side{display:inline-flex; align-items:baseline; gap:6px; font:400 calc(12px * var(--fs))/1.5 var(--sans); color:var(--ink-soft);}
.war-key .wk-sw{
  flex:none; width:calc(11px * var(--fs)); height:calc(11px * var(--fs)); border-radius:3px;
  border:1.5px solid; align-self:center;
}
.war-key .wk-name{font-weight:600; color:var(--ink);}
.war-key .wk-role{color:var(--ink-faint); font-size:calc(11px * var(--fs));}

/* the media frame's own chrome — the border, the rounding and the shadow every surface that shows a
   picture or a clip shares. The SHAPE is set by whoever mounts it: 16:9 here, which is what the editor's
   media slot, the deck page's sample and the picture round want, and the picture's own proportions
/* ---- THE LIBRARY BANNER IN A GLOSS POPUP (Aug 2026, on request) ----
   The shelf's banner at popup scale, below the prose and above the citations. It shares `--bk-accent`
   with `.book-tile` — the four declarations above name both, so the day/night derivation and the OKLCh
   refinement are written ONCE and a book cannot come to wear two different versions of its own colour on
   two surfaces. What it does not share is the LAYOUT: a 360px popup has no room for the shelf's two
   columns, so the title leads and the author drops to the foot beside the reading position.
   `--bk-ink` is the shelf's own compromise for a title on a washed card and is repeated here for the same
   reason: the accent alone is too light on night's paper for 14px type. */
.gloss-book{
  --bk-ink:color-mix(in srgb, var(--bk-accent) 80%, var(--ink));
  position:relative; display:flex; gap:0; width:100%; text-align:left; padding:0; overflow:hidden;
  margin:14px 0 0; clear:both;
  background:linear-gradient(105deg, color-mix(in srgb, var(--bk-accent) 26%, transparent), transparent 70%) var(--card);
  border:1px solid var(--rule); border-radius:11px; box-shadow:var(--shadow-sm);
  cursor:pointer; font:inherit; color:inherit;
  transition:transform .14s var(--ease), box-shadow .14s var(--ease), border-color .14s var(--ease);
}
body.night .gloss-book{background:linear-gradient(105deg, color-mix(in srgb, var(--bk-accent) 16%, transparent), transparent 70%) var(--card);}
.gloss-book:hover{transform:translateY(-2px); box-shadow:var(--shadow-lg); border-color:color-mix(in srgb,var(--bk-accent) 45%, var(--rule));}
.gloss-book:active{transform:translateY(0) scale(.995);}
.gloss-book:focus-visible{outline:2px solid var(--bk-accent); outline-offset:3px;}
.gloss-book .bk-spine{flex:0 0 6px; align-self:stretch; background:var(--bk-accent);}
.gb-body{flex:1; min-width:0; display:flex; flex-direction:column; gap:2px; padding:9px 12px 10px;}
.gb-eyebrow{font-family:var(--sans); font-size:calc(9px * var(--fs)); font-weight:700; letter-spacing:.09em; text-transform:uppercase; color:var(--ink-faint);}
.gb-title{font-family:var(--display); font-size:calc(14.5px * var(--fs)); font-weight:600; line-height:1.25; color:var(--bk-ink); overflow-wrap:anywhere;}
.gb-foot{display:flex; align-items:baseline; justify-content:space-between; gap:10px; margin-top:1px;}
.gb-author{font-family:var(--sans); font-size:calc(11px * var(--fs)); color:var(--ink-soft); min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.gb-where{font-family:var(--sans); font-size:calc(11px * var(--fs)); font-weight:600; color:var(--bk-accent); flex:none;}
   inside `.card-imgslot` / `.gloss-imgslot`. Click to open the viewer. */
.card-img{
  position:relative; display:block; margin:2px 0 18px; padding:0; cursor:zoom-in;
  aspect-ratio:16/9; width:100%; overflow:hidden; border-radius:10px;
  border:1px solid var(--rule); background:var(--paper-2); box-shadow:var(--shadow-sm);
}
/* CONTAIN, not cover.  Where the frame's shape is fixed and a great many illustrations are not — a
   museum portrait is 3:4 and a standing skeleton nearer 2:3 — `cover` shows one of those as a
   horizontal band across its middle: the whole picture is there and none of it is legible. Contained,
   a tall picture is whole, with paper either side. Inside the two floated slots there is no band to
   fill, the frame taking the picture's shape instead (Aug 2026, on request), and the rule is inert. */
/* A LIGHT GROUND UNDER THE PICTURE ITSELF, and it is a theme-independent literal on purpose (Aug 2026,
   found on the China cards). A great many of the corpus's illustrations are woodblock and engraved PLATES
   uploaded as PNGs with a TRANSPARENT background — black line and nothing else — so on a dark theme they
   were black ink on `--paper-2`'s near-black and the picture was a blank rectangle: nothing throws, the
   file decodes, `naturalWidth` is right, and only looking at it after dark shows anything. A `var(--paper)`
   would flip dark with the theme and fix nothing; line art is ink on PAPER, which is a fact about the
   object rather than about the reader's theme, so the colour is written out. An opaque photograph or flag
   covers it entirely and never sees it, and in the two floated slots the frame takes the picture's own
   shape, so the ground is confined to the picture. */
.card-img img{width:100%; height:100%; object-fit:contain; display:block; background:#F7F5EF; transition:transform .35s var(--ease);}
.card-img:hover img{transform:scale(1.025);}
.card-img:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.ci-zoom{
  position:absolute; right:10px; bottom:10px; width:28px; height:28px; border-radius:8px;
  display:flex; align-items:center; justify-content:center; pointer-events:none;
  background:rgba(0,0,0,.45); color:#fff; opacity:0; transition:opacity .2s var(--ease);
}
.ci-zoom svg{width:15px; height:15px;}
.card-img:hover .ci-zoom{opacity:1;}
/* a frame whose file never arrived (see the delegated `error` listener in app.js). A reader gets nothing —
   a broken illustration is worse than none, and they cannot fix it — so it leaves the flow entirely. */
.card-img.media-dead{display:none;}
/* …but in the EDITOR the frame stays and says so, because the author is the one person who can fix the link */
.ces-img.media-dead, .ces-vid.media-dead{
  display:flex; align-items:center; justify-content:center; text-align:center; padding:14px; cursor:pointer;
  border-style:dashed; border-color:color-mix(in srgb, var(--zh) 45%, var(--rule)); background:var(--paper-2);
}
.ces-img.media-dead > img, .ces-vid.media-dead > .cv-media{display:none;}
.ces-img.media-dead::after, .ces-vid.media-dead::after{
  content:"This link doesn’t load — check the URL"; font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--zh);
}
.ces-img.media-dead .ces-img-edit{opacity:1;}
/* card video: the identical frame, playing in place. The figure is not itself a button — the player owns
   clicks inside it — so the corner control is a real button and stays visible (there is no hover on touch). */
.card-vid{cursor:default;}
.card-vid .cv-media{width:100%; height:100%; display:block; border:0; background:#000;}
/* the corner control sits TOP-right, not where an image's zoom hint sits: a <video> with native controls
   owns its bottom edge, and a chip at bottom-right lands on the browser's own fullscreen button */
.card-vid .ci-zoom{opacity:.72; pointer-events:auto; border:0; padding:0; cursor:pointer; top:10px; bottom:auto;}
.card-vid:hover .ci-zoom, .card-vid .ci-zoom:focus-visible{opacity:1;}
.card-vid .ci-zoom:focus-visible{outline:2px solid #fff; outline-offset:1px;}

/* THE CARD'S MEDIA SLOT — floated to the TOP-RIGHT of the background prose, which the opening sentences
   then run down the left of (Aug 2026, on request). It is the glossary popup's `.gloss-imgslot` on a
   card, and it changes two things at once.
   THE FRAME NO LONGER LETTERBOXES. Above the prose the frame was a fixed 16:9 band and a great many
   illustrations are not — a museum portrait is 3:4 — so the picture sat in the middle of it with grey
   paper down both sides. Here the frame has no shape of its own: `aspect-ratio:auto` and `width:auto`
   make it shrink to fit whatever the picture is, capped by the slot's own width and the image's
   max-height, so a tall picture is narrow and a wide one short and neither carries bars.
   A VIDEO KEEPS 16:9, because it has no intrinsic size to shrink to — an iframe is whatever it is told
   to be — so the two frames share their chrome and part company on shape alone.
   The float must be CONTAINED: `.bg-collapse-inner` is `display:flow-root` for that, or a picture taller
   than its prose hangs out of the section and is cut off by `.bg-collapse`'s own overflow. */
.card-imgslot{float:right; max-width:44%; margin:2px 0 12px 18px;}
/* ---------- A CARD QUOTES THE BOOK IT CITES (Aug 2026, on request) ----------
   It stands between the two halves of the background, where sentence 5 ends, and it has to read as
   SOMEBODY ELSE'S WORDS inside the card's own prose — hence the rule down its inner edge and the quiet
   paper behind it rather than a border all the way round, which would read as a second card.
   `clear:both` is load-bearing: the illustration is floated right out of the first paragraph, and a
   quotation that wrapped around it would be set in a column half the card wide. */
.card-quote{
  clear:both; margin:16px 0; padding:12px 0 12px 16px;
  border-inline-start:2px solid color-mix(in srgb, var(--indigo) 42%, transparent);
  background:linear-gradient(90deg, color-mix(in srgb, var(--indigo) 5%, transparent), transparent 70%);
}
.card-quote blockquote{
  margin:0; font-family:var(--display); font-size:calc(15px * var(--fs)); line-height:1.6;
  color:var(--ink-soft); font-style:italic;
}
.card-quote blockquote p{margin:0 0 .5em;}
.card-quote blockquote p:last-child{margin-bottom:0;}
.card-quote figcaption{
  display:flex; flex-wrap:wrap; align-items:baseline; gap:4px 10px; margin-top:9px;
  font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase;
  color:var(--ink-faint);
}
.card-quote cite{font-style:normal; color:var(--ink-soft);}
.cq-who, .cq-where{white-space:nowrap;}
/* the way through to the passage. A quiet link rather than a filled button: it sits under a quotation
   inside a card's prose, and a primary control there would out-shout the words it is attached to. */
.cq-go{
  appearance:none; padding:0; border:0; background:none; cursor:pointer;
  font:inherit; letter-spacing:inherit; text-transform:inherit;
  color:var(--indigo); border-bottom:1px solid color-mix(in srgb, var(--indigo) 45%, transparent);
}
.cq-go:hover{color:var(--indigo-bright); border-bottom-color:currentColor;}
.cq-go:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px; border-radius:2px;}
@media (max-width:430px){
  .card-quote{margin:14px 0; padding-left:13px;}
  .card-quote blockquote{font-size:calc(14px * var(--fs));}
}
.card-imgslot[hidden]{display:none;}                       /* set when the file never arrives — see the `error` listener */
.card-imgslot .card-img{margin:0; aspect-ratio:auto; width:auto; max-width:100%; line-height:0;}
.card-imgslot .card-img img{width:auto; height:auto; max-width:100%; max-height:280px; object-fit:contain;}
.card-imgslot .card-vid{aspect-ratio:16/9; width:100%;}
.card-imgslot .ci-zoom{right:8px; bottom:8px; width:24px; height:24px; border-radius:7px;}
.card-imgslot .ci-zoom svg{width:13px; height:13px;}
.card-imgslot .card-vid .ci-zoom{top:8px; bottom:auto;}
@media (max-width:640px){
  /* a phone's card column is about 330px wide, so the slot gives back height as well as width: a float
     that runs deeper than a couple of sentences leaves the prose threading down a narrow gutter for the
     length of the card. The glossary sheet makes the same trade one surface over. */
  .card-imgslot{max-width:42%; margin:2px 0 10px 13px;}
  .card-imgslot .card-img img{max-height:150px;}
}
/* fullscreen image viewer */
.img-viewer{
  /* above the gloss popups (8000–9400) and the mobile gloss sheet (9600) — a term's illustration opens it from inside one */
  position:fixed; inset:0; z-index:9800; background:rgba(10,9,8,.92);
  opacity:0; transition:opacity .22s var(--ease);
  display:flex; flex-direction:column;
}
.img-viewer.show{opacity:1;}
/* A COLUMN, because the caption is inside it (Sep 2026, on request: the title, description and source
   "should stick to the bottom of the image rather than the bottom of the page"). The picture and its
   words are one centred group, so the caption is directly under the image whatever shape the image is —
   where a caption pinned to the overlay's own foot sat half a screen below a wide, short picture.
   `.iv-img` is `flex:0 1 auto` with `min-height:0` against a `flex:none` caption: a tall picture gives up
   the height the words need instead of pushing them off the bottom of the screen. `max-height` is 100% of
   what the flexbox leaves rather than 94% of the stage, the gap being the column's own `gap` now. */
.iv-stage{flex:1; min-height:0; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:12px; overflow:hidden; cursor:zoom-in; touch-action:none; padding:3vh 0;}
.iv-stage.zoomed{cursor:grab;}
.iv-stage.zoomed:active{cursor:grabbing;}
/* THE ENLARGED PICTURE SITS ON THE SAME PAPER THE CARD'S FRAME GIVES IT (Sep 2026, illustrating
   Biology). `.card-img img` has carried this ground since it was written; the viewer had none, and a
   viewer's stage is dark — so a diagram with a TRANSPARENT background, which is how Commons stores
   nearly every chemical structure, enlarged to black lines on a black screen and showed nothing at
   all. A photograph is opaque, so the ground never shows and nothing else on the site moves. */
.iv-img{max-width:94%; max-height:100%; min-height:0; flex:0 1 auto; object-fit:contain; background:#F7F5EF; transition:transform .18s var(--ease); user-select:none;}
.iv-stage.iv-live .iv-img{transition:none;}   /* a pinch or a drag moves the picture per frame; the ease would lag behind the fingers */
/* a profile photo in the viewer — round, because that is the shape it was cropped in, and capped rather
   than shown at its stored 128px, which would be barely larger than the row it was tapped in. See
   openAvatarViewer for why raising the stored size is not the answer. */
.iv-avatar{width:min(320px,74vw); height:auto; border-radius:50%;}
/* the same overlay for a video: no zoom/pan, just the biggest 16:9 the viewport allows */
.iv-vidstage{cursor:default;}
.iv-vid{width:min(94%, calc((94vh - 120px) * 16 / 9)); aspect-ratio:16/9; max-height:100%; min-height:0; flex:0 1 auto; border:0; display:block; background:#000; border-radius:8px;}
.iv-close{
  position:absolute; top:14px; right:14px; width:42px; height:42px; border-radius:50%; border:0;
  background:rgba(255,255,255,.12); color:#fff; cursor:pointer; display:flex; align-items:center; justify-content:center;
  transition:background .15s;
}
.iv-close:hover{background:rgba(255,255,255,.24);}
.iv-close svg{width:18px; height:18px;}
/* Under the picture rather than under the screen — see `.iv-stage`. It keeps its own pointer events (the
   credit is a link, and the stage's pan/zoom handlers step over it), and the gradient it used to carry is
   gone with the position: a wash rising from the foot of the overlay meant nothing once the words stopped
   being there. `flex:none` so the picture yields the height instead. */
.iv-meta{
  flex:none; max-width:min(94%, 78ch); padding:0 clamp(12px,4vw,32px); text-align:center; color:#EDE9E2;
  overflow-y:auto; max-height:34%;
}
.iv-title{font-family:var(--display); font-weight:600; font-size:calc(18px * var(--fs)); letter-spacing:-.01em; color:#fff;}
.iv-desc{font-family:var(--serif); font-size:calc(14px * var(--fs)); line-height:1.55; color:#D8D3C9; margin:6px auto 0; max-width:72ch;}
.iv-credit{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:#A39C8F; margin-top:8px;}
.iv-credit a{color:#C9C2B4;}
/* admin editor: the image field group */
.admin-imgbox{border-top:1px dashed var(--rule); margin-top:16px; padding-top:14px; display:flex; flex-direction:column; gap:12px;}
.aib-head{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; color:var(--indigo); font-weight:600;}
.aib-hint{color:var(--ink-faint); text-transform:none; letter-spacing:.02em; font-weight:400;}
.af-imgdesc{resize:vertical; min-height:44px; font:inherit;}
/* the glossary date box — a textarea rather than a single-line input, since the popup's date line may
   run to several lines (glossDatesHTML). Sized to two, growable by hand. */
.af-glossdates{resize:vertical; min-height:40px; font:inherit; line-height:1.5;}
.abstract{font-size:calc(16px * var(--fs)); line-height:1.66; color:var(--ink-soft); margin:0 0 20px;}
.abstract b, .abstract strong{color:inherit; font-weight:700;}
.abstract b.ans-term, .abstract strong.ans-term{color:var(--ink); font-weight:700;}
.abstract .ttip, .abstract .ttip b, .abstract .ttip strong{font-weight:400;}
.abstract a{color:var(--indigo-bright); text-decoration:none; border-bottom:1px solid rgba(78,76,176,.32);}
.abstract a.geo{color:var(--geo); border-bottom:1px solid rgba(78,76,176,.32); font-size:.92em;}
.night .abstract a{border-bottom-color:rgba(166,164,240,.4);}

.cite{font-family:var(--mono); font-size:calc(12.5px * var(--fs)); line-height:1.55; color:var(--ink-faint); border-top:1px solid var(--rule); padding-top:14px;}
.cite .clabel{display:block; color:var(--indigo); font-weight:600; letter-spacing:.04em; margin:0 0 9px;}
.refs{margin:0; padding-left:1.5em; list-style:decimal;}
.refs li{margin:0 0 9px; line-height:1.55; padding-left:4px;}
.refs li:last-child{margin-bottom:0;}
.refs i{font-style:italic;}
.doi{display:inline-block; margin-top:2px;}
.doi a{color:var(--indigo-bright); text-decoration:none; border-bottom:1px solid rgba(78,76,176,.3); word-break:break-all;}
.night .doi a{border-bottom-color:rgba(166,164,240,.4);}

/* reveal animation */
.reveal{overflow:hidden; display:grid; grid-template-rows:0fr; transition:grid-template-rows .44s var(--ease);}
.reveal.show{grid-template-rows:1fr;}
/* min-WIDTH:0 as well as min-height, and it is not optional: a grid item's automatic minimum size is its
   MIN-CONTENT width, so one unbreakable string anywhere inside (a citation URL is the usual culprit) widens
   the single auto column past the card and `overflow:hidden` then clips the WHOLE revealed side — answer box
   and background prose included, not just the offending line. That is the "card cut off on the right" bug.
   Every grid-collapse wrapper on the site needs the pair; cf. .tr-collapse-inner, which always had it. */
.reveal-inner{min-height:0; min-width:0;}
.reveal.show .reveal-inner{animation:fadeUp .5s var(--ease) .08s both;}
@keyframes fadeUp{from{opacity:0; transform:translateY(8px);}to{opacity:1; transform:none;}}

/* action area */
.actions{margin-top:22px;}
.reveal-cta{display:flex; justify-content:center;}
.grades{display:grid; grid-template-columns:repeat(4,1fr); gap:10px;}
/* help (?) icon to the left of the grade buttons, with hover tooltip */
.grade-wrap{display:grid; grid-template-columns:1fr auto 1fr; align-items:stretch; gap:12px;}
.grade-wrap .grade-help{justify-self:end;}
.grade-wrap .grades{justify-self:center; grid-template-columns:repeat(4, minmax(96px, 132px));}
.grade-wrap .suspendbtn{justify-self:end; align-self:center; border-radius:9px; padding:7px 11px; font-size:calc(9.5px * var(--fs)); gap:5px; white-space:nowrap;}
.grade-wrap .suspendbtn svg{width:12px; height:12px;}
/* Undo, repeated inside the grade bar. On a PHONE the study bar is a scroll away above a card that runs
   several screens, so the one control for a misclicked grade sat off screen at exactly the moment it was
   wanted; down here it is beside the ? that explains it. Desktop keeps it in the study bar only — the two
   are within a glance of each other there — so this copy is hidden above the phone breakpoint. */
.grade-wrap .gb-undo{display:none;}
.grade-help{position:relative; flex:0 0 auto; width:30px; height:30px; align-self:center; display:flex; align-items:center; justify-content:center; background:none; border:1px solid var(--rule); border-radius:50%; color:var(--ink-faint); font-family:var(--mono); font-weight:700; font-size:calc(18px * var(--fs)); line-height:1; cursor:help; transition:color .18s, border-color .18s;}
.grade-help:hover{color:var(--indigo); border-color:var(--indigo);}
.grade-help:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.grade-help-bubble{position:absolute; bottom:calc(100% + 11px); left:0; width:262px; background:#1B1A17; color:rgba(255,255,255,.88); padding:11px 13px; border-radius:9px; font-family:var(--sans); font-size:calc(12px * var(--fs)); line-height:1.5; text-align:left; box-shadow:var(--shadow-md); opacity:0; visibility:hidden; transform:translateY(5px); transition:opacity .18s var(--ease), transform .18s var(--ease), visibility .18s; pointer-events:none; z-index:5;}
.grade-help:hover .grade-help-bubble, .grade-help:focus-visible .grade-help-bubble{opacity:1; visibility:visible; transform:translateY(0);}
.grade-help-bubble::after{content:""; position:absolute; top:100%; left:13px; border:6px solid transparent; border-top-color:#1B1A17;}
.night .grade-help-bubble{background:#34343C;}
.night .grade-help-bubble::after{border-top-color:#34343C;}
.grade-help-bubble .ghb-title{display:block; font-weight:600; color:#fff; margin-bottom:7px; font-size:calc(10.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; opacity:.82;}
.grade-help-bubble .ghb-row{display:block;}
.grade-help-bubble .ghb-row + .ghb-row{margin-top:5px;}
.grade-help-bubble .ghb-row b{color:#fff; font-weight:700; margin-right:5px;}
/* the shortcuts. They existed and nothing said so — this bubble is where a reader already comes to ask
   what the buttons do, so it is where the keys that press them belong. */
.grade-help-bubble .ghb-keys{display:block; margin-top:9px; padding-top:8px; border-top:1px solid rgba(255,255,255,.16); font-size:calc(11px * var(--fs)); color:rgba(255,255,255,.7);}
kbd{font-family:var(--mono); font-size:.86em; line-height:1; padding:1.5px 5px; border-radius:4px; border:1px solid currentColor; opacity:.9; white-space:nowrap;}
.grade-help-bubble kbd{color:#fff; border-color:rgba(255,255,255,.4); background:rgba(255,255,255,.09);}
.ah-tip kbd{color:var(--ink-soft); border-color:var(--rule); background:var(--paper-2);}
.grade{
  appearance:none; cursor:pointer; border:1px solid transparent;
  border-radius:11px; padding:12px 8px 10px; text-align:center; line-height:1;
  transition:transform .12s var(--ease), box-shadow .2s, filter .2s;
  box-shadow:var(--shadow-sm); color:#fff;
}
.grade:hover{transform:translateY(-2px); box-shadow:var(--shadow-md); filter:brightness(1.07) saturate(1.04);}
.grade:active{transform:translateY(0) scale(.97);}
.grade:focus-visible{outline:2px solid var(--ink); outline-offset:2px;}
.grade .gl{display:block; font-family:var(--mono); font-size:calc(12.5px * var(--fs)); font-weight:600; letter-spacing:.06em; text-transform:uppercase; color:#fff;}
.grade .gi{display:block; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:rgba(255,255,255,.82); margin-top:6px; letter-spacing:.04em;}
.grade .gk{display:block; font-family:var(--mono); font-size:calc(9px * var(--fs)); color:rgba(255,255,255,.6); margin-top:6px;}
/* Anki-style colours: Again = red · Hard = amber · Good = green · Easy = blue */
.grade.again{background:#D9544C;}
.grade.hard{background:#DB8B3A;}
.grade.good{background:#4F9D67;}
.grade.easy{background:#4F74C2;}
.night .grade.again{background:#E26B62;}
.night .grade.hard{background:#E0A057;}
.night .grade.good{background:#5FB07E;}
.night .grade.easy{background:#6E8FD6;}

/* grade buttons pinned to the bottom of the viewport (always visible, Anki-style) */
#gradebar{
  position:fixed; left:0; right:0; bottom:0; z-index:60; pointer-events:none;
  padding:13px 0 max(13px, env(safe-area-inset-bottom));
  background:color-mix(in srgb, var(--paper) 90%, transparent);
  backdrop-filter:saturate(150%) blur(12px); -webkit-backdrop-filter:saturate(150%) blur(12px);
  border-top:1px solid var(--rule); box-shadow:0 -10px 34px rgba(27,26,23,.07);
  transform:translateY(118%); transition:transform .34s var(--ease);
}
.night #gradebar{box-shadow:0 -12px 36px rgba(0,0,0,.42);}
#gradebar.show{transform:translateY(0); pointer-events:auto;}
#gradebar .gradebar-inner{max-width:880px; margin:0 auto; padding:0 clamp(16px,4vw,40px);}
body.grading .stage{padding-bottom:150px;}
/* the CHEVRON along the bar's top edge — phone-only (Aug 2026, on request: it was a drag grip). The bar has
   exactly two heights, so a drag was a gesture whose whole range mapped onto one bit; a chevron says "there
   are two states and this is the other one" outright, and being a real <button> it needs no press
   classification and reaches a keyboard for free. Above the breakpoint the bar is one comfortable row
   already and there is nothing to reclaim, so it is not rendered at all — display:none rather than a
   second markup path. */
.gb-fold{display:none;}

/* empty / complete states */
.placard{
  background:var(--card); border:1px solid var(--rule); border-radius:14px; box-shadow:var(--shadow-md);
  padding:46px 34px; text-align:center; max-width:560px; margin:10px auto;
}
.placard .big{font-family:var(--han); font-size:calc(54px * var(--fs)); color:var(--indigo); opacity:.85; line-height:1; margin-bottom:6px;}
/* A placard's mark may be an inline SVG rather than a character — the daily "Played today" screen takes
   the game's own tile icon, so a reader is answered in the hand they pressed. It is sized to the same
   54px square the character occupies, and takes the rule's --indigo through currentColor. The stroke is
   thinned from the tiles' 2: at this size a 24-viewBox stroke of 2 draws 4.5px, which reads as a logo
   rather than as a mark. */
.placard .big svg{display:block; width:calc(54px * var(--fs)); height:calc(54px * var(--fs)); margin:0 auto; stroke-width:1.5;}
.placard h2{font-family:var(--display); font-weight:600; font-size:calc(26px * var(--fs)); margin:10px 0 8px;}
.placard p{color:var(--ink-soft); margin:0 auto 20px; max-width:42ch; font-size:calc(15.5px * var(--fs));}
.placard .row{display:flex; gap:12px; justify-content:center; flex-wrap:wrap;}
/* "Continue with <the next deck>" on the completion screen (Aug 2026, on request). It sits BELOW the three
   above it and is separated by a hairline rather than by a gap alone: the row above answers "what do I do
   with THIS session" and this one answers "what next", and without a division the four read as one row of
   choices in which the longest is the odd one out. The rule is drawn on the block rather than as a border
   on the button so it spans the placard at every text size. */
.placard .row.sc-next{margin-top:14px; padding-top:14px; border-top:1px solid var(--rule);}

/* lazy-data loading state — shown while a DATA_BUNDLES fetch is in flight (currently only the Atlas,
   whose map layers are several MB and no longer ship with the page). */
.data-loading{
  display:flex; flex-direction:column; align-items:center; gap:12px; text-align:center;
  padding:64px 24px; margin:0 auto; max-width:460px; min-height:52vh; justify-content:center;
}
.data-loading strong{font-family:var(--display); font-weight:600; font-size:calc(21px * var(--fs)); color:var(--ink);}
.data-loading .dl-note{font-family:var(--sans); font-size:calc(14px * var(--fs)); line-height:1.55; color:var(--ink-soft); max-width:34ch;}
.data-loading .btn{margin-top:6px;}
/* a wireframe globe: two rings, one spinning — the meridian sweep reads as a turning sphere */
.data-loading .dl-globe{
  width:54px; height:54px; border-radius:50%; border:2px solid var(--rule); position:relative;
  border-top-color:var(--indigo); animation:dl-spin 1.15s linear infinite;
}
.data-loading .dl-globe::before{
  content:""; position:absolute; inset:-2px; border-radius:50%;
  border:2px solid transparent; border-bottom-color:var(--gold, var(--indigo)); opacity:.55;
  animation:dl-spin 1.9s linear infinite reverse;
}
.data-loading.dl-fail .dl-globe{display:none;}
/* the determinate progress bar. It counts FILES, not bytes — byte progress would need fetch() plus
   running the text, i.e. an inline script, which script-src 'self' forbids — and it is drawn only where a
   bundle set has two or more files (see dlBarHTML), so a bar is never a lie about a single big download.
   The width TRANSITIONS, so the global reduced-motion killswitch already lands it on its true value. */
.data-loading .dl-bar{
  width:min(280px, 100%); height:5px; border-radius:99px; overflow:hidden;
  background:var(--paper-2); border:1px solid var(--rule); margin-top:2px;
}
.data-loading .dl-bar i{
  display:block; height:100%; width:0; border-radius:99px;
  background:var(--indigo); transition:width .28s var(--ease);
}
.data-loading.dl-fail .dl-bar{display:none;}
@keyframes dl-spin{to{transform:rotate(360deg);}}
@media (prefers-reduced-motion:reduce){
  .data-loading .dl-globe, .data-loading .dl-globe::before{animation:none; border-color:var(--rule); border-top-color:var(--indigo);}
}

/* ============================================================
   DAILY CHALLENGE
   ============================================================ */
.dc-shell{max-width:680px; margin:0 auto;}
/* THE CARD BEHIND A MULTIPLE-CHOICE ROUND (Sep 2026, on request) — the whole answer side, below the game
   rather than inside it. It takes `.dc-shell`'s own column so the card and the question above it share an
   edge, and a rule across the top says the game has ended and the card has begun. See PAGES.challenge. */
.mc-cardback{max-width:680px; margin:22px auto 0; padding-top:20px; border-top:1px solid var(--rule);}
.mc-cardback .study-card{margin:0;}
.race{background:var(--card); border:1px solid var(--rule); border-radius:14px; box-shadow:var(--shadow-md); padding:18px 20px; margin-bottom:18px;}
.racer{display:flex; align-items:center; gap:12px; padding:7px 0;}
.racer + .racer{border-top:1px solid var(--rule);}
.av{flex:none; width:34px; height:34px; border-radius:50%; display:grid; place-items:center; font-family:var(--display); font-weight:600; font-size:calc(15px * var(--fs)); color:#fff;}
.racer .name{font-family:var(--mono); font-size:calc(12px * var(--fs)); letter-spacing:.04em; color:var(--ink-soft); width:96px; flex:none; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.racer.you .name{color:var(--zh); font-weight:600;}
.lane{flex:1; height:8px; border-radius:99px; background:var(--paper-2); border:1px solid var(--rule); position:relative; overflow:hidden;}
.lane .car{position:absolute; left:0; top:0; bottom:0; border-radius:99px; transition:width .5s var(--ease);}
.racer .pts{font-family:var(--mono); font-size:calc(12px * var(--fs)); font-weight:600; color:var(--ink-faint); width:38px; text-align:right; flex:none;}

.dc-q{background:var(--card); border:1px solid var(--rule); border-radius:14px; box-shadow:var(--shadow-md); padding:26px 28px;}
.dc-timer{height:5px; border-radius:99px; background:var(--paper-2); overflow:hidden; margin-bottom:20px;}
.dc-timer .bar{height:100%; background:linear-gradient(90deg,var(--good),var(--ochre)); width:100%; transition:width .25s linear, background .3s;}
.dc-q .qtext{font-family:var(--serif); font-size:calc(18.5px * var(--fs)); line-height:1.55; margin:0 0 20px;}
.dc-q .qtext .blank{display:inline-block; box-sizing:border-box; width:2.7em; color:transparent; overflow:hidden; white-space:nowrap; vertical-align:baseline; border-bottom:2px solid var(--ochre); margin:0 3px; line-height:1;}
/* "Who said it?" quote */
.ws-quote{font-family:var(--serif); font-size:calc(21px * var(--fs)); line-height:1.5; font-style:italic; color:var(--ink); margin:0 0 22px; padding:2px 0 2px 18px; border-left:3px solid var(--indigo);}
.ws-quote::before{content:"“"; color:var(--indigo); font-weight:700; margin-right:1px;}
.ws-quote::after{content:"”"; color:var(--indigo); font-weight:700; margin-left:1px;}
.opts{display:grid; gap:10px;}
.opt{
  appearance:none; cursor:pointer; text-align:left; border:1px solid var(--rule); background:var(--paper);
  border-radius:11px; padding:15px 18px; font-family:var(--serif); font-size:calc(16px * var(--fs)); color:var(--ink);
  transition:transform .12s var(--ease), background .18s, border-color .18s, box-shadow .2s;
  display:flex; align-items:center; gap:13px;
}
.opt .key{font-family:var(--mono); font-size:calc(11px * var(--fs)); font-weight:600; color:var(--ink-faint); border:1px solid var(--rule); border-radius:6px; width:24px; height:24px; display:grid; place-items:center; flex:none;}
.opt:hover{background:var(--card); border-color:color-mix(in srgb,var(--indigo) 35%,var(--rule)); transform:translateY(-1px); box-shadow:var(--shadow-sm);}
.opt:active{transform:scale(.99);}
.opt:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.opt.correct{background:color-mix(in srgb,var(--good) 16%, var(--card)); border-color:var(--good); color:var(--good);}
.opt.correct .key{border-color:var(--good); color:var(--good);}
.opt.wrong{background:var(--zh-wash); border-color:var(--zh); color:var(--zh);}
.opt.wrong .key{border-color:var(--zh); color:var(--zh);}
.opt[disabled]{cursor:default;}
.dc-meta{display:flex; justify-content:space-between; align-items:center; margin-bottom:14px; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint);}

/* True or False minigame */
.tf-pips{display:flex; gap:7px; margin:0 0 16px;}
.tf-pip{flex:1; max-width:44px; height:6px; border-radius:99px; background:var(--rule); transition:background .2s;}
.tf-pip.cur{background:color-mix(in srgb,var(--indigo) 55%, var(--rule));}
.tf-pip.ok{background:var(--good);}
.tf-pip.no{background:var(--zh);}
.tf-opts{display:flex; gap:12px; margin-top:6px;}
.tf-opts .opt{flex:1; justify-content:center; font-family:var(--display); font-weight:600; font-size:calc(18px * var(--fs));}
.tf-reveal{margin-top:18px; border-top:1px solid var(--rule); padding-top:16px;}
.tf-verdict{font-family:var(--display); font-weight:600; font-size:calc(16px * var(--fs)); margin-bottom:8px;}
.tf-verdict.ok{color:var(--good);}
.tf-verdict.no{color:var(--zh);}
.tf-why{font-family:var(--serif); font-size:calc(15px * var(--fs)); line-height:1.6; color:var(--ink-soft); margin:0 0 18px;}
.tf-reveal .btn{width:100%;}
/* the Sources fold under an explanation keeps its distance from the button below it (Sep 2026, on
   request: the header sat almost on top of "See results") */
.tf-reveal .src-note{margin-bottom:18px;}
.tf-summary{display:flex; flex-direction:column; gap:14px; margin-bottom:22px;}
.tf-sum-row{display:flex; gap:13px; align-items:flex-start;}
.tf-sum-mark{flex:none; width:24px; height:24px; border-radius:99px; display:grid; place-items:center; font-weight:700; font-size:calc(13px * var(--fs)); color:#fff; margin-top:2px;}
.tf-sum-mark.ok{background:var(--good);}
.tf-sum-mark.no{background:var(--zh);}
.tf-sum-q{font-family:var(--serif); font-weight:600; font-size:calc(15.5px * var(--fs)); line-height:1.45; margin:0 0 4px;}
.tf-sum-a{font-size:calc(13px * var(--fs)); line-height:1.55; color:var(--ink-soft); margin:0;}
/* what happens next, now that nothing on this screen offers another go (Aug 2026, with the one-play-a-day
   rule). It is a line rather than a disabled button: the answer to "can I play again" is a fact about the
   calendar, and a greyed control invites a reader to keep pressing it. */
.tf-tomorrow{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase;
  color:var(--ink-faint); text-align:center; margin:0 0 16px;}
.tf-actions{display:flex; gap:10px;}
.tf-actions .btn{flex:1;}

/* Common Thread — the daily grouping puzzle (PAGES.thread).
   The board is ONE grid holding both states: a solved row spans all four columns (grid-column:1/-1) and
   sits above the tiles still in play, so a row gathering itself is the grid closing up rather than two
   separate lists. The four group colours are fixed hex mixed into var(--card), so every theme and both
   modes get a wash of the same weight without a rule of its own. */
.th-lives{display:flex; align-items:center; gap:9px; margin:0 0 14px; min-height:20px;}
.th-lives-label{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.09em; text-transform:uppercase; color:var(--ink-faint);}
.th-dots{display:flex; gap:6px;}
.th-dot{width:9px; height:9px; border-radius:99px; background:var(--indigo); transition:background .25s, transform .25s var(--ease);}
.th-dot.gone{background:var(--rule); transform:scale(.7);}
.th-board{display:grid; grid-template-columns:repeat(4,1fr); gap:8px; margin-bottom:18px;}
.th-tile{
  appearance:none; cursor:pointer; border:1px solid var(--rule); background:var(--card); color:var(--ink);
  border-radius:11px; padding:8px 6px; min-height:74px; display:grid; place-items:center; text-align:center;
  font-family:var(--serif); font-size:calc(13px * var(--fs)); line-height:1.25; hyphens:auto; overflow-wrap:anywhere;
  transition:transform .12s var(--ease), background .18s, border-color .18s, box-shadow .2s;
}
.th-tile:hover{border-color:color-mix(in srgb,var(--indigo) 35%,var(--rule)); box-shadow:var(--shadow-sm); transform:translateY(-1px);}
.th-tile:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.th-tile.on{background:var(--indigo); border-color:var(--indigo); color:#fff; transform:translateY(-1px);}
.th-tile[disabled]{cursor:default;}
.th-tile.shake{animation:thShake .42s var(--ease);}
@keyframes thShake{
  0%,100%{transform:translateX(0);} 20%{transform:translateX(-5px);} 40%{transform:translateX(5px);}
  60%{transform:translateX(-3px);} 80%{transform:translateX(3px);}
}
.th-solved{
  grid-column:1 / -1; border-radius:11px; padding:11px 14px; text-align:center;
  background:color-mix(in srgb, var(--th-c) 20%, var(--card)); border:1px solid color-mix(in srgb, var(--th-c) 55%, var(--rule));
  animation:thPop .3s var(--ease);
}
@keyframes thPop{from{opacity:0; transform:scale(.97);} to{opacity:1; transform:none;}}
.th-solved-name{
  display:block; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.11em;
  text-transform:uppercase; font-weight:600; color:color-mix(in srgb, var(--th-c) 72%, var(--ink)); margin-bottom:5px;
}
.th-solved-terms{display:flex; flex-wrap:wrap; justify-content:center; gap:4px 10px;}
/* a solved term opens its glossary popup — a control, so it reads as one rather than as plain text */
.th-term{
  appearance:none; background:none; border:0; padding:0; cursor:pointer; color:var(--ink);
  font-family:var(--serif); font-size:calc(14.5px * var(--fs)); line-height:1.35;
  border-bottom:1px dotted color-mix(in srgb, var(--th-c) 60%, var(--rule)); transition:color .15s, border-color .15s;
}
.th-term:hover{color:var(--indigo); border-bottom-color:var(--indigo);}
.th-term:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px; border-radius:3px;}
.th-g0{--th-c:#C2793A;} .th-g1{--th-c:#4F74C2;} .th-g2{--th-c:#4F9D67;} .th-g3{--th-c:#8257C2;}
/* WHICH GROUPS THE READER ACTUALLY FOUND (Sep 2026, on request). `finish` reveals the unsolved groups by
   pushing them onto the same list, so without this every band looks identical and a reader who found one
   of four sees the same screen as one who found all four. A found band keeps its full colour; a missed one
   is washed back towards the paper and its terms go quiet, so the four read at a glance as two kinds.
   THE COLOUR IS NEVER THE ONLY SIGNAL — `.th-verdict` says which in words, for the reader who cannot see
   the difference and for the one on a screen that flattens it. */
.th-solved.th-missed{
  background:color-mix(in srgb, var(--th-c) 7%, var(--card));
  border-color:color-mix(in srgb, var(--th-c) 26%, var(--rule));
  border-style:dashed;
}
.th-solved.th-missed .th-solved-name{color:var(--ink-soft);}
.th-solved.th-missed .th-term{color:var(--ink-soft); border-bottom-color:color-mix(in srgb, var(--th-c) 30%, var(--rule));}
.th-solved.th-missed .th-term:hover{color:var(--indigo); border-bottom-color:var(--indigo);}
.th-verdict{
  margin-left:9px; font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.09em;
  font-weight:600; white-space:nowrap;
}
.th-got .th-verdict{color:color-mix(in srgb, var(--th-c) 78%, var(--ink));}
.th-missed .th-verdict{color:var(--ink-faint);}
.th-actions{display:flex; gap:10px;}
.th-actions .btn{flex:1;}
.th-msg{color:var(--ink-soft); margin:-4px 0 18px; font-family:var(--serif); font-size:calc(15px * var(--fs));}
/* A four-column grid on a 390px phone leaves each tile about 80px wide, so the type steps down with it —
   the names are glossary head words and a few run to the 24-character cap the generator enforces. */
@media (max-width:560px){
  .th-board{gap:6px;}
  .th-tile{min-height:66px; font-size:calc(11.5px * var(--fs)); padding:6px 4px; border-radius:9px;}
  .th-term{font-size:calc(13px * var(--fs));}
}

/* ============================================================
   CROSSWORD (PAGES.crossword)
   ============================================================
   The grid is ONE `display:grid` of square cells — `aspect-ratio:1` on the square rather than a fixed px
   size, so the whole board scales with the column count and with the screen and never needs a breakpoint
   of its own. The letter is a real `<input>`: it is what brings up a keyboard on a phone, and it carries
   the caret, the selection and the tab order for free. Its font-size is NOT scaled by --fs — a text-size
   setting that grew the letters would overflow squares whose size is set by the grid's width. */
.xw-shell{max-width:760px;}
.xw-grid{display:grid; gap:2px; margin:0 auto 18px; width:100%;}
.xw-block{aspect-ratio:1; min-width:0;}
.xw-sq{position:relative; aspect-ratio:1; min-width:0; background:var(--card); border:1px solid var(--rule); border-radius:3px;}
.xw-sq.on{background:color-mix(in srgb, var(--indigo) 14%, var(--card)); border-color:color-mix(in srgb, var(--indigo) 42%, var(--rule));}
.xw-n{position:absolute; top:1px; left:2px; font-family:var(--mono); font-size:9px; line-height:1; color:var(--ink-faint); pointer-events:none; z-index:1;}
.xw-cell{
  appearance:none; width:100%; min-width:0; height:100%; border:0; background:transparent; border-radius:3px;
  text-align:center; text-transform:uppercase; padding:0;
  font-family:var(--display); font-weight:600; font-size:clamp(13px, 3.4vw, 20px); color:var(--ink); caret-color:var(--indigo);
}
/* the focused square keeps a real ring, drawn INSIDE it (offset -2) so 65 of them cannot shift the grid —
   dropping the outline for the background tint alone would leave a keyboard solver with no caret to follow */
.xw-cell:focus{outline:2px solid var(--indigo-bright); outline-offset:-2px; background:color-mix(in srgb, var(--indigo) 26%, var(--card));}
/* An entry marks itself the moment its last square is typed (Aug 2026, on request), so these two are no
   longer the verdict on a spent grid but live feedback on one being worked: green is an answer that is
   right and settled — its squares are readOnly, so the wash also says "this letter is not yours to change"
   — and red is an answer that is wrong and can simply be typed over. The washes matter as much as the ink:
   a whole answer changing colour at once is what makes it read as a WORD being judged rather than as five
   letters that happen to have gone green. `.on` keeps its own highlight where a square is neither. */
.xw-cell.ok{color:var(--good); font-weight:700;}
.xw-cell.bad{color:var(--zh);}
.xw-sq:has(.xw-cell.bad){background:var(--zh-wash); border-color:color-mix(in srgb,var(--zh) 40%,var(--rule));}
/* The green is mixed at the same strength the red's own `--zh-wash` token is (measured: ~10% of `--zh`
   over white), so neither verdict shouts louder than the other — a right answer and a wrong one are the
   same KIND of statement about a word, and the difference between them should be the hue and nothing
   else. `:has()` takes the specificity of its argument, so both of these outrank `.xw-sq.on`'s highlight
   and a judged square keeps its verdict while it is the one being typed in. */
.xw-sq:has(.xw-cell.ok){background:color-mix(in srgb,var(--good) 11%,var(--card)); border-color:color-mix(in srgb,var(--good) 45%,var(--rule));}
/* a clue whose answer is in: struck through and quietened, so the list says what is left to do */
.xw-clue.done .xw-ct{color:var(--ink-faint); text-decoration:line-through; text-decoration-color:color-mix(in srgb,var(--good) 70%,transparent);}
.xw-clue.done .xw-cn{color:var(--good);}
/* the clue for the entry being typed, pinned above the grid — on a phone the clue lists are below the fold */
.xw-cur{
  min-height:44px; background:var(--paper-2); border:1px solid var(--rule); border-radius:11px;
  padding:10px 14px; margin-bottom:12px; font-family:var(--serif); font-size:calc(14.5px * var(--fs)); line-height:1.45;
}
.xw-cur-n{display:block; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.11em; text-transform:uppercase; color:var(--ink-faint); margin-bottom:3px;}
.xw-actions{display:flex; flex-wrap:wrap; gap:10px; margin-bottom:18px;}   /* three buttons since Give up joined them (Aug 2026) — at 390px they wrap rather than squeezing "Clear the wrong letters" to one word a line */
.xw-actions .btn{flex:1;}
.xw-clues{display:grid; grid-template-columns:1fr 1fr; gap:22px; margin-top:20px;}
.xw-h{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint); margin:0 0 10px; font-weight:600;}
.xw-list{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:12px;}
/* a real <button> (see clueHTML), so it needs the reset as well as the look */
.xw-clue{
  appearance:none; border:0; background:none; color:inherit; font:inherit; width:100%; text-align:left;
  display:flex; gap:9px; cursor:pointer; border-radius:8px; padding:5px 7px; margin:-5px -7px; transition:background .15s;
}
.xw-clue:hover{background:var(--paper-2);}
.xw-clue:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:1px;}
.xw-clue.on{background:color-mix(in srgb, var(--indigo) 12%, var(--paper-2));}
.xw-cn{flex:none; font-family:var(--mono); font-weight:600; font-size:calc(12px * var(--fs)); color:var(--ink-faint); min-width:16px;}
.xw-ct{font-family:var(--serif); font-size:calc(14.5px * var(--fs)); line-height:1.5;}
/* the clue is a card's question, so it carries the study card's own blank */
.xw-ct .blank, .xw-cur .blank{display:inline-block; box-sizing:border-box; width:2.7em; color:transparent; overflow:hidden; white-space:nowrap; vertical-align:baseline; border-bottom:2px solid var(--ochre); margin:0 3px; line-height:1;}
/* the enumeration, which is what makes the dropped hyphens and diacritics honest rather than a trap */
.xw-len{font-family:var(--mono); font-size:calc(11.5px * var(--fs)); color:var(--ink-faint);}
@media (max-width:560px){
  .xw-clues{grid-template-columns:1fr; gap:18px;}
  .xw-grid{gap:1.5px;}
  .xw-n{font-size:8px;}
}

/* ============================================================
   PICTURE ROUND (PAGES.picture)
   ============================================================
   `contain`, never `cover`: the round is to name what is in the picture, and cropping the sides off a map,
   a diagram or a wide landscape can take the answer out of the frame. The plate under it is the paper
   rather than the card, so a picture with a white ground still reads as a picture. */
.pic-frame{margin:0 0 20px; background:var(--paper-2); border:1px solid var(--rule); border-radius:12px; overflow:hidden; display:grid; place-items:center; min-height:200px;}
.pic-img{display:block; width:100%; max-height:46vh; object-fit:contain;}
/* a link that has rotted — there is no upload path here, so every picture is somebody else's URL */
/* THE PICTURE IS ENLARGEABLE ONLY ONCE THE ANSWER IS OUT (Sep 2026, on request). The class is added by
   the reveal, never by the markup, and it carries the cursor and the zoom mark alone — `.card-img`, the
   frame the rest of the site enlarges from, would also impose its 16:9 box and reshape the picture under
   the reader at the moment of the reveal. */
.pic-frame.pic-open{position:relative; cursor:zoom-in;}
.pic-frame.pic-open:hover .ci-zoom{opacity:1;}
.pic-frame.pic-open:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
/* the artefact's own five sentences, and the works they rest on, under the reveal */
.pic-note{margin-bottom:10px;}
.pic-frame.pic-dead::after{content:"This picture didn’t load"; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink-faint); padding:40px 16px;}
.pic-frame.pic-dead .pic-img{display:none;}
.pic-cap{font-family:var(--display); font-weight:600; font-size:calc(15px * var(--fs)); line-height:1.35; margin:0 0 6px;}
/* what THIS picture shows, under what the subject IS (Aug 2026, on request) — the third line of the
   reveal, so quieter than the title above it; a caption set as loud as the title reads as a second one. */
.pic-shows{font-size:calc(11.5px * var(--fs)); line-height:1.45; color:var(--ink-faint); margin:0 0 6px;}
/* `.pic-credit` — the picture's attribution line under the fold — was REMOVED in Sep 2026, on request:
   it is already on the enlarged picture, which is where a reader who wants it goes. The 16px of clearance
   it gave the Next button goes on the button instead, so the spacing holds whether the block above it is
   the caption, the sources fold or the paragraph. */
#picReveal .btn{margin-top:10px;}

/* ============================================================
   WHAT YEAR? (PAGES.whatyear)
   ============================================================
   The rail is a native range input over a lattice of ticks, so the snap, the drag, the arrow keys and
   Home/End all come from the browser. The two `.wy-out` bands are the spans a wrong guess has ruled out:
   they stay DRAWN rather than being cut off the rail, so the scale never changes under the reader between
   one guess and the next — only the reachable part of it does. */
.wy-events{list-style:none; margin:0 0 22px; padding:0; display:flex; flex-direction:column; gap:9px;}
.wy-events li{display:flex; align-items:baseline; gap:11px; font-family:var(--serif); font-size:calc(17px * var(--fs)); line-height:1.4; color:var(--ink);}
.wy-dot{flex:none; width:7px; height:7px; border-radius:99px; background:var(--tile, var(--indigo)); opacity:.75;}
.wy-rail{background:var(--card); border:1px solid var(--rule); border-radius:14px; box-shadow:var(--shadow-md); padding:20px 22px 16px; margin-bottom:16px;}
/* the year and the two chevrons that step it (see the note in PAGES.whatyear). The readout keeps
   `flex:1` so it stays centred in the rail whatever the two buttons weigh, and the row carries the
   margin the readout used to. */
.wy-readrow{display:flex; align-items:center; gap:6px; margin-bottom:16px;}
.wy-readout{font-family:var(--display); font-weight:600; font-size:calc(30px * var(--fs)); line-height:1.1; text-align:center; color:var(--ink); flex:1; min-width:0;}
.wy-step{appearance:none; flex:none; width:40px; height:40px; border-radius:11px; border:1px solid var(--rule); background:var(--card); color:var(--ink-soft); cursor:pointer; display:flex; align-items:center; justify-content:center; transition:background .15s var(--ease), color .15s var(--ease), border-color .15s var(--ease);}
.wy-step svg{width:19px; height:19px;}
.wy-step:hover:not(:disabled){border-color:var(--indigo); color:var(--indigo);}
.wy-step:disabled{opacity:.35; cursor:default;}
.wy-step:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.wy-track{position:relative; padding:10px 0;}
/* the ruled-out spans: `--ink-faint` rather than `--rule`, which is the track's own border colour and left
   them all but invisible against it — a band the reader cannot see is a narrowing they are not told about */
.wy-out{position:absolute; top:16px; height:8px; background:var(--ink-faint); opacity:.32; border-radius:99px; pointer-events:none; z-index:1;}
.wy-out-lo{left:0;} .wy-out-hi{right:0;}
/* a mark is a guess already made, so it sits INSIDE the span that guess ruled out — it has to be stacked
   above the grey band, and the thumb above both, or the rail hides the two things it is reporting */
.wy-marks{position:absolute; inset:0; pointer-events:none; z-index:2;}
.wy-mark{position:absolute; top:11px; width:2px; height:18px; margin-left:-1px; background:var(--zh); border-radius:2px;}
/* where the answer was, drawn only once the puzzle is over (see paint) — taller and green, so a reader can
   see at a glance how far out each guess was */
.wy-answer{position:absolute; top:7px; width:3px; height:26px; margin-left:-1.5px; background:var(--good); border-radius:2px;}
.wy-range{appearance:none; width:100%; background:transparent; margin:0; display:block; position:relative; z-index:3;}
.wy-range::-webkit-slider-runnable-track{height:8px; border-radius:99px; background:var(--paper-2); border:1px solid var(--rule);}
.wy-range::-moz-range-track{height:8px; border-radius:99px; background:var(--paper-2); border:1px solid var(--rule);}
.wy-range::-webkit-slider-thumb{appearance:none; width:24px; height:24px; border-radius:50%; background:var(--indigo); border:2px solid var(--card); box-shadow:var(--shadow-sm); margin-top:-9px; cursor:pointer;}
.wy-range::-moz-range-thumb{width:24px; height:24px; border-radius:50%; background:var(--indigo); border:2px solid var(--card); box-shadow:var(--shadow-sm); cursor:pointer;}
.wy-range:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:4px; border-radius:99px;}
.wy-range[disabled]::-webkit-slider-thumb{background:var(--ink-faint); cursor:default;}
.wy-range[disabled]::-moz-range-thumb{background:var(--ink-faint); cursor:default;}
.wy-ends{display:flex; justify-content:space-between; font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-faint); margin-top:6px;}
.wy-tries{display:flex; align-items:center; gap:9px; margin:0 0 14px; min-height:20px;}
.wy-actions{display:flex; gap:10px;}
.wy-actions .btn{flex:1;}
@media (max-width:560px){
  .wy-events li{font-size:calc(15.5px * var(--fs));}
  .wy-readout{font-size:calc(26px * var(--fs));}
}

/* results */
.podium{display:flex; flex-direction:column; gap:10px; margin:18px 0;}
.prow{display:flex; align-items:center; gap:14px; padding:14px 18px; border:1px solid var(--rule); border-radius:12px; background:var(--card);}
.prow.you{border-color:var(--zh); background:var(--zh-wash);}
.prow .rank{font-family:var(--display); font-weight:900; font-size:calc(22px * var(--fs)); width:30px; color:var(--ink-faint);}
.prow.first .rank{color:var(--ochre);}
.prow .pname{font-family:var(--serif); font-weight:600; font-size:calc(17px * var(--fs)); flex:1;}
.prow .pscore{font-family:var(--mono); font-weight:600; font-size:calc(15px * var(--fs)); color:var(--indigo);}

/* ============================================================
   ACCOUNT
   ============================================================ */
.profile{display:flex; align-items:center; gap:20px; background:var(--card); border:1px solid var(--rule); border-radius:14px; box-shadow:var(--shadow-md); padding:24px 26px; margin-bottom:20px;}
.monogram{flex:none; width:74px; height:74px; border-radius:18px; display:grid; place-items:center; background:var(--indigo); color:#fff; font-family:var(--display); font-weight:600; font-size:calc(32px * var(--fs));}
.profile .who{flex:1; min-width:0;}
.namefield{font-family:var(--display); font-weight:600; font-size:calc(26px * var(--fs)); color:var(--ink); background:none; border:0; border-bottom:1.5px dashed transparent; padding:2px 0; width:100%; max-width:340px; transition:border-color .2s;}
.namefield:hover{border-bottom-color:var(--rule);}
.namefield:focus{outline:none; border-bottom-color:var(--indigo);}
.profile .since{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint); margin-top:6px;}

.statgrid{display:grid; grid-template-columns:repeat(auto-fit,minmax(130px,1fr)); gap:14px; margin-bottom:22px;}
.statcard{background:var(--card); border:1px solid var(--rule); border-radius:12px; padding:18px 18px; box-shadow:var(--shadow-sm);}
.statcard b{font-family:var(--display); font-weight:600; font-size:calc(30px * var(--fs)); color:var(--indigo); line-height:1; display:block;}
.statcard.zh b{color:var(--zh);}
.statcard span{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint); display:block; margin-top:8px;}

.section-label{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.18em; text-transform:uppercase; color:var(--indigo); font-weight:600; margin:26px 0 12px;}
/* ---------- THE COLLECTIONS PAGE'S TAB BAR (Sep 2026, on request) ----------
   Six tabs deciding which of the page's shelves is drawn — see COLLECTION_TABS in app.js. It is a filter
   over one page rather than six pages, so it is styled as a segmented row rather than as the site's own
   navigation: the top bar's `.tab` is a destination and these are a view of where you already are, and
   giving them the same treatment would say they were the same kind of control.
   IT SCROLLS SIDEWAYS ON A PHONE rather than wrapping to two lines. Six words do not fit on a 390px
   screen, and a bar that is sometimes one line and sometimes two moves everything under it as the reader
   taps along it. `scrollbar-width:none` because the row is short enough to swipe and a scrollbar under six
   words is louder than the words; the overflow is still real, so a keyboard and a screen reader reach the
   far end normally. */
.coll-tabs{
  display:flex; gap:6px; margin:0 0 18px; padding-bottom:2px;
  overflow-x:auto; scrollbar-width:none; -webkit-overflow-scrolling:touch;
}
.coll-tabs::-webkit-scrollbar{display:none;}
/* …AND THE ROW SAYS THAT IT SCROLLS. With the scrollbar hidden there was no cue at all that four
   visible chips were not all of them. A mask fades the last 22px of the track wherever the content
   overflows it — `mask-image` on the scroll container follows its own scroll position, so the fade
   sits at whichever end still has content beyond it rather than being painted on at a fixed place.
   It is a progressive nicety: a browser without mask support simply gets the row as before. */
@supports (mask-image: linear-gradient(#000, #000)) {
  .coll-tabs{
    mask-image:linear-gradient(to right, #000 calc(100% - 22px), transparent 100%);
    -webkit-mask-image:linear-gradient(to right, #000 calc(100% - 22px), transparent 100%);
  }
  /* above the breakpoint every chip fits, so the fade would be a mark with nothing behind it */
  @media (min-width:641px){ .coll-tabs{mask-image:none; -webkit-mask-image:none;} }
}
.coll-tab{
  appearance:none; flex:none; cursor:pointer; white-space:nowrap;
  padding:7px 14px; border-radius:999px; border:1px solid var(--rule); background:var(--card);
  font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase;
  font-weight:600; color:var(--ink-faint);
  transition:color .15s var(--ease), border-color .15s var(--ease), background .15s var(--ease);
}
.coll-tab:hover{color:var(--ink); border-color:var(--indigo);}
.coll-tab:focus-visible{outline:none; border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash);}
.coll-tab.on{background:var(--indigo); border-color:var(--indigo); color:#fff;}
/* the first section under the bar has the bar's margin above it already */
.coll-tabs + .collection-group > .group-head{margin-top:0;}
.coll-tab-empty{margin-top:8px;}

/* ---------- review statistics: study-history heatmap, retention, due forecast ---------- */
.revstats{display:grid; grid-template-columns:1fr 1fr; gap:14px;}
.rs-heat{grid-column:1 / -1;}                     /* the heatmap wants the full width; retention + forecast share the row below */
.rs-card{background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm); padding:16px 18px 15px; display:flex; flex-direction:column; gap:11px; min-width:0;}
.rs-head{display:flex; align-items:baseline; justify-content:space-between; gap:12px;}
.rs-head h3{font-family:var(--display); font-weight:600; font-size:calc(16px * var(--fs)); color:var(--ink); margin:0;}
.rs-meta{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint); text-align:right;}
.rs-sub{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); line-height:1.5; color:var(--ink-soft);}
/* CARDS FIGHTING YOU (Sep 2026). The lapse count LEADS, in the tabular figure, because the list is
   ordered by it and a reader is scanning that column rather than reading the titles. A card past
   SCHED.leech takes the warning ochre — the scheduler records a leech and deliberately never acts on
   one, so this is the only place the flag is ever shown. */
/* THE READER'S OWN NOTE (Sep 2026). Quiet at rest — one small button under the answer — and a plain
   field once opened. `[hidden]` needs saying because the author display beats the UA rule, the trap
   `.ces-imgpanel` and `.af-src` already carry. */
/* RECALL IN FULL (Sep 2026): the box under the question, and what was written in it shown above the
   answer at the reveal. The written copy is a `<p>` with textContent set, never innerHTML — it is the
   reader's own typing and has no business being parsed. */
/* CARRY ON FROM ANOTHER DEVICE (Sep 2026). A quiet full-width row above the day's review, in the
   indigo the site uses for "somewhere to go" rather than in a colour that competes with the banner. */
/* READY BY A DATE (Sep 2026). The number is the answer, so it leads; the caveats are the point of the
   feature and are set as prose under it rather than as a footnote. */
/* the plan sits on the sheet's own 14px gutter, as the date row above it does (Oct 2026, on a bug report: the
   headline and the warning ran to the box's edge while the note between them was indented) */
.dl-out{margin:2px 0 4px; padding:0 14px;}
.dl-out .dm-note{margin-left:0; margin-right:0;}
.dm-field .dm-num.dl-date{width:auto; min-width:150px;}   /* a date needs room for all of dd/mm/yyyy and its picker */
.dl-big{margin:0 0 4px; font-family:var(--display); font-weight:600; font-size:calc(21px * var(--fs)); color:var(--ink);}
.dm-warn{margin:6px 0 0; font-family:var(--sans); font-size:calc(12px * var(--fs)); line-height:1.5; color:var(--geo);}
/* THE KEYBOARD SHEET (Sep 2026). One overlay on the body, so `render()` closes it like every other;
   above the tab bar and the grade bar, below the image viewer, which is the stack every modal here
   sits in. It is never reachable on a phone and needs no rule for that — there is no keyboard. */
.key-sheet{position:fixed; inset:0; z-index:9600; display:flex; align-items:center; justify-content:center; padding:24px; background:color-mix(in srgb, #000 46%, transparent); backdrop-filter:blur(2px); -webkit-backdrop-filter:blur(2px);}
.ks-card{position:relative; max-width:520px; width:100%; max-height:82vh; overflow:auto; background:var(--card); border:1px solid var(--rule); border-radius:14px; box-shadow:var(--shadow-lg, 0 18px 50px rgba(0,0,0,.28)); padding:22px 24px 20px;}
.ks-card h2{font-family:var(--display); font-weight:600; font-size:calc(19px * var(--fs)); color:var(--ink); margin:0 0 14px;}
.ks-group{margin:0 0 16px;}
.ks-group h3{font-family:var(--sans); font-size:calc(10.5px * var(--fs)); letter-spacing:.09em; text-transform:uppercase; color:var(--ink-quiet); margin:0 0 7px;}
.ks-row{display:flex; align-items:baseline; gap:12px; padding:3px 0;}
.ks-row kbd{flex:none; min-width:5.6em; font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink); background:var(--paper-2); border:1px solid var(--rule); border-bottom-width:2px; border-radius:5px; padding:2px 6px; text-align:center;}
.ks-row span{font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink-soft);}
.ks-none{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-quiet); margin:0;}
.ks-x{position:absolute; top:10px; right:12px; width:28px; height:28px; border:0; background:none; color:var(--ink-quiet); font-size:calc(20px * var(--fs)); line-height:1; cursor:pointer; border-radius:6px;}
.ks-x:hover{color:var(--ink); background:var(--paper-2);}
.handoff-row{display:block; width:100%; text-align:left; margin:0 0 12px; padding:12px 15px; border:1px solid color-mix(in srgb, var(--indigo) 34%, var(--rule)); border-radius:12px; background:var(--card); box-shadow:var(--shadow-sm); cursor:pointer;}
.handoff-row:hover{border-color:var(--indigo);}
.ho-lead{display:block; font-family:var(--display); font-weight:600; font-size:calc(14.5px * var(--fs)); color:var(--indigo);}
.ho-sub{display:block; margin-top:2px; font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-soft);}
.freerecall{margin:14px 0 2px; border-top:1px solid var(--rule); padding-top:12px;}
.fr-lab{display:block; font-family:var(--sans); font-size:calc(10.5px * var(--fs)); letter-spacing:.09em; text-transform:uppercase; color:var(--ink-quiet); margin-bottom:6px;}
.fr-ta{display:block; width:100%; box-sizing:border-box; font-family:var(--sans); font-size:calc(13.5px * var(--fs)); line-height:1.55; color:var(--ink); background:var(--paper-2); border:1px solid var(--rule); border-radius:8px; padding:9px 11px; resize:vertical;}
.fr-ta:focus{outline:none; border-color:var(--indigo);}
.fr-said{margin:0 0 14px; padding:10px 12px; border-left:3px solid var(--ink-quiet); background:var(--paper-2); border-radius:0 8px 8px 0;}
.fr-said .label{display:block; font-family:var(--sans); font-size:calc(10px * var(--fs)); letter-spacing:.09em; text-transform:uppercase; color:var(--ink-quiet); margin-bottom:4px;}
.fr-said p{margin:0; font-family:var(--sans); font-size:calc(13px * var(--fs)); line-height:1.55; color:var(--ink-soft); white-space:pre-wrap;}
.cardnote{margin:10px 0 2px;}
.cn-body[hidden]{display:none !important;}
.cn-open{display:inline-flex; align-items:center; gap:5px; font-family:var(--sans); font-size:calc(11px * var(--fs)); letter-spacing:.02em; color:var(--ink-quiet); background:none; border:0; padding:2px 0; cursor:pointer;}
.cn-open svg{width:13px; height:13px; opacity:.8;}
.cn-open:hover{color:var(--ink);}
.cardnote.cn-has .cn-open{color:var(--geo);}
.cn-ta{display:block; width:100%; margin-top:6px; box-sizing:border-box; font-family:var(--sans); font-size:calc(13px * var(--fs)); line-height:1.5; color:var(--ink); background:var(--paper-2); border:1px solid var(--rule); border-radius:8px; padding:8px 10px; resize:vertical; min-height:calc(52px * var(--fs));}
.cn-ta:focus{outline:none; border-color:var(--indigo);}
.lc-list{display:flex; flex-direction:column; gap:1px; max-height:300px; overflow:auto;}
.lc-row{display:flex; align-items:baseline; gap:10px; width:100%; text-align:left; padding:5px 7px; border:0; border-radius:5px; background:none; cursor:pointer; font:inherit;}
.lc-row:hover{background:var(--paper-2);}
.lc-n{font-family:var(--mono); font-size:calc(11.5px * var(--fs)); font-variant-numeric:tabular-nums; color:var(--ink-soft); min-width:2.2em; text-align:right;}
.lc-row.lc-bad .lc-n{color:var(--geo); font-weight:700;}
/* "Right, but slowly" reuses the leech rows' geometry exactly: two lists of cards, in the same card,
   doing the same thing when pressed. What differs is the FIGURE — seconds rather than lapses, so it is
   wider (a "12.5s" needs more room than a "7") and it is the QUIET ink, never the alarm red the leech
   list spends on a card past `SCHED.leech`. A slow card is not a failing card. */
.sc-n{min-width:3.4em; color:var(--ink-soft);}

/* A PICTURE HELD BACK UNDER "Use less data" KEEPS ITS FRAME (see cardImageHTML). It is the same 16:9 box
   the picture will fill, in the page's own quiet paper with a dashed edge — so the card does not resize
   under the reader when they ask for it, and an empty frame that says why can never be mistaken for a
   card that simply has no illustration. */
.card-img.ci-held{background:var(--paper-2); border-style:dashed;}
.card-img.ci-held img{display:none;}
.card-img.ci-held .ci-zoom{display:none;}
.ci-hold{position:absolute; inset:0; display:flex; align-items:center; justify-content:center; padding:0 16px; text-align:center;
  font-family:var(--sans); font-size:calc(12px * var(--fs)); color:var(--ink-faint); letter-spacing:.02em;}

/* THE MAP IN WORDS (see mapNeighbours). Under the globe rather than over it — it is a caption, not a
   label — in the quiet ink the picture credits use, and `[hidden]` is honoured explicitly because the
   slot ships in the markup and is only filled once the shapes land. */
.mc-alt{margin:0; padding:9px 13px 11px; border-top:1px solid var(--rule); background:var(--paper-2);
  font-family:var(--sans); font-size:calc(12.5px * var(--fs)); line-height:1.5; color:var(--ink-soft);}
.mc-alt[hidden]{display:none;}

/* YOU AND THEM (see versusHTML). A three-column grid rather than a table: the label takes what is left
   and the two figures sit in fixed columns, so four rows line up down the page whatever the numbers are.
   The LEADING figure is marked in the ink the site already uses for a win — never a colour of its own,
   and never both, which would make a draw look like two wins. */
.vs-card{padding:14px 16px 12px;}
.vs-head,.vs-row{display:grid; grid-template-columns:1fr 5.4em 5.4em; align-items:baseline; gap:8px;}
.vs-head{padding-bottom:7px; margin-bottom:5px; border-bottom:1px solid var(--rule);}
.vs-head .vs-a,.vs-head .vs-b{font-family:var(--sans); font-size:calc(10.5px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-quiet);}
.vs-row{padding:5px 0;}
.vs-row + .vs-row{border-top:1px solid var(--rule-soft, var(--rule));}
.vs-lbl{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-soft); min-width:0;}
.vs-a,.vs-b{font-family:var(--mono); font-size:calc(13px * var(--fs)); font-variant-numeric:tabular-nums; text-align:right; color:var(--ink-soft);}
.vs-row.vs-mine .vs-a,.vs-row.vs-theirs .vs-b{color:var(--good); font-weight:700;}
@media (max-width:430px){ .vs-head,.vs-row{grid-template-columns:1fr 4.4em 4.4em; gap:6px;} }

/* A button that reads as part of the sentence it sits in — used for "copy link" on the account page's
   identity line, where a real `.btn` would be a control shouting beside three quiet facts. It keeps a
   focus ring and a real hit area; only the chrome goes. */
.link-btn{font:inherit; color:inherit; background:none; border:0; padding:2px 1px; margin:0; cursor:pointer; text-decoration:underline; text-underline-offset:2px; text-decoration-thickness:1px; opacity:.85;}
.link-btn:hover{opacity:1;}
/* …and a chip that is a STATE rather than an action (a pending friend request), which must not look
   pressable: `.mini-btn` gives it the shape and this takes the affordance away. */
.mini-btn.static{cursor:default; opacity:.7;}
.u-private{max-width:52ch;}
.lc-t{flex:1; min-width:0; font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.lc-w{font-family:var(--sans); font-size:calc(10.5px * var(--fs)); color:var(--ink-quiet); white-space:nowrap; max-width:32%; overflow:hidden; text-overflow:ellipsis;}
@media (max-width:520px){ .lc-w{display:none;} }
.rs-big{font-family:var(--display); font-weight:600; font-size:calc(44px * var(--fs)); line-height:1; color:var(--indigo); display:block;}
.rs-big i{font-style:normal; font-size:calc(22px * var(--fs)); margin-left:2px; color:var(--ink-faint);}
.rs-big.rs-none{color:var(--ink-faint);}
.rs-ret{justify-content:flex-start;}

/* heatmap — whole weeks in columns, Monday at the top. Scrolls inside its own box on narrow screens
   so a year of squares can never push the page sideways. */
.hm-scroll{overflow-x:auto; overflow-y:hidden; margin:0 -2px; padding:0 2px 4px;}
.hm-inner{display:inline-flex; flex-direction:column; gap:4px; min-width:100%;}
.hm-months, .hm-grid{display:grid; grid-auto-flow:column; grid-auto-columns:11px; gap:3px;}
.hm-grid{grid-template-rows:repeat(7,11px);}
.hm-mon{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.04em; color:var(--ink-faint); white-space:nowrap; overflow:visible;}
.hm-cell{width:11px; height:11px; border-radius:2.5px; background:var(--paper-2); border:1px solid color-mix(in srgb, var(--rule) 60%, transparent);}
/* days before the account existed: they hold the Monday-first column alignment but must not read as missed days */
.hm-cell.hm-pre{background:none; border-color:transparent;}
.hm-1{background:color-mix(in srgb, var(--indigo) 26%, var(--paper-2)); border-color:transparent;}
.hm-2{background:color-mix(in srgb, var(--indigo) 50%, var(--paper-2)); border-color:transparent;}
.hm-3{background:color-mix(in srgb, var(--indigo) 74%, var(--paper-2)); border-color:transparent;}
.hm-4{background:var(--indigo); border-color:transparent;}
.hm-legend{display:flex; align-items:center; gap:5px; flex-wrap:wrap; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; color:var(--ink-faint);}
.hm-legend i{display:inline-block;}
.hm-best{margin-left:auto; text-align:right;}

/* forecast — one bar per day, today first and marked */
.fc-bars{display:flex; align-items:flex-end; gap:5px; height:96px; padding-top:4px;}
.fc-bar{flex:1; min-width:0; display:flex; flex-direction:column; align-items:center; justify-content:flex-end; height:100%; gap:5px;}
.fc-bar i{display:block; width:100%; max-width:22px; border-radius:3px 3px 0 0; background:color-mix(in srgb, var(--indigo) 42%, var(--paper-2)); transition:height .35s var(--ease);}
.fc-bar.fc-today i{background:var(--zh);}
.fc-bar em{font-family:var(--mono); font-style:normal; font-size:calc(9px * var(--fs)); letter-spacing:.04em; color:var(--ink-faint); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%;}
/* "Today" is wider than one narrow weekday letter — let it overhang its column rather than clip to "To…" */
.fc-bar.fc-today em{color:var(--zh); font-weight:600; overflow:visible; max-width:none;}
.fc-bar.fc-zero i{height:2px !important; background:var(--rule);}   /* nothing due — a flat tick keeps the axis continuous */
/* THE HORIZON PICKER AND THE PEAK (Sep 2026). The peak is the whole reason to look at 90 or 180 days —
   a term's Easy grades landing in one week is what makes people quit — so it is marked in the warning
   ochre rather than left as the tallest of forty identical bars. At 180 days the bars are 2px wide, so
   the gap closes with them or the chart is more gap than chart. */
.fc-bar.fc-peak i{background:var(--geo);}
.fc-pick{display:flex; gap:2px;}
.fc-pick .fc-h{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.04em; padding:2px 6px; border:1px solid var(--rule); background:none; color:var(--ink-faint); cursor:pointer; border-radius:3px;}
.fc-pick .fc-h:hover{color:var(--ink); border-color:var(--ink-faint);}
.fc-pick .fc-h.on{color:var(--paper); background:var(--indigo); border-color:var(--indigo);}
.rs-fc .fc-bars:has(.fc-bar:nth-child(30)){gap:2px;}
.rs-fc .fc-bars:has(.fc-bar:nth-child(30)) .fc-bar{gap:3px;}
/* On a WEEKLY chart the tick is a month name over a 10px bar, so it has to be allowed out of its own
   box — there is only one per month and they sit four bars apart, which is room enough. */
.rs-fc .fc-bars:has(.fc-bar:nth-child(22)) .fc-bar em{overflow:visible; max-width:none;}

/* answer buttons — one bar per grade, in the grade bar's OWN four colours so the two surfaces cannot come
   to disagree about which colour "Hard" is. It is the forecast's shape deliberately: the two cards sit in
   one grid and a second bar idiom on the same page would read as two unrelated charts. */
.ab-bars{display:flex; align-items:flex-end; gap:8px; height:104px; padding-top:4px;}
.ab-bar{flex:1; min-width:0; display:flex; flex-direction:column; align-items:center; justify-content:flex-end; height:100%; gap:4px;}
/* the bar's own track — see the note where the markup is built: a percentage height needs a containing
   block that the labels are OUTSIDE of, or the tallest bar grows over the word beneath it */
.ab-track{flex:1 1 auto; min-height:0; width:100%; display:flex; align-items:flex-end; justify-content:center;}
.ab-bar i{display:block; width:100%; max-width:34px; border-radius:3px 3px 0 0; background:var(--rule); transition:height .35s var(--ease);}
.ab-bar b{font-family:var(--mono); font-weight:600; font-size:calc(10px * var(--fs)); letter-spacing:.04em; color:var(--ink-soft);}
.ab-bar em{font-family:var(--mono); font-style:normal; font-size:calc(9.5px * var(--fs)); letter-spacing:.04em; color:var(--ink-faint); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%;}
.ab-g1 i{background:#D9544C;}
.ab-g2 i{background:#DB8B3A;}
.ab-g3 i{background:#4F9D67;}
.ab-g4 i{background:#4F74C2;}
.night .ab-g1 i{background:#E26B62;}
.night .ab-g2 i{background:#E0A057;}
.night .ab-g3 i{background:#5FB07E;}
.night .ab-g4 i{background:#6E8FD6;}
.ab-bar.ab-zero i{height:2px !important; background:var(--rule);}   /* never pressed — the flat tick, as above */
.ab-bar.ab-zero b{color:var(--ink-faint);}
@media (max-width:720px){
  .revstats{grid-template-columns:1fr;}
  .hm-best{margin-left:0; width:100%;}
}

/* card types — the list of card templates a type declares. The picker and the name sit side by side on a
   wide editor and stack on a narrow one; the two buttons are their own row so "Remove this card", which is
   the destructive one, is never adjacent to the select a finger is aiming at. */
.ut-cards{display:grid; grid-template-columns:1fr 1fr; gap:10px 14px; align-items:end; margin:2px 0 4px; padding:11px 0 12px; border-top:1px solid var(--rule); border-bottom:1px solid var(--rule);}
.ut-cards .admin-field{margin:0;}
.ut-cardacts{grid-column:1 / -1; display:flex; flex-wrap:wrap; gap:8px; align-items:center;}
@media (max-width:720px){
  .ut-cards{grid-template-columns:1fr;}
}

/* ---------- card info: the state block and the per-review history table ----------
   Both live inside a `.dm-box` sheet, which is why the widths below are caps rather than fixed sizes: the
   sheet is centred at every screen width and the table has to be able to scroll inside it rather than
   stretch it (a seven-column table on a 390px phone is wider than the phone). */
/* Wider than the ordinary options sheet, because a seven-column history table in 400px is a table read
   entirely by scrolling. The class goes on the OVERLAY (deckSheet owns the box) — see openCardInfo.
   It is also the one sheet that can outgrow the screen: an options sheet is four rows, and this is a state
   block plus however many reviews a card has. So the BOX is capped and the TABLE is the part that gives —
   `flex:1 1 auto; min-height:0` on the wrap, which is what lets a flex item shrink below its content — so
   the state block and the Close button stay put and the history scrolls between them. Without the cap a
   card with a year of reviews runs off both ends of a phone, taking Close with it. */
/* `vh` first and `dvh` after it: on a phone `vh` ignores the browser's own retracting toolbars, and a
   browser that does not know `dvh` simply drops the second line and keeps the first. */
.deck-menu.ci-sheet .dm-box{width:min(580px,94vw); max-height:calc(100vh - 32px); max-height:calc(100dvh - 32px);}
.deck-menu.ci-sheet .ci-histwrap{flex:1 1 auto; min-height:0;}
.ci-grid{display:grid; grid-template-columns:auto 1fr; gap:6px 14px; align-items:baseline; padding:4px 14px 6px;}
.ci-k{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint); white-space:nowrap;}
.ci-v{font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink); min-width:0; overflow-wrap:anywhere;}
.ci-due-now{color:var(--zh); font-weight:600;}
.ci-leech{color:var(--zh);}
.ci-id{font-family:var(--mono); font-size:calc(11.5px * var(--fs)); color:var(--ink-soft);}
.ci-of{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; color:var(--ink-faint); margin-left:6px;}
.ci-of.ci-solo{margin-left:0;}   /* a note standing in for a figure (the collection info sheet) has nothing to sit beside */
.ci-h{font-family:var(--display); font-weight:600; font-size:calc(14px * var(--fs)); color:var(--ink); margin:6px 0 0; padding:11px 14px 0; border-top:1px solid var(--rule);}

/* The actions row inside Card info — flag, set due date, forget, suspend. It wraps rather than scrolling,
   because four ghost buttons at Very large text do not fit one 390px line and a row that scrolls sideways
   hides whichever action happens to be last. */
.ci-acts{display:flex; flex-wrap:wrap; gap:8px; padding:8px 14px 2px;}
.ci-acts .btn{font-size:calc(12px * var(--fs)); padding:6px 12px;}

/* The flag picker's swatch row. The number is printed ON the swatch, so the seven are told apart by
   position and figure as well as by hue — which is what makes it usable to a reader who cannot separate
   red from green, and is why the keyboard shortcut is the same figure. */
.fl-row{display:flex; flex-wrap:wrap; gap:8px; padding:10px 14px 4px;}
.fl-sw{display:flex; flex-direction:column; align-items:center; gap:4px; padding:7px 9px; border:1px solid var(--rule); border-radius:10px; background:var(--card); cursor:pointer; transition:border-color .16s var(--ease), background .16s var(--ease);}
.fl-sw .flag-dot{width:calc(16px * var(--fs)); height:calc(16px * var(--fs));}
.fl-sw .fl-n{font-family:var(--mono); font-size:calc(10px * var(--fs)); color:var(--ink-faint);}
.fl-sw:hover{border-color:color-mix(in srgb,var(--flag) 55%,var(--rule)); background:color-mix(in srgb,var(--flag) 8%,var(--card));}
.fl-sw.sel{border-color:var(--flag); background:color-mix(in srgb,var(--flag) 14%,var(--card));}
.fl-sw.sel .fl-n{color:var(--ink);}
.ci-none{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); line-height:1.55; color:var(--ink-soft); margin:0; padding:7px 14px 2px;}
.ci-histwrap{overflow-x:auto; overflow-y:auto; max-height:44vh; padding:7px 14px 2px;}   /* the cap above is what makes this a floor rather than the whole rule */
table.ci-hist{border-collapse:collapse; width:100%; font-family:var(--sans); font-size:calc(11.5px * var(--fs)); white-space:nowrap;}
table.ci-hist th{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint); font-weight:600; text-align:left; padding:0 10px 5px 0; position:sticky; top:0; background:var(--card);}
table.ci-hist td{padding:4px 10px 4px 0; border-top:1px solid var(--rule); color:var(--ink); vertical-align:baseline;}
table.ci-hist tr td:first-child{color:var(--ink-soft);}
.ci-st{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint);}
.ci-st-3{color:var(--indigo);}
/* the rating, in the grade bar's own colours — see the answer-buttons note above */
.ci-g{font-weight:600;}
.ci-g-1{color:#B8433C;} .ci-g-2{color:#A9682A;} .ci-g-3{color:#3D7A50;} .ci-g-4{color:#3D5C9C;}
.night .ci-g-1{color:#E26B62;} .night .ci-g-2{color:#E0A057;} .night .ci-g-3{color:#5FB07E;} .night .ci-g-4{color:#6E8FD6;}
.ci-sum{font-family:var(--sans); font-size:calc(12px * var(--fs)); color:var(--ink-soft); margin:0; padding:6px 14px 0;}
/* the Info button in the study bar wears the same quiet chrome as Undo beside it */
.infobtn svg{opacity:.85;}
/* …and so does the Flag button, until the card HAS a flag, at which point the button becomes the one place
   on the study page that says so — the flag is invisible otherwise, and a marker nobody can see mid-review
   is a marker nobody trusts. The colour is on the mark and the label, never a fill: this bar is four small
   controls in a row and a filled one would read as the selected tab of something. */
/* THE TIME BOX (Sep 2026). It says "Time" at rest and counts down once set, so the bar reports how
   long is left without being opened — which is the only reason to spend a study-bar cell on it. */
.boxbtn svg{opacity:.85;}
.boxbtn.on{color:var(--geo);}
.boxbtn.on svg{opacity:1;}
.flagbtn svg{opacity:.85;}
.flagbtn.on{color:var(--flag);}
.flagbtn.on svg{opacity:1; fill:color-mix(in srgb,var(--flag) 30%,transparent);}
.ci-flag{display:inline-flex; align-items:center; gap:7px;}

/* ---------- deck statistics: a picker over collections/decks and the panel it drives ---------- */
.ds-pick{display:flex; align-items:center; gap:12px; margin-bottom:12px; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint);}
.ds-pick .set-sel{flex:1; min-width:0; max-width:420px;}
.ds-panel{background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm); padding:16px 18px 15px; display:flex; flex-direction:column; gap:14px;}
.ds-bar-row{display:flex; align-items:center; gap:14px; flex-wrap:wrap;}
.ds-bar{flex:1; min-width:140px; height:8px; border-radius:5px; background:var(--paper-2); border:1px solid color-mix(in srgb, var(--rule) 70%, transparent); overflow:hidden;}
.ds-bar i{display:block; height:100%; background:var(--indigo); border-radius:5px; transition:width .4s var(--ease);}
.ds-bar-lbl{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-soft);}
.ds-tiles{display:grid; grid-template-columns:repeat(auto-fit,minmax(104px,1fr)); gap:10px;}
.ds-tile{background:var(--paper-2); border:1px solid color-mix(in srgb, var(--rule) 70%, transparent); border-radius:10px; padding:11px 12px; min-width:0;}
.ds-tile b{font-family:var(--display); font-weight:600; font-size:calc(23px * var(--fs)); line-height:1; color:var(--indigo); display:block;}
.ds-tile span{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint); display:block; margin-top:7px;}
.ds-foot{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-soft); border-top:1px solid var(--rule); padding-top:11px;}
.ds-foot b{color:var(--ink);}
.ds-empty{font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink-faint); background:var(--card); border:1px solid var(--rule); border-radius:12px; padding:18px 20px;}

/* ---------- PAGE: the glossary terms this reader has discovered (PAGES.glossary) ---------- */
.gl-meter{background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm); padding:14px 18px 16px; display:flex; flex-direction:column; gap:9px; margin-bottom:16px;}
.gl-meter-head{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint);}
.gl-meter-head b{font-family:var(--display); font-size:calc(20px * var(--fs)); letter-spacing:0; text-transform:none; color:var(--ink); margin-right:4px;}
.gl-meter .ds-bar{width:100%; flex:none;}
/* filter on the left, order on the right — one row, so a narrow column drops the picker under the
   field rather than squeezing both */
.gl-tools{display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-bottom:12px;}
.gl-tools[hidden]{display:none;}   /* the author display beats the UA rule — cf. .ces-imgpanel */
.gl-tools .gl-search{flex:1 1 220px; margin-bottom:0;}
.gl-search{margin-bottom:12px;}
.gl-search input{width:100%; max-width:340px; font-family:var(--sans); font-size:calc(13.5px * var(--fs)); padding:9px 12px; border:1px solid var(--rule); border-radius:9px; background:var(--card); color:var(--ink); transition:border-color .16s var(--ease), box-shadow .16s var(--ease);}

/* The sort picker shared by the glossary record and the Library shelf. A native <select>: the options
   are a short closed set, and on a phone the platform's own picker beats a row of buttons taking a
   line the list needs. */
.sort-pick{display:inline-flex; align-items:center; gap:7px; flex:none;}
.sort-pick-label{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint);}
.sort-pick select{
  font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink);
  background:var(--card); border:1px solid var(--rule); border-radius:9px; padding:8px 10px; cursor:pointer;
  transition:border-color .16s var(--ease), box-shadow .16s var(--ease);
}
.sort-pick select:focus-visible{outline:none; border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash);}
/* The reverse control beside it (Aug 2026, on request). It carries the direction IN WORDS — "A – Z",
   "Oldest first" — because an arrow alone makes the reader translate, and the same direction is called
   different things by different fields. The arrows flip with .rev so the icon and the words agree. */
.sort-dir{
  appearance:none; cursor:pointer; display:inline-flex; align-items:center; gap:6px; flex:none;
  padding:7px 11px; border:1px solid var(--rule); border-radius:9px; background:var(--card); color:var(--ink-soft);
  font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.06em;
  transition:border-color .16s var(--ease), color .16s var(--ease), background .16s var(--ease);
}
.sort-dir svg{width:13px; height:13px; flex:none; transition:transform .18s var(--ease);}
.sort-dir.rev svg{transform:scaleY(-1);}
.sort-dir:hover{border-color:var(--indigo); color:var(--ink);}
.sort-dir:focus-visible{outline:none; border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash);}
.gl-search input:focus{outline:none; border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash);}
.gl-list{background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm); overflow:hidden;}
.gl-row{display:flex; align-items:baseline; gap:14px; width:100%; text-align:left; appearance:none; background:none; border:0; border-bottom:1px solid color-mix(in srgb,var(--rule) 60%,transparent); padding:12px 18px; cursor:pointer; font:inherit; color:inherit; transition:background .14s var(--ease), padding-left .14s var(--ease);}
.gl-row:last-child{border-bottom:0;}
.gl-row:hover{background:var(--indigo-wash); padding-left:23px;}
.gl-row:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}
.gl-name{font-family:var(--serif); font-weight:600; font-size:calc(15px * var(--fs)); color:var(--ink); flex:1; min-width:0;}
.gl-dates{font-family:var(--mono); font-weight:400; font-size:calc(10.5px * var(--fs)); color:var(--ink-faint); margin-left:8px; white-space:nowrap;}
.gl-tags{display:flex; gap:5px; flex:none; max-width:44%; overflow:hidden;}
.gl-tag{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint); background:var(--paper-2); border:1px solid color-mix(in srgb,var(--rule) 70%,transparent); border-radius:99px; padding:2px 7px; white-space:nowrap;}
.gl-when{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.05em; color:var(--ink-faint); flex:none; width:92px; text-align:right;}
.gl-empty{padding:34px 24px; text-align:center;}
.gl-empty h3{font-family:var(--display); font-weight:600; font-size:calc(19px * var(--fs)); margin:0 0 8px;}
.gl-empty p{font-family:var(--sans); font-size:calc(13.5px * var(--fs)); line-height:1.6; color:var(--ink-faint); max-width:46ch; margin:0 auto 16px;}
@media (max-width:640px){
  /* the tags are the first thing to go at 390px — the name and when it was read are what a reader scans */
  .gl-tags{display:none;}
  .gl-row{padding:11px 14px; gap:10px;}
  .gl-row:hover{padding-left:18px;}
  .gl-when{width:74px;}
  .gl-search input{max-width:none;}
}

/* ---------- "beyond the cards": glossary/Atlas reading and the daily games ---------- */
.ex-meter .ds-bar{width:100%; flex:none;}
.ex-tiles{margin-top:14px;}
/* the glossary meter is a way IN to the terms it counts (PAGES.glossary). Only that one is a button, so
   the arrow is what distinguishes it from the Atlas meter beside it. */
.ex-meter-link{position:relative; text-align:left; cursor:pointer; font:inherit; color:inherit; padding-right:34px; transition:border-color .16s var(--ease), box-shadow .16s var(--ease), transform .16s var(--ease);}
.ex-meter-link:hover{border-color:var(--indigo); box-shadow:var(--shadow-md); transform:translateY(-1px);}
.ex-meter-link:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.ex-go{position:absolute; right:16px; top:16px; font-family:var(--sans); font-size:calc(15px * var(--fs)); color:var(--indigo); transition:transform .16s var(--ease);}
.ex-meter-link:hover .ex-go{transform:translateX(3px);}
.ex-games{display:grid; grid-template-columns:repeat(auto-fit,minmax(210px,1fr)); gap:0 22px; background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm); padding:6px 18px; margin-top:14px;}
.ex-grow{display:flex; align-items:baseline; gap:10px; padding:9px 0; border-bottom:1px solid color-mix(in srgb, var(--rule) 60%, transparent);}
.ex-grow:last-child{border-bottom:0;}
.exg-name{font-family:var(--serif); font-weight:600; font-size:calc(14px * var(--fs)); flex:1; min-width:0;}
.exg-n, .exg-w{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint); white-space:nowrap;}
.exg-w{color:var(--indigo);}
@media (max-width:720px){
  .ds-pick{flex-direction:column; align-items:stretch; gap:6px;}
  .ds-pick .set-sel{max-width:none;}
}

.deckprog{background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm); overflow:hidden;}
.dp-row{display:flex; align-items:center; gap:16px; padding:15px 20px;}
.dp-row + .dp-row{border-top:1px solid var(--rule);}
.dp-name{font-family:var(--serif); font-weight:600; font-size:calc(16px * var(--fs)); width:180px; flex:none;}
.dp-name small{display:block; font-family:var(--mono); font-weight:400; font-size:calc(10px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint); margin-top:3px;}

/* ---- accounts: auth, friends, roles, admin ---- */
.ghost-btn{flex:none; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink); background:var(--card); border:1px solid var(--rule); border-radius:9px; padding:9px 14px; cursor:pointer; transition:background .15s,border-color .15s;}
.ghost-btn:hover{background:var(--paper-2); border-color:var(--ink-soft);}
.auth-btn{font-family:var(--mono); font-size:calc(12px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:#fff; background:var(--indigo); border:0; border-radius:10px; padding:12px 18px; cursor:pointer; transition:filter .15s;}
.auth-btn:hover{filter:brightness(1.08);}
.auth-btn:disabled{opacity:.6; cursor:progress; filter:none;}
.auth-btn.sm{padding:9px 14px; font-size:calc(11px * var(--fs));}
.mini-btn{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink); background:var(--paper-2); border:1px solid var(--rule); border-radius:7px; padding:6px 10px; cursor:pointer;}
.mini-btn:hover{border-color:var(--ink-soft);}
.mini-btn.ok{background:var(--indigo); color:#fff; border-color:transparent;}
.mini-btn.danger:hover,.mini-btn.armed{color:#fff; background:var(--danger,#c8453c); border-color:transparent;}
.role-badge{display:inline-block; font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; padding:2px 7px; border-radius:6px; vertical-align:middle;}
.role-badge.role-admin{background:var(--indigo); color:#fff;}
.role-badge.role-user{background:var(--paper-2); color:var(--ink-faint); border:1px solid var(--rule);}

.auth-card{background:var(--card); border:1px solid var(--rule); border-radius:14px; box-shadow:var(--shadow-md); padding:8px 26px 24px; max-width:460px;}
.auth-tabs{display:flex; gap:4px; border-bottom:1px solid var(--rule); margin:0 -26px 20px; padding:0 18px;}
.auth-tab{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint); background:none; border:0; border-bottom:2px solid transparent; padding:14px 10px; cursor:pointer;}
.auth-tab.active{color:var(--indigo); border-bottom-color:var(--indigo);}
.auth-form{display:flex; flex-direction:column; gap:13px;}
.auth-form[hidden],.acct-panel[hidden]{display:none;}
.auth-form label{display:flex; flex-direction:column; gap:6px; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint);}
.auth-in{font-family:var(--sans); font-size:calc(15px * var(--fs)); color:var(--ink); background:var(--paper-2); border:1px solid var(--rule); border-radius:9px; padding:11px 13px;}
.auth-in:focus{outline:none; border-color:var(--indigo); background:var(--card);}
.auth-msg{font-family:var(--sans); font-size:calc(13px * var(--fs));}
.auth-msg.err{color:var(--danger,#c8453c);}
.auth-msg.ok{color:#3a8a52;}
.auth-note{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-faint); margin:-2px 0 2px; line-height:1.5;}
.auth-foot{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-faint); margin-top:18px; line-height:1.5;}
.recovery-code{font-family:var(--mono); font-size:calc(22px * var(--fs)); letter-spacing:.18em; color:var(--indigo); background:var(--paper-2); border:1px dashed var(--rule); border-radius:10px; padding:14px; text-align:center; margin:14px 0; user-select:all;}
.acct-tools{display:flex; gap:10px; margin:-6px 0 8px; flex-wrap:wrap;}
.acct-panel{background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm); padding:18px 20px; margin-bottom:14px; display:flex; flex-direction:column; gap:11px; max-width:460px;}
/* The account's own actions, inside the profile card (Aug 2026, on request) — they act on the same thing,
   and the sync line under them says what happens to it.
   A 2×2 GRID, not a flex row (Aug 2026, on a second request). Four buttons side by side ran to the width
   of the card on a desktop and squeezed the name, the handle and the joined date into what was left, so
   the block that IDENTIFIES the account had the least room in it. Two columns of two is the same four
   controls in half the width, and the two columns are `1fr` so the buttons are one size rather than four
   sized by their own words. It stays a flex row on a phone, where the card stacks (see the ≤640 block). */
.acct-idacts{display:grid; grid-template-columns:repeat(2, minmax(0, 1fr)); gap:8px; flex:none; align-content:center;}
.acct-idacts .ghost-btn{width:100%; justify-content:center; text-align:center;}
.acct-syncnote{margin:-14px 0 16px; padding-left:2px;}
/* THE ACCOUNT SWITCHER (Aug 2026, on request). A grid rather than a flex row, so the Forget × sits in a
   column of its own and every row's arrow lines up down the list however long the names are. */
.acct-switch{display:grid; grid-template-columns:1fr auto; gap:6px 4px; max-width:460px; margin:0 0 14px;}
.acct-swrow{display:flex; align-items:center; gap:11px; width:100%; text-align:left; padding:10px 12px;
  background:var(--card); border:1px solid var(--rule); border-radius:11px; cursor:pointer; color:inherit;
  transition:border-color .16s var(--ease), background .16s var(--ease);}
.acct-swrow:hover{border-color:var(--indigo); background:var(--paper-2);}
.acct-swrow:disabled{opacity:.6; cursor:default;}
.acct-swrow .monogram{flex:none;}
.asw-who{display:flex; flex-direction:column; gap:1px; min-width:0; flex:1;}
.asw-who b{font-family:var(--display); font-size:calc(14.5px * var(--fs)); color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.asw-at{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:var(--indigo);}
.asw-mail{font-family:var(--sans); font-size:calc(11.5px * var(--fs)); color:var(--ink-faint); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.asw-go{flex:none; font-size:calc(15px * var(--fs)); color:var(--ink-faint);}
.acct-swforget{align-self:center; background:none; border:0; cursor:pointer; padding:6px 8px; border-radius:8px;
  font-size:calc(15px * var(--fs)); line-height:1; color:var(--ink-faint);}
.acct-swforget:hover{color:var(--zh); background:var(--paper-2);}
.acct-current{font-family:var(--sans); font-size:calc(13.5px * var(--fs)); color:var(--ink-faint);}
.acct-current b{color:var(--ink); font-family:var(--mono); font-size:calc(12.5px * var(--fs)); word-break:break-all;}

.friends-box{background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm); padding:16px 18px; margin-bottom:8px;}
.friend-add{display:flex; gap:8px;}
.friend-add .auth-in{flex:1;}
.friend-msg{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); margin-top:8px;}
.friend-msg.err{color:var(--danger,#c8453c);} .friend-msg.ok{color:#3a8a52;}
.friend-sub{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint); margin:16px 0 8px;}
.friend-row{display:flex; align-items:center; gap:10px; padding:8px 0;}
.friend-row + .friend-row{border-top:1px solid var(--rule);}
.friend-link{flex:1; min-width:0; display:flex; align-items:center; gap:11px; background:none; border:0; padding:4px; border-radius:9px; cursor:pointer; text-align:left; color:var(--ink);}
.friend-link:not(.static):hover{background:var(--paper-2);}
.friend-link.static{cursor:default;}
/* A FRIEND'S BANNER IN THE THEME THEY WEAR (Aug 2026, on request — a theme is how an account presents
   itself, not only a preference for its owner's own screen). It borrows the theme's own paper as a wash
   and its accent as a left bar, and NOTHING ELSE: a nested `data-theme` would put that theme's whole
   stylesheet — its fonts, its ornament, its chrome — onto a row inside a page drawn in the reader's own,
   which is why the colours arrive as `--ft-*` custom properties instead.
   The wash is a MIX toward the card rather than the paper colour flat, so a light theme worn by a friend
   is still legible on a dark one and the row never fights `--ink`. `folio` is skipped outright: it is the
   default everyone starts on, and washing every unthemed friend would say something the row does not
   mean. */
.friend-link[data-ftheme]:not([data-ftheme="folio"]){
  background:color-mix(in srgb, var(--ft-p) 16%, var(--card));
  box-shadow:inset 3px 0 0 var(--ft-a);
  padding-inline-start:9px;
}
body.night .friend-link[data-ftheme]:not([data-ftheme="folio"]){background:color-mix(in srgb, var(--ft-a) 15%, var(--card));}
.friend-link[data-ftheme]:not([data-ftheme="folio"]):not(.static):hover{background:color-mix(in srgb, var(--ft-p) 26%, var(--card));}
body.night .friend-link[data-ftheme]:not([data-ftheme="folio"]):not(.static):hover{background:color-mix(in srgb, var(--ft-a) 24%, var(--card));}
/* the same treatment one size up, on the profile box a friend's page opens with */
.profile.friend-profile[data-ftheme]:not([data-ftheme="folio"]){
  background:color-mix(in srgb, var(--ft-p) 20%, var(--card));
  border-inline-start:5px solid var(--ft-a);
}
body.night .profile.friend-profile[data-ftheme]:not([data-ftheme="folio"]){background:color-mix(in srgb, var(--ft-a) 16%, var(--card));}
.ft-wearing{color:var(--ink-faint);}
.monogram.sm{width:38px; height:38px; border-radius:11px; font-size:calc(16px * var(--fs));}
/* profile photo variant of the monogram + the change-photo button on the account page */
.monogram.has-img{background:var(--paper-2); overflow:hidden; padding:0;}
.monogram.has-img img{width:100%; height:100%; object-fit:cover; display:block; border-radius:inherit;}
.mono-btn{appearance:none; background:none; border:0; padding:0; cursor:pointer; position:relative; flex:none; border-radius:18px;}
.mono-btn:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px;}
.mono-cam{position:absolute; right:-5px; bottom:-5px; width:24px; height:24px; border-radius:50%; background:var(--card); border:1px solid var(--rule); display:flex; align-items:center; justify-content:center; color:var(--ink-faint); opacity:0; transition:opacity .15s ease; box-shadow:var(--shadow-sm);}
.mono-btn:hover .mono-cam, .mono-btn:focus-visible .mono-cam{opacity:1;}
.mono-cam svg{width:13px; height:13px;}
.friend-name{font-family:var(--serif); font-weight:600; font-size:calc(15px * var(--fs));}
.friend-name small{font-family:var(--mono); font-weight:400; font-size:calc(10px * var(--fs)); letter-spacing:.04em; color:var(--ink-faint); margin-left:4px;}
.friend-acts{display:flex; gap:6px; flex:none;}
.friend-empty{font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink-faint); padding:6px 4px;}
.friend-title{font-family:var(--display); font-weight:600; font-size:calc(26px * var(--fs)); color:var(--ink);}
.back-link{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--indigo); background:none; border:0; cursor:pointer; padding:4px 0; margin-bottom:14px;}

/* feedback-mode: the readers' message queue takes over the admin area (as the old Accounts tab did) */
.admin.feedback-mode .admin-tree,.admin.feedback-mode .admin-side-foot,.admin.feedback-mode .admin-collapse,.admin.feedback-mode .admin-editor,.admin.feedback-mode .admin-search,.admin.feedback-mode .alt-right,.admin.feedback-mode .alt-right-gloss,.admin.feedback-mode .admin-sel-bar,.admin.feedback-mode .admin-resizer{display:none;}
.admin.feedback-mode .admin-list{width:auto; flex:1; border-right:0;}
/* timeline-mode: the historical-era editor takes over the admin area (like feedback) */
.admin.timeline-mode .admin-tree,.admin.timeline-mode .admin-side-foot,.admin.timeline-mode .admin-collapse,.admin.timeline-mode .admin-editor,.admin.timeline-mode .admin-search,.admin.timeline-mode .alt-right,.admin.timeline-mode .alt-right-gloss,.admin.timeline-mode .admin-sel-bar,.admin.timeline-mode .admin-resizer{display:none;}
.admin.timeline-mode .admin-list{width:auto; flex:1; border-right:0;}
/* dash-mode: the whole-site figures take over the admin area (like feedback and timeline). It also hides the
   list TOOLS row's count, which the other two keep — the dashboard's own header says what it is counting. */
.admin.dash-mode .admin-tree,.admin.dash-mode .admin-side-foot,.admin.dash-mode .admin-collapse,.admin.dash-mode .admin-editor,.admin.dash-mode .admin-search,.admin.dash-mode .alt-right,.admin.dash-mode .alt-right-gloss,.admin.dash-mode .admin-sel-bar,.admin.dash-mode .admin-resizer{display:none;}
.admin.dash-mode .admin-list{width:auto; flex:1; border-right:0;}
/* with the deck tree hidden, the side panel is just the tab strip — drop its full-height right border + fill so it
   doesn't leave a floating vertical divider running down the empty column below the tabs. */
/* quotes-mode: the daily-quote pool takes over the admin area (like feedback, timeline and the dashboard) */
.admin.quotes-mode .admin-tree,.admin.quotes-mode .admin-side-foot,.admin.quotes-mode .admin-collapse,.admin.quotes-mode .admin-editor,.admin.quotes-mode .admin-search,.admin.quotes-mode .alt-right,.admin.quotes-mode .alt-right-gloss,.admin.quotes-mode .admin-sel-bar,.admin.quotes-mode .admin-resizer{display:none;}
.admin.quotes-mode .admin-list{width:auto; flex:1; border-right:0;}
/* artefacts-mode: the chest pool takes over the admin area, exactly as the four above do */
.admin.artefacts-mode .admin-tree,.admin.artefacts-mode .admin-side-foot,.admin.artefacts-mode .admin-collapse,.admin.artefacts-mode .admin-editor,.admin.artefacts-mode .admin-search,.admin.artefacts-mode .alt-right,.admin.artefacts-mode .alt-right-gloss,.admin.artefacts-mode .admin-sel-bar,.admin.artefacts-mode .admin-resizer{display:none;}
.admin.artefacts-mode .admin-list{width:auto; flex:1; border-right:0;}
/* themes-mode: the theme collection takes over the admin area, exactly as the five above do */
.admin.themes-mode .admin-tree,.admin.themes-mode .admin-side-foot,.admin.themes-mode .admin-collapse,.admin.themes-mode .admin-editor,.admin.themes-mode .admin-search,.admin.themes-mode .alt-right,.admin.themes-mode .alt-right-gloss,.admin.themes-mode .admin-sel-bar,.admin.themes-mode .admin-resizer{display:none;}
.admin.themes-mode .admin-list{width:auto; flex:1; border-right:0;}
.admin.feedback-mode .admin-side,.admin.timeline-mode .admin-side,.admin.dash-mode .admin-side,.admin.quotes-mode .admin-side,.admin.artefacts-mode .admin-side,.admin.themes-mode .admin-side{background:transparent; border-right:0;}

/* ---------- Admin → Quotes: the home page's daily quote (Aug 2026, on request) ----------
   A list in RUNNING ORDER rather than array order — the order is solved from the pool, so where a quote
   sits in the source says nothing about when a reader will meet it, and when they will meet it is the
   thing an editor is planning around. Today's quote is marked; the rest carry the date they come round. */
.q-page{padding:16px 18px 26px; max-width:1000px;}
.q-tools{display:flex; gap:8px; align-items:center; margin:14px 0 12px;}
.q-list{display:flex; flex-direction:column; gap:6px;}
.q-row{
  display:flex; align-items:flex-start; gap:12px; padding:2px 4px 2px 0;
  border:1px solid transparent; border-radius:10px; transition:background .14s var(--ease), border-color .14s;
}
.q-row:hover{background:var(--paper-2); border-color:var(--rule);}
/* the one a reader is looking at right now — the left bar is the same gold the version line and the
   card blanks use, so "current" reads the same here as everywhere else */
.q-row.q-today{border-color:color-mix(in srgb,var(--ochre) 40%,transparent); background:color-mix(in srgb,var(--ochre) 7%,transparent);}
.q-row.q-open{border-color:var(--indigo); background:var(--indigo-wash);}
.q-main{
  flex:1; min-width:0; appearance:none; background:none; border:0; cursor:pointer; text-align:left;
  padding:9px 12px; display:flex; flex-direction:column; gap:4px; font:inherit; color:inherit;
}
.q-main:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px; border-radius:9px;}
.q-text{font-family:var(--serif); font-size:calc(14.5px * var(--fs)); line-height:1.45; color:var(--ink);}
.q-meta{display:flex; align-items:center; gap:8px; flex-wrap:wrap; font-family:var(--sans); font-size:calc(11.5px * var(--fs)); color:var(--ink-soft);}
.q-meta b{font-weight:600; color:var(--ink);}
.q-src{color:var(--ink-faint);}
.q-pill{
  font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.07em; text-transform:uppercase;
  padding:1px 7px; border-radius:999px; border:1px solid transparent; white-space:nowrap;
}
.q-orig{color:var(--good); background:color-mix(in srgb,var(--good) 11%,transparent); border-color:color-mix(in srgb,var(--good) 30%,transparent);}
.q-noorig{color:var(--ink-faint); background:var(--paper-2); border-color:var(--rule);}
.q-edited{color:var(--indigo); background:var(--indigo-wash); border-color:color-mix(in srgb,var(--indigo) 30%,transparent);}
.q-added{color:var(--ochre); background:color-mix(in srgb,var(--ochre) 12%,transparent); border-color:color-mix(in srgb,var(--ochre) 32%,transparent);}
/* when it next comes round — held to the right, and never allowed to wrap onto the quote's line */
.q-when{
  flex:none; align-self:center; width:15ch; text-align:right; white-space:nowrap;
  font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:var(--ink-faint);
}
.q-row.q-today .q-when{color:var(--ochre); font-weight:600;}
.q-form{
  border:1px solid var(--indigo); border-radius:12px; background:var(--card);
  padding:14px 16px 12px; margin:0 0 14px; display:flex; flex-direction:column; gap:2px;
}
.q-form-head{display:flex; align-items:baseline; gap:10px; flex-wrap:wrap; margin-bottom:6px;}
.q-form-head b{font-family:var(--display); font-size:calc(15px * var(--fs)); color:var(--ink);}
.q-form-note, .q-form-sub{font-family:var(--sans); font-size:calc(11.5px * var(--fs)); line-height:1.5; color:var(--ink-faint);}
.q-form-sub{margin:12px 0 6px; padding-top:10px; border-top:1px solid var(--rule);}
.q-form-acts{display:flex; justify-content:flex-end; gap:8px; margin-top:12px;}
/* the "Who said it?" pool under the daily quotes — a switch per line rather than a form (see whoSaidAdminHTML) */
.ws-admin{margin-top:28px; padding-top:18px; border-top:1px solid var(--rule);}
.ws-admin-head{display:flex; align-items:baseline; gap:10px; margin-bottom:6px;}
.ws-admin-head b{font-family:var(--display); font-size:calc(17px * var(--fs)); color:var(--ink);}
.ws-admin-count{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:var(--ink-faint);}
.ws-admin .q-main{cursor:default;}
.ws-row .mini-btn{flex:none; align-self:center; margin-right:8px;}
.ws-row.ws-off .q-text{color:var(--ink-faint); text-decoration:line-through; text-decoration-color:color-mix(in srgb,var(--ink-faint) 60%,transparent);}
.tf-form .admin-field{margin-top:10px;}
.tf-form textarea.af-input{resize:vertical; font-family:var(--sans); line-height:1.5;}
.tf-row .mini-btn + .mini-btn{margin-left:-2px;}
@media (max-width:640px){
  .ws-row .mini-btn{align-self:flex-start; margin:0 12px 8px;}
  .q-page{padding:12px 12px 22px;}
  .q-row{flex-direction:column; align-items:stretch; gap:0;}
  .q-when{align-self:flex-start; width:auto; text-align:left; padding:0 12px 8px;}
}

/* ---- Edit → Dashboard: Folio in numbers ---- */
.dsh-wrap{padding:20px 24px 40px; max-width:1180px;}
.dsh-lede{font-family:var(--serif); font-size:calc(14.5px * var(--fs)); line-height:1.65; color:var(--ink-soft); max-width:74ch; margin-bottom:22px;}
.dsh-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(320px,1fr)); gap:16px; align-items:start;}
.dsh-card{background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm); padding:16px 18px 17px; display:flex; flex-direction:column; gap:13px; min-width:0;}
.dsh-head{display:flex; align-items:baseline; justify-content:space-between; gap:12px; flex-wrap:wrap;}
.dsh-head h3{font-family:var(--display); font-weight:600; font-size:calc(16.5px * var(--fs)); color:var(--ink); margin:0;}
.dsh-sub{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.09em; text-transform:uppercase; color:var(--ink-faint);}
.dsh-tiles{display:grid; grid-template-columns:repeat(auto-fit,minmax(112px,1fr)); gap:10px;}
.dsh-tile{background:var(--paper-2); border:1px solid color-mix(in srgb, var(--rule) 70%, transparent); border-radius:10px; padding:11px 12px; min-width:0;}
.dsh-tile b{display:block; font-family:var(--display); font-weight:600; font-size:calc(21px * var(--fs)); color:var(--ink); line-height:1.15;}
.dsh-tile span{display:block; font-family:var(--sans); font-size:calc(11px * var(--fs)); color:var(--ink-faint); margin-top:3px; line-height:1.35;}
.dsh-bar{display:flex; flex-direction:column; gap:6px;}
.dsh-bar-head{display:flex; align-items:baseline; justify-content:space-between; gap:10px; font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-soft);}
.dsh-bar-head b{font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink); font-weight:500; white-space:nowrap;}
.dsh-bar .ds-bar{width:100%; flex:none;}
.dsh-bar small{font-family:var(--sans); font-size:calc(11.5px * var(--fs)); color:var(--ink-faint); line-height:1.45;}
.dsh-note{font-family:var(--sans); font-size:calc(12px * var(--fs)); line-height:1.55; color:var(--ink-faint);}
.dsh-langs{display:grid; grid-template-columns:repeat(auto-fit,minmax(150px,1fr)); gap:6px 14px;}
.dsh-lang{display:flex; align-items:baseline; gap:8px; font-family:var(--sans); font-size:calc(12px * var(--fs)); color:var(--ink-soft); border-bottom:1px dotted color-mix(in srgb,var(--rule) 80%,transparent); padding-bottom:4px;}
.dl-code{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.08em; color:var(--indigo); font-weight:600; flex:none; width:26px;}
.dl-n{color:var(--ink-faint); font-size:calc(11.5px * var(--fs));}
.dl-n + .dl-n{margin-left:auto;}
.dsh-files{display:flex; flex-direction:column; gap:0;}
.dsh-file{display:grid; grid-template-columns:1fr auto auto auto; align-items:baseline; gap:10px; font-family:var(--sans); font-size:calc(11.5px * var(--fs)); color:var(--ink-soft); border-bottom:1px dotted color-mix(in srgb,var(--rule) 80%,transparent); padding:4px 0;}
.dsh-file:last-child{border-bottom:0;}
.df-n{overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:var(--ink);}
.df-b,.df-t,.df-ms{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:var(--ink-faint); white-space:nowrap;}
.df-b{color:var(--ink-soft);}
.thr-list{display:flex; flex-direction:column; gap:12px;}
.thr{display:grid; grid-template-columns:auto 1fr auto; align-items:center; gap:12px;}
.thr-mock{width:74px; flex:none;}
.thr-mock .theme-mock{width:100%; height:auto; aspect-ratio:16/10;}
.thr-locked .thr-mock{opacity:.55; filter:saturate(.55);}
.thr-name{font-family:var(--display); font-size:calc(14px * var(--fs)); color:var(--ink); display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;}
.thr-kind{font-family:var(--sans); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink-faint);}
.thr-tag{font-family:var(--sans); font-size:calc(11.5px * var(--fs)); color:var(--ink-faint); margin:2px 0 6px;}
.thr-n{font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-soft); white-space:nowrap;}
.tl-edit{padding:22px 26px; max-width:900px;}
.tl-intro{font-family:var(--serif); font-size:calc(14px * var(--fs)); line-height:1.6; color:var(--ink-soft); margin-bottom:20px;}
.tl-add{border:1px solid var(--rule); border-radius:12px; background:var(--paper-2); padding:18px; margin-bottom:24px;}
.tl-add-row{display:flex; gap:14px; align-items:flex-end; flex-wrap:wrap; margin-bottom:14px;}
.tl-add-row:last-of-type{margin-bottom:0;}
.tl-file{display:inline-flex; align-items:center; gap:12px; cursor:pointer;}
.tl-file-btn{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink); background:var(--card); border:1px solid var(--rule); border-radius:8px; padding:9px 14px; transition:border-color .15s var(--ease);}
.tl-file:hover .tl-file-btn{border-color:var(--indigo);}
.tl-file-name{font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-faint);}
.tl-field{display:flex; flex-direction:column; gap:5px;}
.tl-field>span{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint);}
.tl-field>span small{text-transform:none; letter-spacing:0;}
.tl-field input,.tl-field select{font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink); background:var(--card); border:1px solid var(--rule); border-radius:8px; padding:8px 10px; min-width:150px;}
.tl-field-grow{flex:1;}.tl-field-grow input{width:100%; min-width:120px;}
.tl-field-sm input,.tl-field-sm select{min-width:104px;}
.tl-status{font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-faint);}
.tl-hint{font-family:var(--sans); font-size:calc(11px * var(--fs)); color:var(--ink-faint); flex:1; min-width:180px; line-height:1.45;}
.tl-prev{margin-top:14px; border:1px solid var(--rule); border-radius:8px; background:var(--card); max-width:100%; display:block;}
.tl-prev[hidden]{display:none;}
.tl-eras{display:flex; flex-direction:column; border:1px solid var(--rule); border-radius:12px; overflow:hidden;}
.tl-empty{padding:26px; text-align:center; font-family:var(--serif); font-size:calc(14px * var(--fs)); color:var(--ink-faint);}
.tl-era{display:flex; align-items:center; gap:16px; padding:14px 18px; border-bottom:1px solid var(--rule);}
.tl-era:last-child{border-bottom:0;}
.tl-era-year{font-family:var(--mono); font-size:calc(13px * var(--fs)); font-weight:600; color:var(--indigo); min-width:84px;}
.tl-era-meta{flex:1; min-width:0; font-family:var(--serif); font-size:calc(14px * var(--fs));}
.tl-era-name{font-weight:600;}
.tl-era-meta small{display:block; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.04em; color:var(--ink-faint); margin-top:3px;}
.tl-era-acts{display:flex; gap:7px; flex-shrink:0;}
/* ---- Edit → Feedback: the readers' message queue ----
   Every row wears its triage colour on the left edge (--fb-col, set inline from FEEDBACK_STATUS in app.js),
   so scanning the list for what still needs a decision is a glance rather than a read. */
.admin-tab-badge{display:inline-block; min-width:16px; margin-left:6px; padding:0 4px; border-radius:8px; background:var(--indigo); color:#fff; font-family:var(--mono); font-size:calc(9px * var(--fs)); line-height:15px; text-align:center; vertical-align:1px;}
.admin-tab-badge[hidden]{display:none;}
.fbq-wrap{padding:18px 22px 40px; max-width:1000px;}
.fbq-bar{display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:16px;}
.fbq-filters{display:flex; gap:6px; flex-wrap:wrap; flex:1;}
.fbq-chip{display:inline-flex; align-items:center; gap:6px; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-soft); background:var(--paper-2); border:1px solid var(--rule); border-left:3px solid var(--sw,var(--rule)); border-radius:7px; padding:6px 9px; cursor:pointer;}
.fbq-chip:hover{border-color:var(--ink-soft); border-left-color:var(--sw,var(--ink-soft));}
.fbq-chip.on{background:var(--ink); color:var(--paper); border-color:var(--ink);}
.fbq-n{font-size:calc(9px * var(--fs)); opacity:.7;}
.fbq-empty{font-family:var(--sans); font-size:calc(14px * var(--fs)); color:var(--ink-faint); padding:28px 4px; line-height:1.6;}
.fbq-row{display:flex; gap:16px; align-items:flex-start; padding:15px 16px 15px 17px; margin-bottom:10px; background:var(--card); border:1px solid var(--rule); border-left:4px solid var(--fb-col,var(--rule)); border-radius:10px;}
.fbq-row.st-discarded{opacity:.6;}
.fbq-main{flex:1; min-width:0;}
.fbq-head{display:flex; align-items:baseline; gap:9px; flex-wrap:wrap; margin-bottom:8px;}
.fbq-kind{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--paper); background:var(--fb-col,var(--ink-faint)); border-radius:5px; padding:2px 6px;}
.fbq-who{font-family:var(--serif); font-size:calc(15px * var(--fs)); font-weight:600;}
.fbq-anon{font-family:var(--mono); font-size:calc(8px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint); font-weight:400;}
.fbq-mail{font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--indigo);}
.fbq-meta{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.04em; color:var(--ink-faint);}
.fbq-state{margin-left:auto; font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--fb-col,var(--ink-faint));}
.fbq-msg{font-family:var(--serif); font-size:calc(14.5px * var(--fs)); line-height:1.6; color:var(--ink); white-space:normal; overflow-wrap:anywhere;}
.fbq-ua{margin-top:7px; font-family:var(--mono); font-size:calc(9.5px * var(--fs)); color:var(--ink-faint); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.fbq-note{display:block; width:100%; margin-top:10px; resize:none; overflow:hidden; font-family:var(--sans); font-size:calc(12.5px * var(--fs)); line-height:1.5; color:var(--ink); background:var(--paper-2); border:1px solid var(--rule); border-radius:8px; padding:7px 9px;}
.fbq-note:focus{outline:none; border-color:var(--indigo);}
.fbq-acts{display:flex; flex-direction:column; gap:5px; flex:none; width:118px;}
.fbq-sw{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; text-align:left; color:var(--ink-soft); background:var(--paper-2); border:1px solid var(--rule); border-left:4px solid var(--sw); border-radius:7px; padding:6px 8px; cursor:pointer;}
.fbq-sw:hover{border-color:var(--ink-soft); border-left-color:var(--sw);}
.fbq-sw.on{color:#fff; background:var(--sw); border-color:var(--sw);}
.fbq-del{margin-top:4px; text-align:center;}
@media (max-width:640px){
  .fbq-wrap{padding:14px 12px 32px;}
  .fbq-row{flex-direction:column; gap:10px;}
  .fbq-acts{flex-direction:row; flex-wrap:wrap; width:auto;}
}

/* ---- About page: the beta feedback form ---- */
.fb-intro{font-family:var(--serif); font-size:calc(15px * var(--fs)); line-height:1.65; color:var(--ink-soft); margin:0 0 18px;}
.fb-form{display:flex; flex-direction:column; gap:14px;}
.fb-row{display:flex; gap:14px; flex-wrap:wrap;}
.fb-row .fb-field{flex:1; min-width:210px;}
.fb-field{display:flex; flex-direction:column; gap:6px;}
.fb-field>span{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint);}
.fb-field>span small{text-transform:none; letter-spacing:0;}
.fb-field-kind{max-width:320px;}
.fb-field input,.fb-field select,.fb-field textarea{font-family:var(--sans); font-size:calc(14px * var(--fs)); line-height:1.55; color:var(--ink); background:var(--card); border:1px solid var(--rule); border-radius:9px; padding:10px 12px; width:100%;}
.fb-field textarea{resize:vertical; min-height:110px;}
.fb-field input:focus,.fb-field select:focus,.fb-field textarea:focus{outline:none; border-color:var(--indigo);}
.fb-foot{display:flex; align-items:center; gap:18px; flex-wrap:wrap;}
.fb-privacy{flex:1; min-width:220px; font-family:var(--sans); font-size:calc(11.5px * var(--fs)); line-height:1.5; color:var(--ink-faint);}
.fb-foot .btn{flex:none;}
.fb-status{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.04em; min-height:14px;}
.fb-status.ok{color:var(--indigo);}
.fb-status.bad{color:var(--danger,#c8453c);}

/* daily quote (home) */
/* The figure is held at the height of its TALLER language (lockHeight in app.js sets the min-height), so
   flipping it to the original moves nothing under it. Centring the content in that box is what keeps the
   shorter of the two from reading as a quote with a hole beneath it. */
/* ON A PHONE THE QUOTE COMES AFTER THE DAY'S WORK (Sep 2026, found in the site review).
   It sat between the page head and the daily-study banner, and at 390x844 that put the first control
   that starts a session BELOW THE FOLD on a fresh install: the quote's own 38px of padding top and
   bottom, its 34px opening mark, the line, the attribution and the source run to about 300px before
   the banner begins. For a site whose whole proposition is a few minutes a day, the few minutes
   should not open with a scroll.
   It is `order` on the phone rather than a change to the markup, so the DESKTOP keeps the running
   order it was designed with — there the quote is the first thing under the masthead and there is
   room for the banner beside it. `.page` becomes a flex column only inside this query; the children
   are all full-width blocks, so nothing else about the layout moves. The quote is not demoted, only
   moved: it still opens the page's second screen, which is where a reader who has finished today's
   cards lands. */
/* The quote is a SIBLING of `.banners` again (Sep 2026, on request — see PAGES.home): it stands between
   the page head and the day's work, so it needs no ordering rule and no phone override of its own. */
.daily-quote{position:relative; margin:0 0 26px; padding:38px 0; text-align:center; background:none; box-shadow:none;
  display:flex; flex-direction:column; justify-content:center;}
.daily-quote blockquote{margin:0; font-family:var(--serif); font-size:calc(18px * var(--fs)); font-style:italic; line-height:1.5; color:var(--ink);}
.daily-quote blockquote::before{content:"\201C"; display:block; font-family:var(--display); font-style:normal; font-size:calc(34px * var(--fs)); line-height:.4; color:var(--zh); opacity:.75; margin-bottom:8px;}
.daily-quote figcaption{margin-top:9px; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint);}
.daily-quote figcaption::before{content:""; display:inline-block; width:16px; height:1.5px; background:#C39A2E; vertical-align:3px; margin-right:8px;}
/* where the words come from, small under the speaker — italic serif, since most of these are work titles */
.daily-quote .dq-src{display:block; margin-top:5px; font-family:var(--serif); font-style:italic; font-size:calc(11.5px * var(--fs)); letter-spacing:0; text-transform:none; color:var(--ink-faint);}
/* click the quote to read it in its original language. The original is set in its own script's natural
   face: no mono and no uppercasing in the caption (which mangles the accents on a Greek name), and no
   synthetic oblique on Chinese — the body serif chain ends in generic `serif`, so CJK falls through to
   the reader's own system font, as everywhere else on the site. */
/* THE WORDS ARE SELECTABLE (Aug 2026, on request). This carried `user-select:none` from the day the flip
   shipped, on the grounds that the figure is a button and clicking it twice to toggle back would sweep the
   ::selection wash across the whole quote — which is true, and the wrong trade: a quotation is the one thing
   on the home page a reader might want to copy, and the site's own read-aloud menu is built on selecting
   prose. The flip is guarded in JS instead (wireDailyQuote): a click that ends a selection, or that ends a
   drag, does not turn the page over. */
.daily-quote.dq-flip{cursor:pointer; -webkit-tap-highlight-color:transparent;}
.daily-quote.dq-flip:focus{outline:none;}
.daily-quote.dq-flip:focus-visible{outline:2px solid var(--zh); outline-offset:8px; border-radius:4px;}
/* The two languages dissolve into each other rather than cutting: the words on screen fade out and
   lift, the incoming ones rise into place, and the figure's height eases between the two so the page
   below never jumps. `dq-in` holds the incoming words at their start with no transition, so removing
   the class is what animates them. The global prefers-reduced-motion killswitch flattens all of this,
   and wireDailyQuote swaps outright in that case rather than waiting out the timings. */
.daily-quote.dq-flip blockquote, .daily-quote.dq-flip figcaption{transition:opacity .17s ease, transform .17s ease;}
.daily-quote.dq-flip.dq-out blockquote, .daily-quote.dq-flip.dq-out figcaption{opacity:0; transform:translateY(-5px);}
.daily-quote.dq-flip.dq-in blockquote, .daily-quote.dq-flip.dq-in figcaption{opacity:0; transform:translateY(6px); transition:none;}
.daily-quote.dq-flip.dq-sizing{overflow:hidden; transition:height .24s ease;}
.daily-quote figcaption .dq-orig{font-family:var(--serif); font-style:normal; font-size:calc(13px * var(--fs)); letter-spacing:.02em; text-transform:none;}
.daily-quote .dq-src .dq-orig{font-size:calc(11.5px * var(--fs)); font-style:italic;}
.daily-quote .dq-orig[lang="lzh"]{font-style:normal; letter-spacing:.05em;}

/* badges (profile) */
.dp-empty{padding:18px 20px; font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink-faint);}
.badges-box{background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm); padding:14px 16px;}
.badges-head{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint); margin-bottom:12px;}
.badges{display:grid; grid-template-columns:repeat(auto-fill,minmax(108px,1fr)); gap:10px;}
/* a badge is a button that flips on click to show how it is earned (delegated .badge listener in app.js).
   The two faces are absolutely positioned inside .badge-inner, so the button carries the height itself. */
.badge{position:relative; min-height:104px; padding:0; border:0; background:none; font:inherit; color:inherit; text-align:center; cursor:pointer; perspective:800px; -webkit-tap-highlight-color:transparent;}
.badge-inner{position:absolute; inset:0; transform-style:preserve-3d; transition:transform .5s cubic-bezier(.2,.7,.3,1);}
.badge.flipped .badge-inner{transform:rotateY(180deg);}
.badge-face{position:absolute; inset:0; display:flex; flex-direction:column; align-items:center; justify-content:center; text-align:center; gap:6px; padding:13px 8px; border:1px solid var(--rule); border-radius:11px; background:var(--paper-2); overflow:hidden; backface-visibility:hidden; -webkit-backface-visibility:hidden;}
.badge-back{transform:rotateY(180deg); gap:5px;}
.badge.locked{opacity:.42; filter:grayscale(1);}
/* an earned badge is unmistakable at a glance: the site's achievement gold (#C39A2E, as on the level
   numerals and the unlock date) washes the whole tile, so "got" reads as colour, not as a subtle rule. */
.badge.got .badge-front{background:linear-gradient(155deg, color-mix(in srgb, #C39A2E 30%, var(--card)), color-mix(in srgb, #C39A2E 12%, var(--card)));
  border-color:color-mix(in srgb, #C39A2E 60%, var(--rule)); box-shadow:0 1px 0 color-mix(in srgb, #FFF 45%, transparent) inset;}
.badge.got .badge-back{background:color-mix(in srgb, #C39A2E 10%, var(--card)); border-color:color-mix(in srgb, #C39A2E 40%, var(--rule));}
.badge.got .badge-name{color:#6E5410;}
body.night .badge.got .badge-front{background:linear-gradient(155deg, color-mix(in srgb, #E6C765 26%, var(--card)), color-mix(in srgb, #E6C765 9%, var(--card)));
  border-color:color-mix(in srgb, #E6C765 50%, var(--rule)); box-shadow:0 1px 0 color-mix(in srgb, #E6C765 18%, transparent) inset;}
body.night .badge.got .badge-back{background:color-mix(in srgb, #E6C765 8%, var(--card)); border-color:color-mix(in srgb, #E6C765 32%, var(--rule));}
body.night .badge.got .badge-name{color:#F3E3B2;}
.badge:focus-visible{outline:none;}
.badge:focus-visible .badge-face{border-color:var(--indigo); box-shadow:0 0 0 2px color-mix(in srgb, var(--indigo) 30%, transparent);}
.badge-ic{font-size:calc(26px * var(--fs)); line-height:1;}
.badge-name{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.04em; text-transform:uppercase; color:var(--ink); line-height:1.3;}
.badge-sub{font-family:var(--mono); font-size:calc(9px * var(--fs)); color:var(--ink-faint); line-height:1;}
.badge.got .badge-sub{color:#8A6A14;}
body.night .badge.got .badge-sub{color:#E6C765;}
.badge-back-h{font-family:var(--mono); font-size:calc(8px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint);}
.badge-how{font-family:var(--sans); font-size:calc(10.5px * var(--fs)); line-height:1.35; color:var(--ink-soft);}
/* profile hero: level ring around the avatar + headline stat tiles */
.ph-ava{position:relative; flex:none; width:88px; height:88px; display:grid; place-items:center;}
.ph-ring{position:absolute; inset:0; width:100%; height:100%; pointer-events:none;}
.ph-ava .mono-btn{margin:0;}
.ph-ava .monogram{width:64px; height:64px; border-radius:50%; font-size:calc(28px * var(--fs));}
.ph-lvl{position:absolute; bottom:-4px; left:50%; transform:translateX(-50%); font-family:var(--mono); font-size:calc(9px * var(--fs)); font-weight:600; letter-spacing:.06em; color:#7A5E12; background:#EAD9A8; border:1px solid #C39A2E; border-radius:99px; padding:2px 8px; white-space:nowrap;}
body.night .ph-lvl{color:#F3E3B2; background:#4A3B10; border-color:#E6C765;}
.ph-stats{display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin:-8px 0 18px;}
.ph-stat{background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm); padding:14px 16px; display:flex; flex-direction:column; gap:3px; border-top:3px solid var(--ph, var(--rule));}
.ph-stat b{font-family:var(--display); font-weight:600; font-size:calc(24px * var(--fs)); line-height:1; color:var(--ph, var(--ink));}
.ph-stat span{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint);}
.ph-stat.st-seen{--ph:var(--indigo);}
.ph-stat.st-streak{--ph:var(--zh);}
.ph-stat.st-badges{--ph:#C39A2E;}
.ph-stat.st-wins{--ph:var(--good, #4F9D67);}
/* collection-level rows: hue-tinted bar + the friend view's "You: …" chip */
.cl-row .xp-fill{background:linear-gradient(90deg, var(--coll-bg, var(--indigo)), color-mix(in srgb, var(--coll-bg, var(--indigo)) 62%, white));}
.cl-you{font-family:var(--mono); font-size:calc(10px * var(--fs)); color:var(--ink-faint); background:var(--paper-2); border:1px solid var(--rule); border-radius:99px; padding:2px 9px; margin-left:10px; vertical-align:2px;}
.dp-row .prog .fill{background:var(--coll-bg, var(--indigo));}
/* signed out: the form beside what the account is for, rather than a 460px card alone in an 800px stage */
.auth-split{display:grid; grid-template-columns:1fr; gap:18px; align-items:start;}
@media (min-width:820px){
  .auth-split{grid-template-columns:minmax(0,460px) minmax(0,1fr); gap:34px;}
  .auth-split .auth-perks{margin-top:34px; border-top:0; padding-top:0;}   /* beside the form it needs no rule above it */
  .auth-split .perk{font-size:calc(13.5px * var(--fs));}
}
/* signed-out pitch: three icon bullets */
.auth-perks{display:flex; flex-direction:column; gap:10px; margin-top:18px; border-top:1px solid var(--rule); padding-top:16px;}
.perk{display:flex; align-items:center; gap:11px; font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink-soft);}
.perk-ic{flex:none; width:28px; height:28px; border-radius:8px; display:flex; align-items:center; justify-content:center; background:color-mix(in srgb, var(--pk, var(--indigo)) 12%, transparent); color:var(--pk, var(--indigo));}
.perk-ic svg{width:15px; height:15px; display:block;}
@media (max-width:640px){ .ph-stats{grid-template-columns:repeat(2,1fr);} }
/* THE NEXT STREAK CHEST (Aug 2026, on request) — seven pips and a sentence, under the stat tiles whose
   streak figure it counts from. Pips rather than a bar: the unit is a DAY, and a continuous fill would
   suggest a part-finished one. The filled pips take the gold every other reward on this page wears. */
.streak-chest{background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm);
  padding:14px 16px; margin:0 0 18px; display:flex; align-items:center; gap:14px;}
.sc-main{flex:1; min-width:0;}
/* the thing the seven days are building towards, drawn once at the right of the block (Aug 2026, on
   request). Quiet until the run is complete, then the same gold the filled pips wear. */
.sc-chest{flex:none; width:46px; color:var(--ink-faint); opacity:.55; transition:color .2s, opacity .2s;}
.sc-chest svg{width:100%; height:auto; display:block;}
.sc-chest.won{color:#C39A2E; opacity:1;}
body.night .sc-chest.won{color:#E6C765;}
.sc-head{display:flex; align-items:baseline; justify-content:space-between; gap:10px;}
.sc-title{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint);}
/* `.sc-count` — the "3 / 7" beside the title — was REMOVED in Sep 2026, on request: the pips are the
   count, and the sentence under them says how many days are left. `.sc-head` keeps its flex row, which
   now holds the title alone and is where anything put back beside it would go. */
.sc-pips{display:flex; gap:6px; margin:9px 0 8px;}
.sc-pip{flex:1; height:8px; border-radius:99px; background:var(--paper-2); border:1px solid var(--rule);}
.sc-pip.on{background:#C39A2E; border-color:#C39A2E;}
body.night .sc-pip.on{background:#E6C765; border-color:#E6C765;}
.sc-note{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-faint); line-height:1.5;}

/* ============================================================
   SETTINGS
   ============================================================ */
/* The cards used to be a 620px column hard-left inside the 800px stage, so a heading that spanned the whole
   width sat over content that stopped 180px short and the page read as half-drawn. They now FILL the stage —
   which keeps their left edge under the heading's, where centring the column would not have — and pair up
   into two columns once there is room for two. The wide ones (the theme picker, and the danger zone, which
   should never sit quietly beside something else) span both. */
.settings{display:grid; grid-template-columns:1fr; gap:14px; align-items:start;}
@media (min-width:900px){
  .settings{grid-template-columns:1fr 1fr;}
  .settings > .set-card.set-wide, .settings > .set-card.danger{grid-column:1 / -1;}
}
.set-card{background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm); padding:6px 22px;}
.set-row{display:flex; align-items:center; gap:18px; padding:18px 0;}
.set-row + .set-row{border-top:1px solid var(--rule);}
.set-row .info{flex:1; min-width:0;}
.set-row .info h3{font-family:var(--serif); font-weight:600; font-size:calc(16.5px * var(--fs)); margin:0 0 4px;}
.set-row .info p{margin:0; font-size:calc(13.5px * var(--fs)); color:var(--ink-faint);}
.set-row .ctl{flex:none;}
/* a second, quieter line under a row's description — the Data card's storage and backup status (Oct 2026) */
.set-row .info p.set-sub{margin-top:6px; font-size:calc(12.5px * var(--fs)); font-style:italic;}
.set-row .ctl [hidden]{display:none;}

.switch{position:relative; width:50px; height:28px; border-radius:99px; background:var(--paper-2); border:1px solid var(--rule); cursor:pointer; transition:background .25s var(--ease), border-color .25s;}
.switch::after{content:""; position:absolute; top:2px; left:2px; width:22px; height:22px; border-radius:50%; background:#fff; box-shadow:0 1px 3px rgba(0,0,0,.2); transition:transform .26s var(--ease);}
.switch.on{background:var(--indigo); border-color:var(--indigo);}
.switch.on::after{transform:translateX(22px);}
.switch:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}

/* A three-cell segmented control (Settings → Appearance). Each cell shows a specimen — an A at the size
   it sets, a unit abbreviation — above its name: the control demonstrates itself, which is quicker to
   read than three equal words. Text size used the same control until Aug 2026 and is a slider now (see
   .fs-slide below); Measurements, which is a CHOICE between two systems rather than a point on a scale,
   keeps it. */
.fs-pick{display:inline-flex; align-items:stretch; border:1px solid var(--rule); border-radius:9px; overflow:hidden;}
.fs-pick button{
  appearance:none; cursor:pointer; border:0; background:var(--paper); color:var(--ink-soft);
  display:flex; flex-direction:column; align-items:center; justify-content:center; gap:2px;
  min-width:60px; padding:7px 12px 8px;
  font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; font-weight:600;
  transition:background .15s, color .15s;
}
.fs-pick button + button{border-left:1px solid var(--rule);}
.fs-pick .fs-a{font-family:var(--serif); font-weight:600; line-height:1; letter-spacing:0; text-transform:none;}
.fs-pick [data-fs="small"] .fs-a{font-size:calc(12px * var(--fs));}
.fs-pick [data-fs="medium"] .fs-a{font-size:calc(16px * var(--fs));}
.fs-pick [data-fs="large"] .fs-a{font-size:calc(20px * var(--fs));}
.fs-pick button:hover{background:var(--indigo-wash); color:var(--indigo);}
.fs-pick button.on{background:var(--indigo); color:#fff;}
.fs-pick button:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}
/* the measurements picker is the same segmented control, with a unit abbreviation where the A sits */
.units-pick .fs-a{font-family:var(--mono); font-size:calc(12px * var(--fs)); letter-spacing:.02em;}

/* Text size — a SLIDER across the full width of the row (Aug 2026, on request; it was three cells of
   the segmented control above, which ran to 186px and left the rest of the row empty). Three sizes are
   an ordered scale, and a slider is the control that says so.

   The track is styled on the input itself rather than faked with a div, so the browser's own drag,
   arrow keys, Home/End and touch target all come free. The two vendor thumb selectors CANNOT be
   comma-joined — a rule with one unknown selector in the list is dropped whole by both engines — so
   they are written out twice, which is why this block looks repetitive. */
.fs-slide{display:block; width:100%; max-width:none;}
.fs-range{
  -webkit-appearance:none; appearance:none; display:block; width:100%; height:26px; margin:0;
  background:transparent; cursor:pointer;
}
.fs-range::-webkit-slider-runnable-track{height:6px; border-radius:3px; background:var(--rule);}
.fs-range::-moz-range-track{height:6px; border-radius:3px; background:var(--rule);}
.fs-range::-webkit-slider-thumb{
  -webkit-appearance:none; appearance:none; width:20px; height:20px; margin-top:-7px;
  border-radius:50%; background:var(--indigo); border:2px solid var(--card);
  box-shadow:0 1px 4px rgba(0,0,0,.28); transition:transform .12s var(--ease);
}
.fs-range::-moz-range-thumb{
  width:20px; height:20px; border-radius:50%; background:var(--indigo); border:2px solid var(--card);
  box-shadow:0 1px 4px rgba(0,0,0,.28); transition:transform .12s var(--ease);
}
.fs-range:hover::-webkit-slider-thumb{transform:scale(1.12);}
.fs-range:hover::-moz-range-thumb{transform:scale(1.12);}
.fs-range:focus-visible{outline:none;}
.fs-range:focus-visible::-webkit-slider-thumb{box-shadow:0 0 0 4px var(--indigo-wash);}
.fs-range:focus-visible::-moz-range-thumb{box-shadow:0 0 0 4px var(--indigo-wash);}
/* The marks under the track. space-between puts the first and last hard against the ends, where the
   thumb's own travel puts them; the A in each is drawn at the size that stop sets, so the scale shows
   what it does rather than only naming it. */
.fs-ticks{display:flex; justify-content:space-between; align-items:flex-end; margin-top:2px;}
.fs-tick{
  display:flex; flex-direction:column; align-items:center; gap:1px; flex:0 1 auto; min-width:0;
  font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.06em;
  text-transform:uppercase; font-weight:600; color:var(--ink-faint); transition:color .15s;
}
.fs-tick:first-child{align-items:flex-start;}
.fs-tick:last-child{align-items:flex-end;}
.fs-tick.on{color:var(--indigo);}
/* Two of the five names are two words ("Very small", "Very large"), and five labels across a 390px row
   have no space to run them on one line — so the label WRAPS and is centred under its own specimen
   rather than being abbreviated. The ends stay hard against the track's ends, where the thumb's travel
   puts them, so their text is aligned to the outside rather than centred. */
.fs-tick .fs-lbl{display:block; text-align:center; line-height:1.15; max-width:7.5em;}
.fs-tick:first-child .fs-lbl{text-align:left;}
.fs-tick:last-child .fs-lbl{text-align:right;}
.fs-tick .fs-a{font-family:var(--serif); font-weight:600; line-height:1; letter-spacing:0; text-transform:none;}
.fs-tick[data-fs="tiny"] .fs-a{font-size:calc(10px * var(--fs));}
.fs-tick[data-fs="small"] .fs-a{font-size:calc(12px * var(--fs));}
.fs-tick[data-fs="medium"] .fs-a{font-size:calc(16px * var(--fs));}
.fs-tick[data-fs="large"] .fs-a{font-size:calc(20px * var(--fs));}
.fs-tick[data-fs="huge"] .fs-a{font-size:calc(24px * var(--fs));}
/* While "Match my device" is on, the Night mode switch still SHOWS which way the system has gone — the row
   is dimmed rather than removed, because a reader needs to see that the site knows it is dark. Clicking it
   is still allowed and takes the decision back (setNight turns the device switch off), so it must NOT be
   pointer-events:none — this is a state, not a disabled control. */
.set-row.row-locked .info h3,.set-row.row-locked .info p{opacity:.62;}
/* …and the same for a deck sheet's switch row whose value is DERIVED from the switch above it — burying,
   while both directions arrive together. Same rule and same reason: dimmed, still pressable, and the
   press says why (see deckPairNew). */
.dm-item.dm-switch.row-locked .dm-switchmain{opacity:.62;}
.switch.switch-locked{opacity:.62;}
.switch.switch-locked:hover{opacity:.85;}

.stepper{display:flex; align-items:center; gap:0; border:1px solid var(--rule); border-radius:9px; overflow:hidden;}
.stepper button{appearance:none; border:0; background:var(--paper); color:var(--ink); width:38px; height:38px; cursor:pointer; font-family:var(--mono); font-size:calc(18px * var(--fs)); transition:background .15s;}
.stepper button:hover{background:var(--indigo-wash); color:var(--indigo);}
.stepper button:active{transform:scale(.94);}
.stepper .val{width:46px; text-align:center; font-family:var(--mono); font-weight:600; font-size:calc(15px * var(--fs));}

.set-sel{appearance:none; -webkit-appearance:none; border:1px solid var(--rule); border-radius:9px; background-color:var(--paper); color:var(--ink); font-family:inherit; font-size:calc(14px * var(--fs)); font-weight:500; padding:9px 34px 9px 13px; cursor:pointer; max-width:230px;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2.4' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); background-repeat:no-repeat; background-position:right 12px center; transition:border-color .15s;}
.set-sel:hover{border-color:var(--indigo);}
.set-sel:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
/* Settings → Study → Day ends at. A real <input type=time>, so every platform's own clock picker and every
   screen reader's own time semantics come free; only the chrome is ours. */
.set-time{border:1px solid var(--rule); border-radius:9px; background:var(--paper); color:var(--ink);
  font-family:var(--mono); font-size:calc(14px * var(--fs)); padding:8px 11px; cursor:pointer;}
.set-time:hover{border-color:var(--indigo);}
.set-time:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}

/* toast */
/* A TOAST IS SIZED BY `width:max-content`, AND THAT IS NOT COSMETIC (Aug 2026, on a message that had to
   say something a reader could act on). Positioned `left:50%` with no `right`, a shrink-to-fit box may
   only be as wide as the space LEFT of the right edge — half the viewport — so on a 360px phone every
   toast was capped at 180px and wrapped: "Daily limits saved" ran to two lines, and a sentence telling a
   reader how to install a speech voice ran to five. `max-content` escapes that constraint and the
   max-width puts a sane ceiling back. Measured before/after at 360 / 390 / 768 / 1280: the long message
   goes 5 lines to 2 on a phone and 2 lines to 1 at 768, no toast overflows at any width, and nothing that
   already fitted on one line moved. */
/* "Folio has moved" strip — shown only on the old pages.dev address (movedStripBoot in app.js). It sits above the
   sticky top bar in normal flow, so it scrolls away with the page. */
.moved-strip{display:flex; flex-wrap:wrap; align-items:center; gap:10px 14px; padding:12px clamp(16px,4vw,40px); background:var(--indigo-wash); color:var(--ink); border-bottom:1px solid var(--rule); font-size:calc(14px * var(--fs)); line-height:1.45;}
.moved-strip p{margin:0; flex:1 1 320px;}
.moved-strip .btn{padding:10px 16px;}
.moved-x{appearance:none; cursor:pointer; border:0; background:none; color:var(--ink-soft); font-size:calc(22px * var(--fs)); line-height:1; padding:4px 8px; border-radius:6px;}
.moved-x:hover{color:var(--ink); background:var(--paper-2);}
.moved-x:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.toast{
  position:fixed; left:50%; bottom:30px; transform:translateX(-50%) translateY(20px);
  width:max-content; max-width:min(92vw, 560px);
  background:var(--ink); color:var(--paper); font-family:var(--mono); font-size:calc(12.5px * var(--fs)); letter-spacing:.04em;
  border-left:4px solid #C39A2E;
  padding:12px 20px; border-radius:10px; box-shadow:var(--shadow-lg); opacity:0; pointer-events:none;
  transition:opacity .25s var(--ease), transform .25s var(--ease); z-index:100;
}
.toast.show{opacity:1; transform:translateX(-50%) translateY(0);}
/* A MODAL SCRIM IS THEME-INDEPENDENT BLACK, NEVER `var(--ink)` (Aug 2026, on a bug report: "the whole
   background is whited out"). These five overlays — this one, the options sheet, the level-up card, an
   artefact's plate and the chest — were each a mix of `--ink`, which reads as "the darkest thing this
   theme has" and is exactly that in light mode (#1D1B29, so the figures below are unchanged there). At
   NIGHT the token flips to #EFEDFA, so every one of them became a 38–58% WHITE veil: opening a deck's
   options on a dark theme whited the whole page out behind the sheet. A scrim's job is to push the page
   BACK, which is one direction rather than a colour the theme gets a say in — the same reasoning
   `.folio-tour`, `.page-help` and the media viewer were already written to, and the reason all three
   spell their black out. Each keeps its own strength; only the colour is now fixed. */
/* in-page prompt/confirm dialog (replaces native prompt()/confirm(), which are blocked in embedded contexts) */
.inline-prompt{position:fixed; inset:0; z-index:400; display:flex; align-items:center; justify-content:center; background:color-mix(in srgb,#000 38%,transparent); padding:20px;}
.ip-box{width:min(420px,92vw); background:var(--card); border:1px solid var(--rule); border-radius:14px; box-shadow:0 24px 60px rgba(0,0,0,.34); padding:20px 22px;}
.ip-msg{font-family:var(--serif); font-size:calc(14.5px * var(--fs)); line-height:1.5; color:var(--ink); white-space:pre-line; margin-bottom:14px;}
.ip-input{width:100%; box-sizing:border-box; appearance:none; border:1px solid var(--rule); background:var(--paper); border-radius:9px; padding:10px 13px; font-family:var(--sans); font-size:calc(14px * var(--fs)); color:var(--ink);}
.ip-input:focus{outline:none; border-color:var(--indigo-bright); box-shadow:0 0 0 3px var(--indigo-wash);}
.ip-actions{display:flex; justify-content:flex-end; gap:8px; margin-top:16px;}
.ip-actions button{appearance:none; border:1px solid var(--rule); background:var(--paper); color:var(--ink); border-radius:8px; padding:8px 16px; font-family:var(--sans); font-size:calc(13px * var(--fs)); font-weight:600; cursor:pointer;}
.ip-actions .ip-cancel:hover{border-color:var(--ink-faint);}
.ip-actions .ip-ok{background:var(--indigo); border-color:var(--indigo); color:#fff;}
.ip-actions .ip-ok:hover{filter:brightness(1.08);}

/* ---------- positioning a profile photo before it is saved (Aug 2026, on request) ----------
   The dialog borrows `.inline-prompt`'s box, its z-index band and its buttons; what it adds is the round
   window. `.avc-ring` is an OVERLAY rather than a `border-radius` on the canvas, and that is the point of
   it: the canvas has to stay square, because the square is what gets saved, so the circle is drawn on top
   as a huge inset shadow that dims everything outside it and a hairline that marks the edge. It is
   `pointer-events:none`, or it would swallow the drag it exists to describe. */
.av-crop{position:fixed; inset:0; z-index:400; display:flex; align-items:center; justify-content:center; background:color-mix(in srgb,#000 46%,transparent); padding:20px; animation:overlayIn .18s var(--ease) both;}
.avc-box{width:min(360px,92vw); background:var(--card); border:1px solid var(--rule); border-radius:14px; box-shadow:0 24px 60px rgba(0,0,0,.34); padding:18px 20px 16px; animation:dialogIn .26s var(--ease) both;}
.avc-title{font-family:var(--display); font-weight:600; font-size:calc(15.5px * var(--fs)); color:var(--ink); margin-bottom:12px;}
.avc-stage{position:relative; width:100%; aspect-ratio:1; border-radius:12px; overflow:hidden; background:var(--paper-2);}
.avc-canvas{display:block; width:100%; height:100%; touch-action:none; cursor:grab;}
.avc-canvas:active{cursor:grabbing;}
.avc-ring{position:absolute; inset:0; pointer-events:none; border-radius:50%;
  box-shadow:0 0 0 9999px color-mix(in srgb,var(--paper) 62%,transparent), inset 0 0 0 1.5px color-mix(in srgb,#fff 70%,transparent);}
.avc-zoom{display:flex; align-items:center; gap:10px; margin-top:14px;}
.avc-zlabel{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint);}
.avc-range{flex:1; accent-color:var(--indigo);}
.avc-hint{font-size:calc(11.5px * var(--fs)); line-height:1.45; color:var(--ink-faint); margin:8px 0 0;}
.avc-acts{display:flex; justify-content:flex-end; gap:8px; margin-top:14px;}
.avc-acts button{appearance:none; border:1px solid var(--rule); background:var(--paper); color:var(--ink); border-radius:8px; padding:8px 16px; font-family:var(--sans); font-size:calc(13px * var(--fs)); font-weight:600; cursor:pointer;}
.avc-acts .avc-cancel:hover{border-color:var(--ink-faint);}
.avc-acts .avc-ok{background:var(--indigo); border-color:var(--indigo); color:#fff;}
.avc-acts .avc-ok:hover{filter:brightness(1.08);}
/* a photo on someone else's profile is a button — see openAvatarViewer */
.mono-view{appearance:none; background:none; border:0; padding:0; cursor:zoom-in; border-radius:50%; display:inline-flex;}
.mono-view:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px;}

/* the Library's line about how many decks the reader's level allows in the daily review */

/* ---------- the options sheet (held down on a deck's row, or on the review banner above them) ----------
   CENTRED at every width (Aug 2026, on request). It was a bottom sheet below 560px, on the reasoning that
   the row held was near the thumb; what that actually produced was a dialog rising out of the tab bar at
   the very bottom of the screen, furthest from where the reader was looking, and it was reported as such.
   The middle of the screen is where a modal belongs, and the difference between the two widths is now only
   how much of it the box takes.
   Same shell for the two dialogs it opens (Custom study, Daily limits), so Escape, the backdrop and the
   focus behaviour are written once. */
.deck-menu{position:fixed; inset:0; z-index:420; display:flex; align-items:center; justify-content:center; padding:20px; background:color-mix(in srgb,#000 38%,transparent);}
/* NOTHING IN THE SHEET IS SELECTABLE, and the reason is the gesture that opens it (Aug 2026, on a bug
   report: holding a book's banner opened the sheet and immediately selected text inside it). The sheet is
   raised at HOLD_MS with the finger still down, so it lands UNDER that finger — and the browser's own
   long-press recognizer, which fires a moment later, hit-tests wherever the finger is NOW. It therefore
   selects a word of whichever row happens to be under it and raises the platform's Copy / Share / Select
   All bar over the very menu just opened. The element held is guarded too (.active-deck[data-review],
   .book-tile, #b-review), but that alone cannot help: by the time the recognizer runs, the thing under the
   finger is this sheet. It is a menu of buttons, so there is nothing here a reader wants to select anyway. */
.deck-menu{-webkit-user-select:none; user-select:none; -webkit-touch-callout:none;}
/* …EXCEPT the fields, and this is not belt-and-braces: `user-select` INHERITS, and it reaches a form
   control (measured — the Daily limits number box computes `none` under the rule above). Typing still
   works, but selecting the figure already in the box to type over it does not, which is the whole way a
   reader edits a number. The guard is about the menu's PROSE; a box a reader types into is not prose. */
.deck-menu input, .deck-menu textarea{-webkit-user-select:text; user-select:text;}
/* EVERY SHEET IS CAPPED TO THE SCREEN AND SCROLLS (Aug 2026, on a bug report from a phone: "sometimes
   on mobile not the whole long-press menu is visible"). Only the two sheets that were BORN long —
   Scheduling and Card info — declared a height, so the OPTIONS menu, which has since grown to five
   switches, five commands, a swatch row and an icon picker, simply outgrew a 640px screen: `.deck-menu`
   is a centred flex container, so a box taller than it overflows equally at BOTH ends and neither end can
   be scrolled to — the head is off the top, Remove is off the bottom, and nothing on screen says so.
   It belongs on the shared shell rather than on that one sheet, because the next sheet to grow will grow
   the same way; the two that already state a cap are more specific and are untouched. `dvh` is what makes
   it right on a phone whose address bar comes and goes, with `vh` under it for anything that lacks it. */
.dm-box{width:min(400px,94vw); background:var(--card); border:1px solid var(--rule); border-radius:15px; box-shadow:0 24px 60px rgba(0,0,0,.34); padding:8px; display:flex; flex-direction:column;
  max-height:calc(100vh - 40px); max-height:calc(100dvh - 40px); overflow-y:auto; overscroll-behavior:contain;}
/* THE SHEET'S OWN × (Aug 2026, on request). STICKY and first in the DOM rather than absolute: `.ds-sheet`
   scrolls its whole box, where an absolute × would scroll off the top, and first-in-DOM is what puts
   "Close" early for a screen reader (deckSheet skips it when choosing the initial focus, so the ring still
   lands on the sheet's first real control). `align-self:flex-end` puts it right; the negative bottom margin
   cancels its own height so the head below is laid out as though it were not there, and `.dm-head`'s own
   right padding is what keeps a long title and the studied count from running under it. */
/* It is BACKED in the sheet's own paper rather than transparent, because sticky means it floats over
   whatever row the scroll has brought to the top — since Aug 2026 that is any sheet, not just the two
   born long — and an unbacked × sitting across a switch reads as a rendering fault and swallows the tap
   meant for it. At rest it sits in the head's own reserved 38px, where the colour is invisible. */
.dm-x{position:sticky; top:0; z-index:2; align-self:flex-end; margin:0 0 -30px 0; flex:none; width:30px; height:30px;
  display:flex; align-items:center; justify-content:center; padding:0; appearance:none; cursor:pointer;
  background:var(--card); border:0; border-radius:50%; color:var(--ink-faint); transition:background .14s var(--ease), color .14s;}
.dm-x svg{width:15px; height:15px;}
.dm-x:hover{background:var(--paper-2); color:var(--ink);}
/* Title on the left, how far through the deck the reader is on the right — on the SAME line, which is what
   `align-items:baseline` buys: the left-hand block is a column, and a column flex item aligns on its own
   first line's baseline, so the figure sits on the title's baseline and not on the block's centre.
   The 38px on the right is the × above, which floats over this row. */
.dm-head{padding:12px 38px 10px 14px; display:flex; align-items:baseline; justify-content:space-between; gap:12px; border-bottom:1px solid var(--rule); margin-bottom:6px;}
.dm-headmain{display:flex; flex-direction:column; gap:2px; min-width:0;}
.dm-title{font-family:var(--display); font-weight:600; font-size:calc(17px * var(--fs)); color:var(--ink); letter-spacing:-.01em;}
.dm-studied{flex:none; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.02em; color:var(--ink-faint); white-space:nowrap;}
.dm-where{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; color:var(--ink-faint);}
.dm-item{appearance:none; text-align:left; cursor:pointer; background:none; border:1px solid transparent; border-radius:10px; padding:10px 14px; display:flex; flex-direction:column; gap:2px; font-family:var(--sans); transition:background .14s var(--ease), border-color .14s;}
.dm-item b{font-weight:600; font-size:calc(14.5px * var(--fs)); color:var(--ink);}
.dm-item small{font-size:calc(11.5px * var(--fs)); color:var(--ink-faint); line-height:1.4;}
.dm-item:hover{background:var(--paper-2); border-color:var(--rule);}
.dm-item:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}
/* Remove says what it is in its LABEL alone (Aug 2026, on request). It carried a soft red wash on hover
   as well, and on a phone — where a hover state can be left behind by the tap that opened the sheet —
   that read as the option having been chosen by accident. It hovers like every other row now; the deep
   red text is the whole of the warning. */
.dm-item.dm-danger b{color:var(--zh);}
/* A SHEET ROW HOLDING A SWITCH — a setting with two states (Aug 2026, on request: the review's
   Ordered/Random pair, and question variety beside it). It replaces two `.dm-choice` rows for the order,
   which spent a third of a phone sheet saying what one line says. The row is a DIV, not a button: it
   contains a `role="switch"`, and a control inside a button is invalid and unreachable by keyboard.
   Clicking anywhere on the row throws it — the switch is a 40px target on a 390px line, and a reader
   aiming at the label means the same thing — while the switch itself takes the tab stop and the keys. */
.dm-switch{flex-direction:row; align-items:center; justify-content:space-between; gap:12px; cursor:pointer;}
.dm-switch .dm-switchmain, .dm-cycle .dm-switchmain{display:flex; flex-direction:column; gap:2px; min-width:0;}
/* the cycler is a <button>, which centres its text and would set the label, note and "Set here" on one line
   (Oct 2026, on a bug report: the Review order row ran its three parts together) */
.dm-cycle .dm-switchmain{align-items:flex-start; text-align:left;}
.dm-switch .switch{flex:none;}
/* THE REVIEW-ORDER CYCLER (Aug 2026, on request, when a third order arrived). Laid out exactly like a
   switch row — the same two-line main block, the same right-hand control slot — so the sheet reads as one
   list of settings rather than one of settings and one of something else. What sits in that slot is the
   NAME of the order in force, in the mono face the sheet uses for every other figure, boxed so it reads
   as a control rather than as a second note; pressing anywhere on the row steps it. It is a real <button>,
   so it needs no role and no key handling of its own. */
.dm-cycle{flex-direction:row; align-items:center; justify-content:space-between; gap:12px;}
.dm-cyval{
  flex:none; font-family:var(--mono); font-size:calc(11px * var(--fs)); font-weight:600;
  letter-spacing:.02em; color:var(--ink-soft); background:var(--paper-2);
  border:1px solid var(--rule); border-radius:999px; padding:5px 11px; white-space:nowrap;
}
.dm-cycle:hover .dm-cyval{border-color:var(--indigo); background:var(--card);}
/* WHERE A SETTING COMES FROM. A third line under the note, quieter than either — it is a fact ABOUT the
   row rather than part of what the row says — and set in the mono face because it names an entry rather
   than reading as prose. `display:none` is spelled out: the author `display` below would otherwise beat
   the UA's own [hidden] rule, which is this file's standing trap. */
.dm-from{
  display:inline-block; margin-top:3px; font-family:var(--mono);
  font-size:calc(9.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase;
  color:var(--ink-faint);
}
.dm-from[hidden]{display:none;}
/* An inherited value is the stronger of the two marks — it is the one saying something the reader may
   not expect — and it takes `--ink-soft`, a SOLVED ink, rather than `--indigo`, which is a brand colour
   that falls short of the bar on several papers (see the CONTRAST block at the head of this file). */
.dm-from.dm-inherit{color:var(--ink-soft);}

/* a sheet row that is one of a SET rather than a command: the chosen one carries a tick, so the sheet
   says what the setting currently is and not only what it can be changed to. (No longer used by the
   review's order, which is a switch above; kept for the book shelf's own favourite row.) */
/* THE GROUP COLOUR ROW (Aug 2026, on request: right-click a group to set its colour). A closed set of
   swatches laid out in the sheet itself rather than a submenu — eight circles take less room than the words
   naming them, and a menu inside a menu is a target inside a target on a phone (the book's own context menu
   makes the same call). It is a DIV carrying .dm-item, like .dm-switch, because it CONTAINS buttons and a
   control inside a button is invalid and unreachable by keyboard. */
.dm-colors{cursor:default;}
.dm-colors:hover{background:none; border-color:transparent;}
.dm-swatches{display:flex; flex-wrap:wrap; gap:7px; margin:7px 0 3px;}
.dm-swatch{
  appearance:none; cursor:pointer; width:24px; height:24px; flex:none; padding:0; border-radius:99px;
  background:var(--sw, var(--paper-2)); border:2px solid transparent; box-shadow:0 0 0 1px var(--rule) inset;
  transition:transform .14s var(--ease), border-color .14s var(--ease);
}
.dm-swatch:hover{transform:scale(1.12);}
.dm-swatch.on{border-color:var(--ink); box-shadow:0 0 0 1px var(--card) inset;}
.dm-swatch:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
/* ---- the icon picker (Aug 2026, on request) ----
   A grid rather than the colour row's wrapping flex line: 33 symbols wrap into ragged rows there, and a
   grid of a fixed cell keeps them in columns whatever the width, which is what makes a set of marks
   scannable. Marked the same way a swatch is (an ink border), so the two controls in one sheet agree. */
.ip-grid{display:grid; grid-template-columns:repeat(auto-fill, minmax(46px, 1fr)); gap:6px; margin:10px 0 4px;}
.ip-cell{
  appearance:none; cursor:pointer; padding:0; height:46px; display:flex; align-items:center; justify-content:center;
  background:var(--paper-2); color:var(--ink); border:2px solid transparent; border-radius:10px;
  box-shadow:0 0 0 1px var(--rule) inset; transition:transform .14s var(--ease), border-color .14s var(--ease);
}
.ip-cell svg{width:26px; height:26px; display:block;}
.ip-cell:hover{transform:scale(1.07);}
.ip-cell.on{border-color:var(--ink);}
.ip-cell:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
/* "Default" is a WORD rather than an empty cell, because clearing takes the row back to whatever it is
   entitled to by itself — for most rows that is a mark rather than a blank, and an empty square would
   promise otherwise. It spans two columns so the word has room at every width. */
.ip-default{grid-column:span 2;}
.ip-x{font:600 11px/1 var(--display); letter-spacing:.04em; text-transform:uppercase; color:var(--ink-faint);}
.ip-cell.on .ip-x{color:var(--ink);}
.ip-up{display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin:10px 0 2px;}
.ip-cur{display:flex; align-items:center; gap:7px; font:500 12px/1.3 var(--sans); color:var(--ink-faint);}
.ip-cur img{width:34px; height:34px; object-fit:contain; border-radius:6px; box-shadow:0 0 0 1px var(--rule);}
.ip-msg{min-height:1.2em; font:500 12px/1.35 var(--sans); color:var(--zh); margin-top:4px;}

/* "no colour of its own" — a slash across the swatch, which says *cleared* where an empty circle would just
   read as one more colour that happens to match the paper */
.dm-swatch-off{position:relative; background:var(--paper-2); overflow:hidden;}
.dm-swatch-off::after{content:""; position:absolute; left:-2px; right:-2px; top:50%; height:1.5px; background:var(--ink-faint); transform:rotate(-45deg);}
.dm-choice{position:relative; padding-right:38px;}
.dm-choice.on{background:var(--indigo-wash); border-color:color-mix(in srgb,var(--indigo) 26%,transparent);}
.dm-choice.on b{color:var(--indigo);}
.dm-choice.on::after{content:""; position:absolute; right:16px; top:50%; width:6px; height:11px; margin-top:-7px; border:solid var(--indigo); border-width:0 2px 2px 0; transform:rotate(45deg);}
/* THE DAILY-LIMITS TABS (Aug 2026, on request): this deck's own figures, and the default every deck follows
   — which is where Settings → New cards per day moved to when it was taken off that page. A tab bar rather
   than two sections stacked, because the two panes hold the SAME two fields and stacking them would ask the
   reader to tell four identical boxes apart by the sentence above them. */
.dm-tabs{display:flex; gap:4px; padding:4px; margin:2px 10px 8px; background:var(--paper-2); border-radius:9px;}
.dm-tab{
  flex:1; appearance:none; cursor:pointer; border:0; background:none; border-radius:7px; padding:7px 8px;
  font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint);
  transition:background .14s var(--ease), color .14s var(--ease);
}
.dm-tab:hover{color:var(--ink-soft);}
.dm-tab.active{background:var(--card); color:var(--indigo); box-shadow:0 1px 3px rgba(0,0,0,.08);}
.dm-tab:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}
/* the author `display:block` a pane would otherwise take beats the UA [hidden] rule — the trap
   `.ces-imgpanel[hidden]` and `.gs-results[hidden]` already carry */
.dm-pane[hidden]{display:none;}
.dm-clear{width:100%;}
.dm-field{display:flex; align-items:center; justify-content:space-between; gap:12px; padding:9px 14px; font-family:var(--sans); font-size:calc(13.5px * var(--fs)); color:var(--ink);}
/* the Scheduling sheet: a wider box than an options sheet (it carries two paragraphs of explanation and a
   parameter box), and one field that stacks because a 21-number list has nowhere to sit beside its label */
.deck-menu.ds-sheet .dm-box{width:min(480px,94vw); max-height:calc(100vh - 32px); max-height:calc(100dvh - 32px); overflow-y:auto;}
.dm-field.dm-fieldcol{flex-direction:column; align-items:stretch; gap:6px;}
.dm-field.dm-fieldcol > span{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint);}
.dm-field.dm-fieldcol > span small{font-family:var(--sans); letter-spacing:0; text-transform:none; margin-left:6px;}
/* a UNIT carried in a row's own label, because the field beside it is a bare number box */
.dm-field > span small{color:var(--ink-faint); font-size:calc(11.5px * var(--fs));}
textarea.ds-params{width:100%; font-family:var(--mono); font-size:calc(11.5px * var(--fs)); line-height:1.5; resize:vertical;}
/* the optimiser's row: a button and the sentence it answers with, which runs to three lines on a phone and
   so is a BLOCK under the button rather than a label beside it */
.ds-optrow{display:flex; flex-direction:column; align-items:flex-start; gap:8px; padding:2px 14px 8px;}
.ds-optrow .btn{flex:none;}
.ds-optmsg:empty{display:none;}
.ds-optmsg{font-size:calc(11.5px * var(--fs)); line-height:1.45; color:var(--ink-faint);}
.ds-optmsg.ds-optok{color:var(--good);}
.ds-optmsg.ds-optwarn{color:var(--ink-soft);}
/* a sheet row holding a picker rather than a number — the label stays on the left and the control takes
   what is left, which on a 390px screen is most of the row */
.dm-field select.af-input{flex:1 1 auto; min-width:0; width:auto; max-width:62%;}
.dm-field .dm-num{width:88px; flex:none; appearance:auto; font-family:var(--mono); font-size:calc(14px * var(--fs)); padding:6px 8px; border:1px solid var(--rule); border-radius:8px; background:var(--paper); color:var(--ink);}
.dm-note{margin:2px 14px 4px; font-size:calc(11.5px * var(--fs)); line-height:1.45; color:var(--ink-faint);}
.dm-actions{display:flex; justify-content:flex-end; gap:8px; padding:10px 14px 8px;}
/* the update popup's per-deck summary (Oct 2026, on request) — see openDeckUpdateSheet */
.dm-upd{margin:4px 14px 8px;}
.dm-upd-t{display:block; font-family:var(--display); font-size:calc(14px * var(--fs)); color:var(--ink);}
.dm-upd-l{margin:4px 0 0; padding-left:18px; font-size:calc(13px * var(--fs)); line-height:1.5; color:var(--ink-quiet);}
.dm-upd-none{margin:4px 0 0; font-size:calc(13px * var(--fs)); color:var(--ink-quiet);}
.dm-actions .btn{font-size:calc(13px * var(--fs)); padding:8px 16px;}
@media (max-width:560px){
  .deck-menu{padding:16px;}
  .dm-box{width:100%; padding:8px;}
}

/* full-screen "level up!" congratulations — dismissed by clicking anywhere on screen (see congratsPopup) */
.levelup-pop{position:fixed; inset:0; z-index:500; display:flex; align-items:center; justify-content:center; padding:24px; background:color-mix(in srgb,#000 42%,transparent); opacity:0; transition:opacity .18s ease; cursor:pointer; overflow:hidden;}
.levelup-pop.show{opacity:1;}
.lu-card{position:relative; width:min(340px,92vw); background:var(--card); border:1px solid var(--rule); border-radius:16px; box-shadow:0 26px 64px rgba(0,0,0,.4); padding:26px 28px 20px; text-align:center; transform:translateY(26px) scale(.6); transition:transform .5s cubic-bezier(.18,1.45,.32,1);}   /* springy overshoot entrance */
.levelup-pop.show .lu-card{transform:none;}
.lu-star{font-size:calc(38px * var(--fs)); line-height:1;}
.levelup-pop.show .lu-star{animation:lu-star .8s cubic-bezier(.2,1.5,.35,1) both .12s;}
@keyframes lu-star{0%{transform:scale(0) rotate(-50deg);}60%{transform:scale(1.4) rotate(10deg);}100%{transform:scale(1) rotate(0);}}
/* full-screen falling confetti — one pass, pieces fade out at the bottom; removed with the overlay */
.lu-confetti{position:absolute; inset:0; overflow:hidden; pointer-events:none;}
.lu-conf{position:absolute; top:-5vh; display:block; opacity:0; animation:lu-fall linear both;}
@keyframes lu-fall{0%{transform:translate(0,0) rotate(0); opacity:1;} 78%{opacity:1;} 100%{transform:translate(var(--dx,0px),110vh) rotate(var(--spin,540deg)); opacity:0;}}
/* an expanding gold flash behind the card at the moment it lands */
.lu-burst{position:absolute; left:50%; top:50%; width:130px; height:130px; margin:-65px 0 0 -65px; border-radius:50%; pointer-events:none; background:radial-gradient(circle, rgba(230,199,101,.6), rgba(230,199,101,0) 70%); animation:lu-burst 1.1s ease-out both;}
@keyframes lu-burst{0%{transform:scale(.15); opacity:0;} 25%{opacity:1;} 100%{transform:scale(7); opacity:0;}}
@media (prefers-reduced-motion: reduce){
  .lu-conf,.lu-burst{display:none;}
  .levelup-pop.show .lu-star{animation:none;}
  .lu-card{transition-duration:.2s; transform:translateY(8px) scale(.97);}
}
.lu-title{font-family:var(--display); font-weight:600; font-size:calc(25px * var(--fs)); letter-spacing:-.01em; color:var(--ink); margin:4px 0 15px;}
.lu-rows{display:flex; flex-direction:column; gap:9px; margin-bottom:16px;}
.lu-row{display:flex; align-items:center; gap:12px; text-align:left; border:1px solid var(--rule); border-radius:10px; padding:9px 12px;}
.lu-badge{flex:none; min-width:38px; height:38px; display:flex; align-items:center; justify-content:center; border-radius:9px; background:var(--paper); border:1px solid var(--rule); font-family:var(--display); font-weight:600; font-size:calc(22px * var(--fs)); color:#C39A2E;}
.lu-text{font-family:var(--serif); font-size:calc(14px * var(--fs)); line-height:1.35; color:var(--ink-soft);}
.lu-text b{color:var(--ink); font-weight:600;}
.lu-hint{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; color:var(--ink-faint);}
body.night .lu-badge{color:#E6C765;}

/* ============================================================
   THE RELIQUARY — artefact chests (Aug 2026, on request)
   ============================================================
   Four rarities, four colours, one token pair each so every surface that has to say "this is an epic"
   — the chest, the reveal, the inventory tile, the admin row's swatch — says it the same way and none
   of them carries a literal. `[data-rar]` is the switch, set on whatever element needs it.

   NIGHT is a separate value rather than a mix, for the reason the collection hues are: a colour mixed
   toward a dark paper loses the thing that identifies it, and the reader is being asked to tell four
   apart at a glance. */
:root{
  --rar-common:#8A867C; --rar-rare:#3A6FB0; --rar-epic:#7B4BA8; --rar-legendary:#C2701E;
}
body.night{
  --rar-common:#ADA99E; --rar-rare:#7FAEE8; --rar-epic:#B98BE0; --rar-legendary:#E9A24E;
}
body.hc:not(.night){
  --rar-common:#6B675E; --rar-rare:#2A568D; --rar-epic:#603683; --rar-legendary:#94540F;
}
[data-rar="common"]{--rar:var(--rar-common);}
[data-rar="rare"]{--rar:var(--rar-rare);}
[data-rar="epic"]{--rar:var(--rar-epic);}
[data-rar="legendary"]{--rar:var(--rar-legendary);}

/* ---- CARD FLAGS (Aug 2026) — the reader's own seven, Anki's order and Anki's names -------------------
   Tokens for exactly the reason the rarities above are: the reader is being asked to tell SEVEN apart at a
   glance, which is more than any other palette on the site asks, so each one is a value chosen per paper
   rather than one value mixed toward whatever paper is under it. `[data-flag="N"]` is the switch.

   The dot is a filled circle with a ring, never coloured TEXT: seven hues that are all legible as 10px
   type on sixteen light papers and eighteen dark ones do not exist, and the name is always beside it in
   the ordinary ink anyway. */
:root{
  --flag-1:#C1392B; --flag-2:#C07213; --flag-3:#3F8F43; --flag-4:#2F6DB5;
  --flag-5:#BC4396; --flag-6:#15837F; --flag-7:#7B4BC0;
}
body.night{
  --flag-1:#F07A6B; --flag-2:#E9A648; --flag-3:#6FC073; --flag-4:#6FA7E8;
  --flag-5:#EC85C4; --flag-6:#4FC3BD; --flag-7:#B08BEA;
}
body.hc:not(.night){
  --flag-1:#97281C; --flag-2:#8E540A; --flag-3:#2C6C30; --flag-4:#21528C;
  --flag-5:#8E2F6B; --flag-6:#0B6360; --flag-7:#5C3494;
}
[data-flag="1"]{--flag:var(--flag-1);}
[data-flag="2"]{--flag:var(--flag-2);}
[data-flag="3"]{--flag:var(--flag-3);}
[data-flag="4"]{--flag:var(--flag-4);}
[data-flag="5"]{--flag:var(--flag-5);}
[data-flag="6"]{--flag:var(--flag-6);}
[data-flag="7"]{--flag:var(--flag-7);}
.flag-dot{display:inline-block; width:calc(10px * var(--fs)); height:calc(10px * var(--fs)); border-radius:99px; background:var(--flag,var(--ink-faint)); box-shadow:0 0 0 1px color-mix(in srgb,var(--flag,var(--rule)) 55%,transparent); vertical-align:middle; flex:0 0 auto;}

/* ---- the rarity chip ---- */
.ar-chip{display:inline-flex; align-items:center; font-family:var(--mono); font-size:calc(9px * var(--fs)); font-weight:600; letter-spacing:.14em; text-transform:uppercase; padding:2px 7px; border-radius:99px; color:var(--rar,var(--ink-faint)); border:1px solid color-mix(in srgb,var(--rar,var(--rule)) 40%,transparent); background:color-mix(in srgb,var(--rar,var(--rule)) 12%,transparent);}

/* ---- the profile showcase: four slots, filled or empty ---- */
.showcase{display:grid; grid-template-columns:repeat(4,1fr); gap:12px; margin-bottom:10px;}
/* the way in to the whole collection. It sits ABOVE the four slots and to the right, where a "see the
   rest of this" control belongs — under them it would read as a caption on the fourth tile. */
/* The showcase's heading and its one button on a single line, centred against each other (Sep 2026, on
   request). It carries `.section-label` as well, so the mono small-caps and the 26px top margin are the
   page's own rather than a second set of numbers — what this adds is the row. `align-items:center` and not
   `baseline`: the button is a bordered box and a baseline would hang it a couple of pixels low against a
   line of small caps. It keeps the label's bottom margin, so the tiles below have not moved either. */
.ar-schead{display:flex; align-items:center; justify-content:space-between; gap:12px;}
.ar-all{font-size:calc(12.5px * var(--fs)); flex:none;}
.ar-note{font-size:calc(12px * var(--fs)); color:var(--ink-faint); margin:6px 2px 16px;}
/* an empty slot is DRAWN rather than left out: four dashed frames say "four of these can go here", where
   two tiles and a gap say nothing at all.
   On your own profile it is a BUTTON that opens the collection to pin from (see showcaseHTML) — the "+"
   invites a click, so it had better answer one — and it says so in words under the mark, since a lone "+"
   on a dashed frame is the one thing on this page whose meaning has to be guessed. On a friend's it stays
   the decoration it always was, hence `cursor:default` on the div and a pointer only on the button. */
.ar-slot{border-style:dashed !important; background:transparent !important; cursor:default; align-items:center; justify-content:center; gap:2px;}
.ar-slotmark{font-size:calc(22px * var(--fs)); color:var(--ink-faint); opacity:.5; line-height:1;}
button.ar-slot{cursor:pointer; transition:border-color .14s var(--ease), background .14s var(--ease);}
button.ar-slot:hover{border-color:color-mix(in srgb,var(--indigo) 55%,var(--rule)) !important; background:color-mix(in srgb,var(--indigo) 7%,transparent) !important;}
button.ar-slot:hover .ar-slotmark,button.ar-slot:hover .ar-slotlbl{color:var(--indigo); opacity:1;}
.ar-slotlbl{font-family:var(--sans); font-size:calc(10.5px * var(--fs)); color:var(--ink-faint); text-align:center; line-height:1.25;}

/* ---- waiting chests, directly above the Daily-study banner (see chestBannerHTML) ---- */
/* Its slot on the home page sits in `.banners`, a flex column with a gap — so an EMPTY slot would still
   spend a gap and leave a hole above the Daily-study banner on every day nothing is waiting. */
#chestSlot:empty{display:none;}
/* …and inside that column the gap is what separates it, so it carries no bottom margin of its own there. */
.banners .chest-banner{margin-bottom:0;}
/* NOT SIGNED IN (Sep 2026, on request — see guestNoticeHTML). Amber: a caution, not an error. */
.guest-notice{display:flex; align-items:center; gap:14px; padding:14px 16px; border-radius:14px;
  background:color-mix(in srgb, #D98A1E 13%, var(--card)); border:1px solid color-mix(in srgb, #D98A1E 45%, var(--rule));
  border-left:4px solid #D98A1E; box-shadow:var(--shadow-sm);}
body.night .guest-notice{background:color-mix(in srgb, #E6A64A 14%, var(--card)); border-color:color-mix(in srgb, #E6A64A 40%, var(--rule)); border-left-color:#E6A64A;}
.guest-notice .gn-ic{flex:none; width:26px; height:26px; color:#B06A0E;}
body.night .guest-notice .gn-ic{color:#E6A64A;}
.guest-notice .gn-ic svg{width:100%; height:100%; display:block;}
.guest-notice .gn-body{flex:1; min-width:0; font-family:var(--sans); font-size:calc(13.5px * var(--fs)); line-height:1.5; color:var(--ink);}
.guest-notice .gn-body b{font-weight:700;}
.guest-notice .gn-go{flex:none; white-space:nowrap;}
@media (max-width:640px){
  .guest-notice{flex-wrap:wrap;}
  .guest-notice .gn-go{width:100%;}
}
.chest-banner{display:flex; align-items:center; gap:14px; padding:14px 16px; margin:0 0 18px;
  background:linear-gradient(105deg, color-mix(in srgb,#C39A2E 22%,transparent), transparent 68%) var(--card);
  border:1px solid color-mix(in srgb,#C39A2E 40%,var(--rule)); border-radius:14px; box-shadow:var(--shadow-sm);}
body.night .chest-banner{background:linear-gradient(105deg, color-mix(in srgb,#E6C765 16%,transparent), transparent 68%) var(--card); border-color:color-mix(in srgb,#E6C765 34%,var(--rule));}
.cb-ic{flex:none; display:block; color:#C39A2E;}
body.night .cb-ic{color:#E6C765;}
.cb-ic .chest-svg{width:calc(42px * var(--fs)); height:auto;}
.cb-text{flex:1; min-width:0; display:flex; flex-direction:column; justify-content:center; gap:2px;}
.cb-text b{font-family:var(--sans); font-size:calc(14px * var(--fs)); font-weight:700; color:var(--ink);}
.cb-text small{font-size:calc(11.5px * var(--fs)); color:var(--ink-faint); line-height:1.4;}
.cb-open{flex:none;}
@media (max-width:520px){
  .chest-banner{gap:11px; padding:12px 13px;}
}

/* ---- the inventory ---- */
.reliquary{margin-bottom:16px;}
.ar-head{display:flex; align-items:center; justify-content:space-between; gap:12px; flex-wrap:wrap; margin-bottom:12px;}
.ar-count{font-family:var(--mono); font-size:calc(11.5px * var(--fs)); letter-spacing:.04em; color:var(--ink-faint);}
.ar-open{font-size:calc(13px * var(--fs)); padding:8px 16px;}
.ar-empty{color:var(--ink-faint); font-size:calc(14px * var(--fs)); padding:4px 2px 14px;}
.ar-grid{display:grid; grid-template-columns:repeat(auto-fill,minmax(132px,1fr)); gap:12px;}
/* ---------- the Reliquary's own page (#reliquary, Aug 2026, on request) ----------
   It borrows the glossary record's page furniture wholesale — `.gl-meter`, `.gl-tools`, `.gl-search`,
   `.gl-empty` — and the Library shelf's `.sort-pick` + `.sort-dir` pair, so it needs almost nothing of
   its own: one row for the chest button and one for the grid when what it is holding is a SENTENCE
   rather than tiles, which `.ar-grid`'s 132px auto-fill columns would otherwise set one word wide. */
.rq-chest{margin:0 0 14px;}
.rq-msg{display:block;}
/* the tile: a picture over its name, framed and washed in its own rarity. The colour is the whole of the
   signal, so it is on the border, the wash AND the label rather than on one of the three. */
.ar-tile{position:relative; display:flex; flex-direction:column; gap:6px; padding:10px; text-align:left; cursor:pointer;
  background:linear-gradient(160deg,color-mix(in srgb,var(--rar,var(--indigo)) 13%,var(--card)),var(--card) 62%);
  border:1px solid color-mix(in srgb,var(--rar,var(--rule)) 45%,transparent); border-radius:12px;
  transition:transform .16s var(--ease), box-shadow .16s var(--ease);}
.ar-tile:hover{transform:translateY(-2px); box-shadow:0 8px 22px color-mix(in srgb,var(--rar,var(--ink)) 22%,transparent);}
.ar-tile:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.ar-img{width:100%; aspect-ratio:1/1; object-fit:cover; border-radius:8px; display:block; background:var(--paper-2);}
/* no picture yet → the object's initial on a wash of its rarity. An empty frame reads as a page that
   failed to load; a lettered plate reads as a thing waiting for its photograph. */
.ar-noimg{display:grid; place-items:center; background:linear-gradient(145deg,color-mix(in srgb,var(--rar,var(--indigo)) 30%,var(--paper-2)),var(--paper-2));}
.ar-noimg span{font-family:var(--display); font-weight:600; font-size:calc(30px * var(--fs)); color:color-mix(in srgb,var(--rar,var(--ink)) 70%,var(--ink)); opacity:.75;}
.ar-tname{font-family:var(--display); font-weight:600; font-size:calc(13.5px * var(--fs)); line-height:1.25; color:var(--ink);}
.ar-trar{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; color:var(--rar,var(--ink-faint));}
/* a pinned artefact is MARKED, not made into a second control — the way to pin and unpin is the window */
.ar-pin{position:absolute; top:7px; right:8px; font-size:calc(13px * var(--fs)); color:#C39A2E; pointer-events:none; text-shadow:0 1px 2px rgba(0,0,0,.35);}

/* ---- one artefact, opened from a tile ---- */
/* IT SITS BELOW THE GLOSSARY STACK, and that is what makes its prose linkable at all (Aug 2026). The
   plate's five sentences auto-link the glossary now, and a gloss popup opens at 8000–9400 on a desktop
   and 9600 as a phone sheet — so at the 9700 this carried, every definition opened from a plate rendered
   BEHIND the plate that raised it: nothing throws, the popup is really there, and the reader sees a click
   that did nothing. 7600 keeps it above the page and its bars and below every popup, and the fullscreen
   image viewer (9800) and the chest (9750) are unaffected. The collection overlay shares this class and
   moves with it, so a plate raised from a tile inside it still stacks above by source order. */
.artefact-pop{position:fixed; inset:0; z-index:7600; display:flex; align-items:center; justify-content:center; padding:20px; background:color-mix(in srgb,#000 52%,transparent); opacity:0; transition:opacity .18s ease; overflow-y:auto;}
.artefact-pop.show{opacity:1;}
/* THE PLATE IS WASHED IN ITS OWN RARITY, top to bottom (Aug 2026, on request). The rarity was already the
   plate's whole language — the chip, the top rule, the border and the picture frame all speak it — and every
   one of those is an EDGE, so a common and a legendary read alike anywhere but the rim. The gradient is laid
   OVER `var(--card)` rather than mixed into it, so it needs no per-theme rule: whatever paper a theme uses is
   what the wash fades into. Strongest at the top, where the name and the chip are, and gone by two thirds
   down, so the five sentences and the citations under it sit on ordinary paper and lose no contrast —
   `test-a11y.js` measures every text node against the background it really renders on. */
.ar-win{position:relative; display:flex; gap:22px; width:min(720px,96vw); max-height:min(88vh,760px); overflow-y:auto; padding:24px 26px;
  background:linear-gradient(180deg,color-mix(in srgb,var(--rar,transparent) 22%,transparent),color-mix(in srgb,var(--rar,transparent) 5%,transparent) 46%,transparent 72%), var(--card);
  border:1px solid color-mix(in srgb,var(--rar,var(--rule)) 45%,transparent); border-top:4px solid var(--rar,var(--indigo)); border-radius:14px; box-shadow:0 26px 64px rgba(0,0,0,.42);}
/* Night takes a weaker mix, for the reason every other wash on the site does: a colour laid over a dark
   paper reads far stronger than the same percentage over a light one. */
body.night .ar-win{background:linear-gradient(180deg,color-mix(in srgb,var(--rar,transparent) 16%,transparent),color-mix(in srgb,var(--rar,transparent) 4%,transparent) 46%,transparent 72%), var(--card);}
.ar-close{position:absolute; top:8px; right:10px; width:28px; height:28px; border:0; background:transparent; color:var(--ink-faint); font-size:calc(20px * var(--fs)); line-height:1; cursor:pointer; border-radius:8px;}
.ar-close:hover{background:var(--paper-2); color:var(--ink);}
.ar-winart{flex:none; width:220px;}
.ar-big{aspect-ratio:1/1;}
/* The plate's picture is the site's own media frame (`.card-img`), so it opens the fullscreen viewer with
   no wiring of its own. Two things are re-set here: the frame is SQUARE, as the lettered placeholder
   beside it is and as every artefact tile is, rather than the 16:9 a card wants; and it carries a border
   in its own RARITY (Aug 2026, on request), which is the same language the chip, the spine and the tile
   already speak — `--rar` comes from the `[data-rar]` on `.ar-win`. */
.ar-winart .card-img{margin:0; aspect-ratio:1/1; border:2px solid color-mix(in srgb,var(--rar,var(--rule)) 70%,transparent);}
.ar-winart .ar-noimg{border:2px solid color-mix(in srgb,var(--rar,var(--rule)) 70%,transparent);}
/* a picture whose file never arrived leaves the frame entirely (the delegated `error` listener), so the
   column would otherwise hold 220px of nothing beside the text */
.ar-winart:has(.card-img.media-dead){display:none;}
/* ---------- A PICTURE STILL ON ITS WAY SPINS (Sep 2026, on request) ----------
   An artefact's picture is a link to somebody else's file, and a 220px square of nothing while it travels
   reads as an artefact that HAS no picture. The ring is drawn on the frame's own `::after` rather than as
   an element, so nothing has to be inserted or removed from the markup — `.ar-loading` is put on by
   `artefactPlateArtHTML` and taken off by the delegated `load` listener (or by `wireArtefactPlate`, for a
   file the cache answered first). The IMG is faded rather than hidden, so a picture that decodes in
   stages is not snapped in at the end; the ring stops on the reduced-motion killswitch and is replaced by
   a still ring, which still says "waiting" where a frozen spinner says "broken". */
.ar-frame.ar-loading{background:color-mix(in srgb, var(--ink) 6%, var(--paper-2));}
.ar-frame.ar-loading img{opacity:0;}
.ar-frame img{transition:opacity .25s var(--ease);}
.ar-frame.ar-loading::after{
  content:""; position:absolute; top:50%; left:50%; width:30px; height:30px; margin:-15px 0 0 -15px;
  border-radius:50%; border:3px solid color-mix(in srgb, var(--rar, var(--indigo)) 26%, transparent);
  border-top-color:var(--rar, var(--indigo)); pointer-events:none;
  animation:ar-spin .8s linear infinite;
}
@keyframes ar-spin{to{transform:rotate(360deg);}}
@media (prefers-reduced-motion:reduce){ .ar-frame.ar-loading::after{animation:none;} }
body.no-anim .ar-frame.ar-loading::after{animation:none;}
.ar-wintext{flex:1; min-width:0;}
/* the name and its rarity on one line, the chip on the title's own baseline; it wraps rather than
   squeezing the name on a narrow plate */
.ar-wtop{display:flex; align-items:baseline; gap:9px; flex-wrap:wrap; margin:8px 0 6px;}
.ar-wname{font-family:var(--display); font-weight:600; font-size:calc(22px * var(--fs)); letter-spacing:-.01em; margin:0;}
.ar-wmeta{font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-faint); letter-spacing:.02em; margin-bottom:3px;}
.ar-wdesc{font-family:var(--serif); font-size:calc(15px * var(--fs)); line-height:1.66; color:var(--ink-soft); margin:12px 0 10px;}
.ar-wdesc b{color:var(--ink);}
/* `.ar-wcredit` — the picture's credit under the plate's prose — is gone (Aug 2026, on request): the
   plate's picture opens the site's fullscreen viewer, whose caption bar carries the credit, so the plate
   itself no longer repeats it. */
/* Showcase, at the TOP of the plate now (Aug 2026, on request). It is an action on a page that is otherwise
   all reading, so it sits where the reader arrives; `flex-start` keeps it the width of its own words rather
   than the width of the column, which would read as a banner. */
.ar-pinbtn{font-size:calc(12.5px * var(--fs)); display:inline-block; margin:0;}
/* with the button above it the name's own top margin would double the gap; the × sits in the same corner,
   so the row it opens on keeps clear of it */
.ar-pinbtn + .ar-wtop{margin-top:10px;}

/* ---- the whole collection, raised from the showcase's "See all" ----
   It reuses `.artefact-pop`'s backdrop and the inventory's own grid, so the list in here is byte-for-byte
   the one the account page's Reliquary section renders. Only the frame is new. */
.ar-collwin{position:relative; width:min(760px,96vw); max-height:min(88vh,820px); overflow-y:auto; padding:22px 24px; background:var(--card); border:1px solid var(--rule); border-radius:14px; box-shadow:0 26px 64px rgba(0,0,0,.42);}
.ar-collhead{font-family:var(--display); font-weight:600; font-size:calc(19px * var(--fs)); margin:0 0 14px;}
/* the inventory's own bottom margin is for a page, not for the foot of a dialog */
.ar-collbody .reliquary{margin-bottom:0;}

/* ---- the plate, previewed live in Admin → Artefacts ----
   The editor's column is narrow and the plate is a two-column frame, so the preview stacks and the
   picture is capped; nothing else about it is restyled, which is the point of previewing it at all. */
.a-edit{display:grid; gap:16px; margin-bottom:14px;}
.a-preview{border:1px solid var(--rule); border-radius:12px; background:var(--paper-2); padding:12px;}
.a-prevhead{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.16em; text-transform:uppercase; color:var(--ink-faint); margin-bottom:10px;}
.ar-win.ar-inline{width:auto; max-height:none; box-shadow:none; overflow:visible;}
@media (min-width:900px){
  /* wide enough for the two to sit side by side — the form on the left, what it makes on the right */
  .a-edit{grid-template-columns:minmax(0,1fr) minmax(0,1fr); align-items:start;}
  .a-edit .a-preview{order:2; position:sticky; top:8px;}
  .a-edit .q-form{order:1;}
}
@media (max-width:899px){
  .ar-win.ar-inline{flex-direction:column; gap:14px;}
  .ar-win.ar-inline .ar-winart{width:100%; max-width:220px; align-self:center;}
}

/* THE PICTURE SITS IN THE TOP LEFT AND THE WORDS RUN ROUND IT (Sep 2026, on request: on a tablet the
   plate "has the image in a column on the left and everything else in a column on the right, leaving a
   lot of empty space in the bottom left"). Five sentences and a fold of citations run far longer than a
   220px square, so a two-column flex left the rest of the left column empty for most of the plate's
   height. The picture is FLOATED instead, as a card's own illustration is, and the text column stops
   being a flex item so its lines can flow round the float and back to the full width below it. The
   admin preview keeps its own stacked layout below 900px (see `.ar-inline` above). */
@media (min-width:641px){
  .ar-win:not(.ar-inline), .ar-win.ar-inline{display:block;}
  .ar-win .ar-winart{float:left; width:200px; margin:4px 20px 10px 0;}
  .ar-win .ar-wintext{display:block;}
  .ar-win .ar-wintext .src-note{clear:both;}
}
@media (min-width:641px) and (max-width:899px){
  .ar-win.ar-inline .ar-winart{float:none; margin:0 auto 14px;}
}
@media (max-width:640px){
  .ar-win{flex-direction:column; gap:14px; padding:20px 18px;}
  .ar-winart{width:100%; max-width:260px; align-self:center;}
  .showcase{grid-template-columns:repeat(2,1fr);}
  .ar-grid{grid-template-columns:repeat(auto-fill,minmax(112px,1fr));}
  .ar-collwin{padding:18px 16px;}
}

/* ---- the chest ----
   Phase 1 waits, phase 2 opens (its LENGTH is set in JS, from the rarity rolled), phase 3 reveals. The
   overlay is NOT dismissed by clicking the backdrop: unlike the level-up popup it holds buttons, and an
   overlay where a stray tap outside the card takes the reward away is one nobody trusts. */
.chest-pop{position:fixed; inset:0; z-index:9750; display:flex; align-items:center; justify-content:center; padding:22px; background:color-mix(in srgb,#000 58%,transparent); opacity:0; transition:opacity .2s ease; overflow:hidden;}
.chest-pop.show{opacity:1;}
/* THE BADGE OVERLAY (openAchPop): the account page's own tile, larger, which turns over into its chest. */
.ach-pop{position:fixed; inset:0; z-index:9750; display:flex; align-items:center; justify-content:center; padding:22px;
  background:color-mix(in srgb,#000 58%,transparent); opacity:0; transition:opacity .2s ease;}
.ach-pop.show{opacity:1;}
.ach-stage{width:min(400px,94vw); text-align:center; background:var(--card); border:1px solid var(--rule); border-radius:16px;
  box-shadow:0 26px 64px rgba(0,0,0,.45); padding:24px 22px 18px; display:flex; flex-direction:column; align-items:center; gap:14px;}
.ach-kicker{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; color:#8A6A14;}
body.night .ach-kicker{color:#E6C765;}
.ach-tiles{display:flex; flex-wrap:wrap; justify-content:center; gap:12px;}
.ach-tile{width:170px; min-height:170px;}
.ach-tile .badge-ic{font-size:calc(44px * var(--fs));}
.ach-tile .badge-name{font-size:calc(12px * var(--fs));}
.ach-tile .badge-sub{font-family:var(--sans); font-size:calc(11px * var(--fs)); line-height:1.35;}
.ach-tile .badge-face{padding:16px 12px;}
.ach-chest{color:#C39A2E; display:block;}
body.night .ach-chest{color:#E6C765;}
.ach-chest .chest-svg{width:calc(64px * var(--fs)); height:auto;}
.ach-hint{font-size:calc(12.5px * var(--fs)); color:var(--ink-soft); line-height:1.45;}
.ach-acts{display:flex; justify-content:center;}
.chest-stage{position:relative; z-index:1; width:min(420px,94vw); max-height:92vh; overflow-y:auto; text-align:center; background:var(--card); border:1px solid var(--rule); border-radius:16px; box-shadow:0 26px 64px rgba(0,0,0,.45); padding:26px 24px 20px;}
.chest-pop.opened .chest-stage{border-color:color-mix(in srgb,var(--rar,var(--rule)) 55%,transparent); border-top:4px solid var(--rar,var(--indigo));}
.chest-lvl{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.18em; text-transform:uppercase; color:#C39A2E; margin-bottom:14px;}
body.night .chest-lvl{color:#E6C765;}
.chest-body{position:relative; display:grid; place-items:center; min-height:132px;}
.chest-btn{border:0; background:transparent; padding:6px; cursor:pointer; color:#C39A2E; border-radius:14px;}
body.night .chest-btn{color:#E6C765;}
.chest-btn:disabled{color:var(--ink-faint); cursor:default;}
.chest-btn:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px;}
.chest-svg{width:min(190px,60vw); height:auto; display:block;}
/* the idle breath, so a chest waiting to be tapped reads as a control rather than as a picture */
.chest-pop:not(.opening) .chest-btn:not(:disabled) .chest-svg{animation:chest-breathe 2.4s ease-in-out infinite;}
@keyframes chest-breathe{0%,100%{transform:translateY(0) scale(1);} 50%{transform:translateY(-4px) scale(1.02);}}
.chest-pop:not(.opening) .chest-btn:not(:disabled):hover .chest-svg{transform:scale(1.05);}
/* the opening: the box shudders, then the lid swings up and away. The DURATION of each is set once here
   and the JS waits out a rarity-sized total, so a legendary lingers on the lid where a common snaps. */
.chest-pop.opening .chest-svg{animation:chest-shake .42s cubic-bezier(.36,.07,.19,.97) 2;}
@keyframes chest-shake{10%,90%{transform:translateX(-2px) rotate(-1deg);} 30%,70%{transform:translateX(3px) rotate(1.4deg);} 50%{transform:translateX(-3px) rotate(-1.4deg);}}
.chest-pop.opening .chest-lid{animation:chest-open .5s cubic-bezier(.3,-.4,.4,1.5) .55s both; transform-origin:50% 92%;}
@keyframes chest-open{0%{transform:rotate(0) translateY(0); opacity:1;} 100%{transform:rotate(-118deg) translateY(-16px); opacity:.15;}}
/* once the lid is off, the chest's own slot collapses — leaving its 132px reserved would sit the
   artefact under a band of nothing, which is exactly where the eye is at that moment */
.chest-pop.opened .chest-btn{display:none;}
.chest-pop.opened .chest-body{min-height:0;}
/* the light out of the box — legendary only, where the wait is long enough for it to be worth the room */
.chest-rays{position:absolute; left:50%; top:52%; width:280px; height:280px; margin:-140px 0 0 -140px; border-radius:50%; pointer-events:none; opacity:0;
  background:radial-gradient(circle,color-mix(in srgb,var(--rar,#C39A2E) 55%,transparent),transparent 62%);}
.chest-pop.opening[data-rar="epic"] .chest-rays{animation:chest-rays 1.5s ease-out .5s both;}
.chest-pop.opening[data-rar="legendary"] .chest-rays{animation:chest-rays 2.1s ease-out .5s both;}
@keyframes chest-rays{0%{transform:scale(.2); opacity:0;} 40%{opacity:1;} 100%{transform:scale(2.4); opacity:0;}}
.chest-hint{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; color:var(--ink-faint); margin-top:12px; min-height:1em;}
.chest-more{font-size:calc(11.5px * var(--fs)); color:var(--ink-faint); margin-top:6px;}
/* The reveal is the artefact's first plate, so it is washed in its rarity exactly as `.ar-win` is (Aug 2026,
   on request) — the same gradient, the same direction, and the same weaker mix at night. It carries its own
   `data-rar`, so `--rar` is already here. It is boxed as well as washed: the reveal floats on the chest
   overlay's dark backdrop rather than on a card, so without a surface under it a gradient fading to
   transparent would fade into the backdrop instead of into paper. */
.chest-reveal{margin-top:10px; padding:16px 18px 18px; border-radius:14px;
  background:linear-gradient(180deg,color-mix(in srgb,var(--rar,transparent) 24%,var(--card)),var(--card) 72%);
  border:1px solid color-mix(in srgb,var(--rar,var(--rule)) 42%,transparent); border-top:4px solid var(--rar,var(--indigo));
  opacity:0; transform:translateY(14px) scale(.94); animation:chest-reveal .5s cubic-bezier(.18,1.35,.34,1) both;}
body.night .chest-reveal{background:linear-gradient(180deg,color-mix(in srgb,var(--rar,transparent) 17%,var(--card)),var(--card) 72%);}
@keyframes chest-reveal{to{opacity:1; transform:none;}}
.chest-art{width:min(190px,52vw); margin:12px auto 0;}
.chest-name{font-family:var(--display); font-weight:600; font-size:calc(21px * var(--fs)); letter-spacing:-.01em; margin:12px 0 5px;}
.chest-meta{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:var(--ink-faint); margin-bottom:3px;}
.chest-acts{display:flex; flex-wrap:wrap; justify-content:center; gap:8px; margin-top:16px;}
.chest-acts .btn{font-size:calc(13px * var(--fs)); padding:8px 16px;}
.chest-conf{position:absolute; inset:0; overflow:hidden; pointer-events:none;}
.chest-bit{position:absolute; top:-6vh; display:block; opacity:0; animation:lu-fall linear both;}
/* a legendary flashes the whole screen once, which is the one thing the smaller rarities do not get */
.chest-pop.opening[data-rar="legendary"]::after{content:""; position:absolute; inset:0; pointer-events:none; background:var(--rar-legendary); opacity:0; animation:chest-flash 1.1s ease-out .55s both;}
@keyframes chest-flash{0%{opacity:0;} 22%{opacity:.34;} 100%{opacity:0;}}
/* Under reduced motion the chest still OPENS — the JS drops the wait to a tick — but nothing travels,
   nothing shakes and nothing falls. The reveal lands on its end state, as everything here does. */
@media (prefers-reduced-motion: reduce){
  .chest-bit,.chest-rays{display:none;}
  .chest-pop.opening .chest-svg,.chest-pop:not(.opening) .chest-btn .chest-svg,.chest-pop.opening .chest-lid{animation:none;}
  .chest-pop.opening[data-rar="legendary"]::after{animation:none; opacity:0;}
}
body.no-anim .chest-bit,body.no-anim .chest-rays{display:none;}
body.no-anim .chest-pop.opening .chest-svg,body.no-anim .chest-pop:not(.opening) .chest-btn .chest-svg,body.no-anim .chest-pop.opening .chest-lid{animation:none;}
body.no-anim .chest-pop.opening[data-rar="legendary"]::after{animation:none; opacity:0;}
/* The home banner's chest CHIP is gone (Aug 2026, on request): a chest is never shown as a number there.
   What says one is waiting is `.chest-banner` above the banner — the notice that used to sit on the
   account page — so the rules that dressed the chip as a fourth stat went with it. */

/* ---- Admin → Artefacts ---- */
.a-row{border:1px solid var(--rule); border-radius:10px; margin-bottom:7px; overflow:hidden; background:var(--card);}
.a-row.a-open{border-color:var(--indigo);}
.a-main{display:flex; align-items:center; gap:12px; width:100%; padding:10px 12px; border:0; background:transparent; text-align:left; cursor:pointer; color:var(--ink);}
.a-main:hover{background:var(--paper-2);}
.a-swatch{flex:none; width:12px; height:34px; border-radius:4px; background:var(--rar,var(--rule));}
.a-name{flex:1; min-width:0; font-family:var(--display); font-weight:600; font-size:calc(14.5px * var(--fs));}
.a-name small{display:block; font-family:var(--mono); font-weight:400; font-size:calc(10px * var(--fs)); color:var(--ink-faint); margin-top:2px;}
.a-meta{display:flex; align-items:center; gap:6px; flex-wrap:wrap; justify-content:flex-end;}
.af-area{min-height:150px; resize:vertical; font-family:var(--serif); line-height:1.6;}
.af-count{display:block; margin-top:5px; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.04em;}
.af-count.ok{color:var(--good);}
.af-count.warn{color:var(--ochre);}

/* map placeholder */
.map-wrap{position:relative; background:var(--card); border:1px solid var(--rule); border-radius:14px; box-shadow:var(--shadow-md); padding:40px; overflow:hidden; min-height:360px; display:grid; place-items:center; text-align:center;}
.map-wrap svg{position:absolute; inset:0; width:100%; height:100%; opacity:.5;}
.map-wrap .inner{position:relative; z-index:1;}
.map-wrap .big{font-family:var(--han); font-size:calc(46px * var(--fs)); color:var(--geo); opacity:.9; margin-bottom:8px;}

/* ===== Atlas: globe + timeline ===== */
.atlas-head{margin-bottom:14px;}
.page:has(.atlas){animation:none;}
.atlas{display:contents;}
/* ---------- the two atlases (Sep 2026, on request) ----------
   `.atlas` is `display:contents`, so this bar has no box of its own to sit in — it is fixed to the
   viewport over the stage, like every other control on this page. Centred at the top rather than in a
   corner: it names which of two maps is on screen, which is a fact about the whole page, where the
   corners belong to the search and the legend. It is a pair of buttons rather than a switch because a
   switch says "on/off" about one thing and these are two things. */
.atlas-tabs{
  /* …and the tab pill, for `.globe-stage`'s reason: it sits ABOVE the stage rather than inside it, so it
     does not inherit the narrowing and is a two-finger target of its own. */
  touch-action:pan-y;
  position:fixed; z-index:6; top:calc(var(--bar-h) + 12px); left:50%; transform:translateX(-50%);
  display:inline-flex; gap:2px; padding:3px; border-radius:99px;
  background:color-mix(in srgb, var(--card) 92%, transparent); border:1px solid var(--rule);
  box-shadow:var(--shadow-sm); backdrop-filter:saturate(150%) blur(9px); -webkit-backdrop-filter:saturate(150%) blur(9px);
}
.at-tab{
  border:0; background:none; cursor:pointer; border-radius:99px; padding:5px 13px;
  font-family:var(--sans); font-size:calc(12px * var(--fs)); font-weight:600; color:var(--ink-soft);
  transition:background .18s var(--ease), color .18s var(--ease);
}
.at-tab:hover{color:var(--ink);}
.at-tab.on{background:var(--indigo); color:#fff;}
.at-tab:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
/* The way back to the card the reader came from (Sep 2026). It is a tab in shape so it belongs to the
   row, and OUTLINED rather than filled: `.on` means "the atlas you are looking at", and a solid third
   button would read as a third atlas. */
.at-back{
  border:1px solid var(--rule); background:color-mix(in srgb, var(--card) 88%, transparent);
  color:var(--ink-soft); margin-inline-start:6px;
}
.at-back:hover{background:var(--card); color:var(--ink);}
@media (max-width:640px){
  .atlas-tabs{top:10px; left:12px; transform:none;}   /* the search chip owns the right corner at this width */
  .at-tab{padding:5px 10px; font-size:calc(11px * var(--fs));}
}
/* THE PERSONAL ATLAS'S POPUP IS A CARD, and a card brings its own margins — see showMinePopup. The panel
   is a narrow column, so the card inside it drops the study page's own width cap and takes what there is. */
.cp-cardback{margin:0; max-width:none; box-shadow:none; border:0; background:none; padding:0;}
.cp-cardback .reveal{margin:0;}
/* The panel is a ~340px column, which is the width the study card's ≤640px rules were written for — so
   the answer box stacks here for the same reason it stacks on a phone: side by side, the term and a
   four-tile grid of figures each get half of 340px and both break mid-word. */
.cp-cardback .answer{flex-direction:column; align-items:stretch; padding:12px 14px;}
.cp-cardback .card-facts{max-width:none; width:100%; margin-top:10px;}
.cp-cardback .card-img{margin-bottom:12px;}
/* WHAT THE PERSONAL ATLAS'S PANEL DOES NOT REPEAT (Sep 2026, on request). Four things on it said
   something the card back beside them already says: the "Answer" label above a term that is the card's
   own heading; a "From your card" section head over the only section there is; the title bar, which
   prints that same term a second time; and the dating, which the card's date line carries. Each is taken
   off by a rule rather than by a write in `showMinePopup`, because the title bar has to come BACK when
   the sheet is collapsed — a collapsed sheet is nothing but its title bar — and because the "Answer"
   label is inside markup that function does not build. Above the sheet's breakpoint `.cp-shut` is never
   applied, so on the desktop the name is simply always off; the × and the chevron keep their row. */
.cp-mine .cp-span{display:none;}
.cp-mine #cpDescSec > .cp-sec-head{display:none;}
.cp-mine .cp-cardback .answer .label{display:none;}
.country-pop.cp-mine:not(.cp-shut) .cp-titlemain{display:none;}
/* `[hidden]` loses to an author `display:flex` — the trap this file already carries for `.ces-imgpanel`
   and `.af-src`. `showMinePopup` hides this row, and without the rule the Atlas's own "Through the ages"
   button stayed on a panel that is showing a card rather than a country. */
.cp-tools[hidden]{display:none;}
/* SEVERAL CARDS, ONE PLACE (Oct 2026, on request): the personal atlas's popup browses every card about a
   place — "2 of 9", an arrow either side, and a swipe on the body. `pan-y` lets a phone scroll the card
   and still hand a SIDEWAYS drag to the page, which is what the swipe is read from (see `mineStep`). */
.cp-stack{display:flex; align-items:center; justify-content:center; gap:10px; margin:0 0 8px; padding:4px 0; border-bottom:1px solid var(--rule);}
.cp-stack[hidden]{display:none;}
.cp-stk-n{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink-soft); min-width:9em; text-align:center;}
.cp-stk-btn{display:inline-flex; align-items:center; justify-content:center; width:30px; height:30px; appearance:none; border:1px solid var(--rule); background:none; border-radius:99px; cursor:pointer; color:var(--ink-soft); padding:0;}
.cp-stk-btn svg{width:14px; height:14px;}
.cp-stk-btn:hover, .cp-stk-btn:focus-visible{border-color:var(--indigo); color:var(--indigo);}
.cp-mine .cp-cols{touch-action:pan-y;}
/* The era cartouche is centred at the top of the stage and so is the tab bar — on the personal atlas the
   two were printed over each other. The bar is a control and the cartouche a caption, so the caption is
   the one that moves down. */
.atlas-mine .map-cartouche{top:52px;}
@media (max-width:640px){ .atlas-mine .map-cartouche{top:14px;} }   /* there the bar is top-LEFT, so nothing is in its way */
/* A PINCH OVER THE ATLAS MUST NOT ZOOM THE PAGE (Sep 2026, on a bug report: "on tablet, sometimes when
   trying to zoom in on the atlas it zooms in on the webpage itself instead, and then it becomes nearly
   impossible to zoom out of it again"). The CANVAS has always declared `touch-action:none`, so a pinch
   that begins on the globe zooms the globe; what the report is about is a pinch that begins anywhere
   ELSE — the strip beside the canvas, the legend, the timeline, the tab bar — where `body`'s own
   `pan-y pinch-zoom` still applied and the browser zoomed the document instead.
   AND THE SECOND HALF OF THE REPORT IS WHY IT STICKS: once the page is zoomed, nearly the whole screen is
   the canvas, which refuses the pinch that would undo it, so the reader has almost nowhere to put two
   fingers. Prevention is therefore the fix rather than a recovery.
   `pan-y` RATHER THAN `none`, and that is load-bearing: `.country-pop` is a descendant of `.globe-stage`
   and is a scrolling sheet on a phone, and `none` on an ancestor takes a descendant's scrolling with it.
   `pan-y` forbids exactly the page zoom and leaves the sheet scrolling.
   THE TOP BAR IS DELIBERATELY LEFT PINCHABLE. Narrowing it would be a site-wide change to a browser
   accessibility feature for the sake of one route — and it is also the strip a reader who has somehow
   zoomed the page can still pinch back out on, which is the stuck half of the report answered directly. */
.globe-stage{position:fixed; left:0; right:0; top:var(--bar-h); bottom:calc(var(--timebar-h) + var(--tabbar-h) + env(safe-area-inset-bottom)); z-index:1; overflow:hidden; touch-action:pan-y;}
.globe-stage canvas{position:absolute; inset:0; width:100%; height:100%; display:block; cursor:grab; touch-action:none;}
/* NO TAP HIGHLIGHT ON A MAP (Sep 2026, on a bug report: "in the Find It minigame, when i select a country,
   for a split second the whole screen lights up as if everything were selected"). The globe's canvas is
   one element the size of the stage and it carries click listeners, so a mobile browser paints its own
   tap highlight — a translucent wash over the WHOLE element — on every press, which on a full-screen map
   reads exactly as every country being selected at once. The press already has its own answer on the
   map (the tinted pick), so the browser's is taken away here, and on every other canvas the site draws. */
canvas{-webkit-tap-highlight-color:transparent;}
.globe-stage canvas:active{cursor:grabbing;}
.globe-hint{position:absolute; left:clamp(16px,4vw,40px); bottom:16px; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint); opacity:.6; pointer-events:none; user-select:none;}
/* "drag to rotate · scroll or +/− to zoom" is written for a mouse — on touch it is three lines of wrong */
@media (hover:none){.globe-hint{display:none;}}
/* on-screen zoom controls (right-centre) — a wheel-free way to zoom that works in any host/preview */
.globe-zoom{position:absolute; right:clamp(16px,4vw,40px); bottom:14px; z-index:5; display:flex; flex-direction:column; gap:6px;}   /* bottom-right — the legend owns the top-right (they collided at top:50% on short viewports) */
.gz-help{font-size:calc(15px * var(--fs)); font-weight:600;}
.globe-stage canvas:focus-visible{outline:2px solid var(--indigo); outline-offset:-3px;}
/* limb shading + atmosphere halo as DOM layers (glow under the canvas, shade over it) — always visible, even while
   moving/zooming, and immune to the canvas-compositor ghosting that forced the old canvas gradients to settle-only */
.globe-limb-glow, .globe-limb-shade{position:absolute; border-radius:50%; pointer-events:none;}
.globe-limb-shade{z-index:1;}
/* "Find it" game mode: the globe is the board — everything that would reveal answers hides, the timebar locks.
   The country info panel STAYS available: it opens on every guess (right or wrong) so the game still teaches. */
.atlas-game .globe-search, .atlas-game .globe-legend, .atlas-game .globe-hovername, .atlas-game .globe-hint{display:none !important;}
/* The timebar goes ENTIRELY (Aug 2026, on request). It was left on screen, inert and slightly dimmed, so the
   board would look like the Atlas the reader knows — but the round names its own year in the question, the
   rail cannot be touched and stepping years is exactly what the game must not allow, so it was a fifth of a
   phone screen spent on a control with nothing to say. Setting --timebar-h to 0 HERE rather than on :root is
   what gives that height back: .globe-stage is a descendant, so it inherits the zero and grows into the
   space, and every other rule written against the variable is left describing the ordinary Atlas. */
.atlas-game{--timebar-h:0px;}
.atlas-game .atlas-timebar{display:none;}
.map-game{position:absolute; top:14px; left:50%; transform:translateX(-50%); z-index:8; width:min(430px, calc(100% - 28px));}
.map-game[hidden]{display:none;}
.mg-card{background:var(--card); border:1px solid var(--rule); border-radius:13px; padding:13px 18px 14px; box-shadow:0 12px 32px -14px rgba(0,0,0,.5); display:flex; flex-direction:column; gap:7px;}
.mg-head{display:flex; justify-content:space-between; align-items:baseline; gap:12px;}
.mg-round{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint);}
.mg-score{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.06em; color:var(--indigo);}
.mg-q{font-family:var(--display); font-size:calc(17px * var(--fs)); font-weight:600; letter-spacing:-.01em; line-height:1.3; color:var(--ink);}
.mg-q b{color:var(--indigo);}
.mg-feedback{font-size:calc(12.5px * var(--fs)); color:var(--ink-soft);}
.mg-feedback[hidden]{display:none;}
/* A TAP SELECTS AND THE BUTTON COMMITS (Aug 2026, on request) — so the card carries a row of actions
   rather than one Next button. `.mg-acts` collapses when every button in it is hidden: as a bare flex
   row it would still spend the card's 7px gap on nothing, which on a panel this small reads as the
   feedback line having drifted away from the question. */
.mg-acts{display:flex; align-items:center; gap:8px; flex-wrap:wrap;}
.mg-acts:not(:has(> button:not([hidden]))){display:none;}
.mg-confirm[hidden], .mg-clear[hidden], .mg-next[hidden]{display:none;}
.mg-clear{padding-inline:12px;}
/* first-visit coach marks */
.atlas-help{position:absolute; inset:0; z-index:9; display:flex; align-items:center; justify-content:center; background:color-mix(in srgb, var(--paper) 42%, transparent); backdrop-filter:blur(2px); -webkit-backdrop-filter:blur(2px);}
.atlas-help[hidden]{display:none;}
.ah-card{position:relative; width:min(430px, calc(100% - 36px)); background:var(--card); border:1px solid var(--rule); border-radius:15px; padding:20px 24px; box-shadow:0 18px 44px -18px rgba(0,0,0,.55); display:flex; flex-direction:column; gap:11px;}
.ah-card h3{font-family:var(--display); font-weight:600; font-size:calc(19px * var(--fs)); letter-spacing:-.01em; margin:0 18px 0 0;}
.ah-tip{font-size:calc(13px * var(--fs)); line-height:1.5; color:var(--ink-soft);}
.ah-tip b{color:var(--ink);}
.ah-close{position:absolute; top:10px; right:11px; appearance:none; border:0; background:transparent; color:var(--ink-faint); font-size:calc(17px * var(--fs)); width:24px; height:24px; border-radius:7px; cursor:pointer; display:flex; align-items:center; justify-content:center;}
.ah-close:hover{background:var(--rule); color:var(--ink);}
#ahGo{align-self:flex-start; margin-top:3px;}
.gz-btn{width:34px; height:34px; border-radius:9px; border:1px solid var(--rule); background:color-mix(in srgb, var(--card) 92%, transparent); color:var(--ink-soft); font-size:calc(20px * var(--fs)); line-height:1; font-weight:400; cursor:pointer; display:grid; place-items:center; box-shadow:var(--shadow-sm); backdrop-filter:saturate(150%) blur(9px); -webkit-backdrop-filter:saturate(150%) blur(9px); transition:background .15s, color .15s, border-color .15s; user-select:none; touch-action:manipulation;}
.gz-btn:hover{background:var(--card); color:var(--ink); border-color:var(--indigo);}
.gz-btn:active{transform:translateY(1px);}
/* country info popup — one at a time, just above the timeline. Three columns: summary | year | stat tiles */
/* max-height caps the box at a fraction of the globe-stage (which has a definite height — position:fixed with top+bottom),
   on EVERY viewport, not just phones — so on a tablet or a short/landscape window the popup can't fill the screen and
   push the × off the top. It's a flex column whose .cp-cols scrolls internally, keeping the globe + the × always visible. */
/* desktop: a vertical panel on the LEFT of the stage (the legend lives top-right, under the search). ≤720px reverts to a
   bottom sheet (see the media block). Still a flex column whose .cp-cols scrolls internally — never put overflow on
   .country-pop itself or the × scrolls away. */
/* `--cp-sheet` IS THE BREAKPOINT, DECLARED ONCE. Which of the two the reader gets is a stylesheet
   decision, and app.js has to know it too — the pager, the dots, the fold and the fitted height are all
   JS. It was a `matchMedia("(max-width:720px)")` in app.js beside a `@media (max-width:720px)` here, i.e.
   the same figure written twice in two files, which is exactly how the tablet came to get half of each
   system. app.js asks the element (`cpSheetMode`), the way tourPlace asks its overlay for its computed
   `align-items`, so there is one number and it is this one. */
.country-pop{--cp-sheet:0; position:absolute; left:clamp(16px,4vw,40px); top:16px; bottom:16px; width:min(360px, calc(100% - 32px)); max-height:none; display:flex; flex-direction:column; background:var(--card); border:1px solid var(--rule); border-radius:13px; padding:15px 20px; box-shadow:0 16px 40px -16px rgba(0,0,0,.5); z-index:7;}
.country-pop[hidden]{display:none;}
/* It arrives rather than appearing (Aug 2026). An animation on the frame it stops being `hidden`, not a
   transition: the element is reused for every place, so there is no insertion for a transition to catch.
   The HEIGHT is deliberately left out of it — the phone sheet's height is fitted to the page on screen
   (cpApplyH) and measured while it settles, and an animating height would be measured mid-flight. */
.country-pop:not([hidden]){animation:cpIn .2s var(--ease) both;}
@keyframes cpIn{from{opacity:0; transform:translateY(8px);} to{opacity:1; transform:none;}}
.cp-close{position:absolute; top:9px; right:10px; appearance:none; cursor:pointer; width:24px; height:24px; border-radius:7px; border:none; background:transparent; color:var(--ink-faint); font-size:calc(17px * var(--fs)); line-height:1; display:flex; align-items:center; justify-content:center; z-index:1;}
.cp-close:hover{background:var(--rule); color:var(--ink);}
/* the title block sits ABOVE the scroller and never moves: on the phone the sections below it are pages you
   swipe between, and a name that slid away with the first swipe would leave the figures unlabelled */
.cp-head{flex:none;}
/* THE PANEL NEVER SCROLLS SIDEWAYS AND ITS SCROLLBAR IS NOT DRAWN (Aug 2026, on a bug report about the
   desktop panel). Three separate causes, all of which look like one bug:
   · a GRID ITEM's `min-width` is `auto`, so any child wider than the track widens the track rather than
     being contained by it — and this panel's children include citation links, whose visible text IS a URL
     (see linkifySrcItem) and which contain no break opportunity at all. `min-width:0` on the items plus
     `overflow-wrap:anywhere` on the panel is what actually contains them; `overflow-x:clip` alone would
     hide the overflow while leaving the text unreadable.
   · `overflow-x:clip` rather than `hidden`, and never the `overflow` shorthand: `clip` beside an untouched
     `overflow-y:auto` is the one pairing that stops sideways scrolling without making a second scroll
     container — which is the same rule `clipStageFor` follows for the page swipe.
   · the bar itself. The panel is a narrow column against the globe and a scrollbar down its inside edge
     reads as a seam in the map; the phone's pages already hide theirs for the same reason. */
.cp-cols{display:grid; grid-template-columns:1fr; gap:14px; align-content:start; overflow-y:auto; overflow-x:clip; min-height:0; padding-right:2px;
  scrollbar-width:none; -ms-overflow-style:none; overflow-wrap:anywhere;}
.cp-cols::-webkit-scrollbar{display:none;}
.cp-cols > *{min-width:0;}
.cp-crumb{display:flex; align-items:center; gap:6px; margin:0 20px 4px 0; min-width:0;}
.cp-crumb[hidden]{display:none;}   /* the author display:flex would otherwise defeat the hidden attribute (codebase convention) */
.cp-crumb-link{appearance:none; border:0; background:none; padding:0; cursor:pointer; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--indigo); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.cp-crumb-link:hover{text-decoration:underline;}
.cp-crumb-sep{color:var(--ink-faint); font-size:calc(11px * var(--fs));}
.cp-crumb-cur{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink-faint); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.cp-tools{display:flex; gap:6px; margin:2px 0 6px; flex-wrap:wrap;}
.cp-tool{display:inline-flex; align-items:center; gap:5px; appearance:none; border:1px solid var(--rule); background:none; border-radius:99px; padding:3px 10px; cursor:pointer; font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-soft);}
.cp-tool svg{width:11px; height:11px;}
.cp-tool:hover{border-color:var(--indigo); color:var(--indigo);}
.cp-hist{display:flex; flex-direction:column; margin:0 0 8px; border:1px solid var(--rule); border-radius:9px; overflow:hidden;}
.cp-hist[hidden]{display:none;}
.cph-row{display:flex; align-items:baseline; gap:9px; appearance:none; border:0; background:none; padding:6px 10px; cursor:pointer; text-align:left; font:inherit; color:var(--ink);}
.cph-row + .cph-row{border-top:1px solid color-mix(in srgb, var(--rule) 55%, transparent);}
.cph-row:hover{background:color-mix(in srgb, var(--indigo) 8%, transparent);}
.cph-row.cur{background:color-mix(in srgb, var(--indigo) 12%, transparent);}
.cph-years{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.04em; color:var(--ink-faint); flex:none; min-width:74px;}
.cph-name{font-size:calc(12px * var(--fs)); font-weight:600; line-height:1.3; min-width:0;}
/* the title and its discovery chip share one row — the chip names the place beside it, and a line of its
   own cost the short phone sheet a whole line before the description started. The 20px right margin that
   used to sit on .cp-name moves up here: it is what clears the × button.
   THE ROW ITSELF NO LONGER WRAPS (Sep 2026, on a bug report: a long name, with the chip beside it, pushed
   the × onto a second line and it appeared at the sheet's bottom LEFT). Four things on one wrapping line
   means the last two — the chevron and the × — are the ones pushed over, so the name and its chip are
   wrapped in `.cp-titlemain`, which is the only item allowed to shrink and the only one that wraps.
   The controls are `align-self:flex-start`, so a name running to two lines keeps them at the TOP right
   rather than centring them against it; on a one-line name the row is the buttons' own height and the two
   rules agree. */
.cp-titlerow{display:flex; align-items:center; gap:8px; flex-wrap:nowrap; margin:0 20px 2px 0;}
.cp-titlemain{display:flex; align-items:center; gap:8px; flex-wrap:wrap; flex:1 1 auto; min-width:0;}
.cp-titlerow > .cp-more,.cp-titlerow > .cp-close{align-self:flex-start;}
/* The × lives INSIDE the title row (see the markup note in app.js) and is still absolutely positioned
   here, so on the desktop it keeps the panel's top-right corner and the row's 20px right margin is still
   what clears it. `margin-left:auto` is what puts it last once the sheet sets it back in flow. */
.cp-titlerow .cp-close{margin-left:auto;}
/* The phone sheet's reveal chevron is drawn ONLY on the phone (see the ≤720px block): the desktop panel
   already runs the height of the stage, so there is nothing to reveal and a control that does nothing is
   worse than none. It must be declared HERE, above that block — a media query adds no specificity, so a
   base rule sitting after it would silently win and the button would never appear at any width.
   `hidden` is set in JS as well (cpSyncMore), which takes it out of the tab order too. */
.cp-more{display:none;}
.cp-name{font-family:var(--display); font-weight:600; font-size:calc(17px * var(--fs)); letter-spacing:-.01em; line-height:1.22; color:var(--ink); margin:0; min-width:0;}
.cp-span{font-family:var(--mono); font-weight:400; font-size:calc(10.5px * var(--fs)); letter-spacing:.03em; color:var(--ink-faint); margin:0 0 7px;}
.cp-span:empty{display:none;}
.cp-desc{font-size:calc(12px * var(--fs)); line-height:1.5; color:var(--ink-soft);}
/* the popup's three sections (description / year / figures) fold away individually. The head is the whole row,
   so the year number doubles as its own section's header and stays readable while the paragraph is shut. */
.cp-sec-head{display:flex; align-items:center; justify-content:space-between; gap:8px; width:100%; appearance:none; border:0; background:none; padding:0 0 6px; cursor:pointer; text-align:left; color:var(--ink);}
.cp-sec-t{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.07em; text-transform:uppercase; color:var(--ink-faint);}
.cp-sec-head:hover .cp-sec-t{color:var(--ink-soft);}
.cp-sec-chev{width:13px; height:13px; flex:none; color:var(--ink-faint); transition:transform .18s var(--ease);}
.cp-sec.collapsed .cp-sec-chev{transform:rotate(-90deg);}
.cp-sec.collapsed .cp-sec-body{display:none;}
.cp-sec.collapsed .cp-sec-head{padding-bottom:0;}
.cp-year{border-top:1px solid var(--rule); padding-top:10px;}
.cp-year-num{font-family:var(--display); font-weight:600; font-size:calc(15px * var(--fs)); letter-spacing:-.01em; color:var(--ink);}
.cp-year-desc{font-size:calc(12px * var(--fs)); line-height:1.5; color:var(--ink-soft);}
.cp-statsec{border-top:1px solid var(--rule); padding-top:10px;}
/* `min-width:0` ON THE TILES, and it is load-bearing (Sep 2026, measured).
   A grid track sized `1fr` will not shrink below its content's minimum, so a tile holding one long
   unbroken figure or place name pushes its column past half the panel and the whole grid past
   `.cp-cols`. Measured on a 390px sheet: 3px of horizontal overflow, which `overflow-x:clip` hides
   from the reader as a clipped final character rather than as a scrollbar — so nothing looked wrong
   and nothing was reported. It only showed up once the webfonts were SELF-HOSTED: until then a
   sandbox run had no webfonts at all and the fallback stack is narrower, so every measurement of this
   panel had been taken in the wrong type. `.cp-cols > *` has carried this rule all along; the tiles
   are one level further in, inside `.cp-stats`, and were missed. */
.cp-stats{display:grid; grid-template-columns:1fr 1fr; gap:7px; align-content:start; min-width:0;}
.cp-stats > *{min-width:0;}
.cp-tile{position:relative; border:1px solid var(--rule); border-radius:8px; padding:7px 9px; display:flex; flex-direction:column; gap:3px; min-width:0;}
.cp-tile .cp-k,.cp-tile .cp-v{min-width:0; overflow-wrap:anywhere;}
.cp-k{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint);}
.cp-v{font-family:var(--display); font-size:calc(13.5px * var(--fs)); font-weight:600; color:var(--ink); line-height:1.1; cursor:help; align-self:flex-start; outline:none;}
/* hovering (or focusing) a number shows a small speech bubble naming its source */
/* THE BUBBLE IS ANCHORED ON BOTH EDGES, and that is what stops the sheet reporting a sideways scroll
   (Sep 2026, bisected in the page). It is absolutely positioned inside `.cp-tile`, and an absolutely
   positioned descendant counts towards an ancestor's scrollWidth — so `width:max-content` made the
   tile 163 wide against a 158 client box, `.cp-stats` 331 against 326, and `.cp-cols` 331 against 328.
   `overflow-x:clip` on `.cp-cols` hid it from the reader as a clipped bubble rather than as a
   scrollbar, so nothing looked wrong and nothing was reported; and it was invisible to every sandbox
   run until the webfonts were self-hosted, because until then there were no webfonts at all and the
   fallback stack is narrower. It had therefore been happening on the live site the whole time.
   `max-width` alone did NOT fix it — measured, the clamped bubble was 140.5px inside a 158px tile and
   the overflow stayed at 5px, so the excess is in how `max-content` is resolved rather than in the
   painted box. `left` AND `right` removes `max-content` from the question outright: the bubble is
   exactly the tile's width less its insets, and wraps, which `white-space:pre-line` already allows
   and which is what a source name in a small box wants anyway. */
.cp-v[data-tip]::after{content:attr(data-tip); position:absolute; bottom:calc(100% + 6px); left:7px; right:7px; white-space:pre-line; width:auto; max-width:none; background:var(--ink); color:var(--card); font-family:var(--sans); font-size:calc(10px * var(--fs)); font-weight:500; letter-spacing:0; text-transform:none; padding:4px 8px; border-radius:6px; box-shadow:var(--shadow-md); opacity:0; transform:translateY(3px); pointer-events:none; transition:opacity .12s ease, transform .12s ease; z-index:9;}
.cp-v[data-tip]::before{content:""; position:absolute; bottom:calc(100% + 1px); left:15px; border:5px solid transparent; border-top-color:var(--ink); opacity:0; transition:opacity .12s ease; z-index:9;}
.cp-v[data-tip]:hover::after, .cp-v[data-tip]:focus::after{opacity:1; transform:translateY(0);}
.cp-v[data-tip]:hover::before, .cp-v[data-tip]:focus::before{opacity:1;}
/* TABLETS TAKE THE SHEET TOO (Aug 2026, on request: "on tablet format, the Atlas should use the same
   popup system as mobile, not the desktop one"). The left-hand panel is written for a wide window — it
   runs the full height of the stage down one edge, which on a 900px tablet held in the hand covers a
   third of the globe with a column of prose the reader has not asked to read yet. The sheet is the
   better answer at that width for the reason it is on a phone: it is short, it opens at the name, and
   the map stays the thing on screen. 1024px is the iPad's landscape width, so both orientations of the
   commonest tablet land here. */
@media (max-width:1024px){
  .country-pop{--cp-sheet:1;}
  /* phones and tablets: back to a BOTTOM sheet, so the globe + the × stay visible. The sheet is short, and
     four sections stacked in it buried the figures three scrolls down — which is why they lay SIDE BY SIDE
     and were swiped between until Sep 2026, when the figures were moved to the TOP of the stack instead
     (on request: "users currently need to swipe right to see the country data boxes — instead, move them
     to above the country background paragraphs so it is all in one page"). Reading them first is the same
     fix without the gesture, and a gesture is the worse half: a page reached only by swiping is a page a
     reader who does not swipe never learns is there. So the sections stack and scroll, as they do on the
     desktop, and `order` — rather than a moved node — puts the figures above the paragraphs HERE only,
     leaving the desktop column, which has room for both and is read top to bottom, as it was.
     An empty section is still dropped outright rather than shown collapsed: on a box this size a header
     with nothing under it spends a line on an absence. */
  .country-pop{left:50%; top:auto; bottom:14px; transform:translateX(-50%); width:calc(100% - 28px); max-height:52%; padding:15px 16px 8px;}
  /* the sheet is centred by a transform, so its entrance has to carry that translate rather than replace it */
  .country-pop:not([hidden]){animation-name:cpInSheet;}
  @keyframes cpInSheet{from{opacity:0; transform:translateX(-50%) translateY(10px);} to{opacity:1; transform:translateX(-50%);}}
  /* THE FOLD (Aug 2026, on request — it was a jumpcut). The height is an explicit px value in both states
     (cpApplyH: the measured name, or the page's own need), so it is the one thing here CSS can ease; the
     52% cap gives way to it entirely, and the head gives up its share of the box first so the title
     survives all the way down to the floor. What CSS cannot ease is the `display:none` below, which is why
     JS applies .cp-shut at the two ENDS of the movement rather than at the start of it — keep this duration
     in step with CP_FOLD_MS in app.js. */
  .country-pop.cp-sized{max-height:none; overflow:hidden; transition:height .26s var(--ease);}
  .cp-sized .cp-head{max-height:none; flex:0 1 auto; min-height:0;}
  .cp-head{max-height:46%; overflow-y:auto;}   /* "Through the ages" can run to a dozen rows — cap it, or it pushes the sections off the sheet */
  .cp-cols{flex:1; gap:12px;}
  .cp-cols > .cp-sec.cp-blank{display:none;}   /* a header with nothing under it spends a line of a small box on an absence */
  /* THE FIGURES COME FIRST HERE. `order` rather than a moved node, so the desktop panel keeps its own
     order (see the block comment above) — the cost is that the sheet's visual order and its DOM order
     differ by one section, which for a four-part information panel read straight through is a fair trade
     against a layout written twice. The rules the grid gave the figures as a MIDDLE section come off with
     it: a separator above the first thing in a box is a line under nothing, and it moves to the
     description, which is now what follows them. */
  .cp-cols > .cp-statsec{order:-1; border-top:0; padding-top:0;}
  .cp-cols > .cp-descsec{border-top:1px solid var(--rule); padding-top:10px;}
  /* ---- SHUT BY DEFAULT (Aug 2026, on request) ----
     A tap on a country asks what it is, and the name is the answer; everything else waits for the chevron.
     The height is set in JS (see cpApplyH — the floor is measured, since a long name wraps and a drilled
     place carries a breadcrumb), so all this does is take the parts below the title out of the box and
     turn the chevron over. `overflow:hidden` on the sheet is already set by .cp-sized. */
  /* No border and no fill (Aug 2026, on request): the chevron sits on the title's own line, and a boxed
     tile there reads as a second thing to press rather than as the name's own control. The hit area stays
     30px — the box was never what made it tappable. */
  .cp-more{display:inline-flex; align-items:center; justify-content:center; margin-left:auto; flex:none;
    width:30px; height:30px; padding:0; border:none; border-radius:9px;
    background:transparent; color:var(--ink-soft); cursor:pointer; transition:transform .22s var(--ease);}
  .cp-more svg{width:17px; height:17px;}
  .cp-more[aria-expanded="true"]{transform:rotate(180deg);}
  /* ---- THE × JOINS THE TITLE'S LINE (Aug 2026, on a bug report) ----
     On the sheet it stops being absolutely positioned and becomes the last item of `.cp-titlerow`, so the
     name, the reveal chevron and the close sit on one baseline and the row's own `align-items:center`
     keeps them there however the name wraps. The row's 20px right margin went with the absolute
     positioning — it existed only to clear the corner button — and `margin-left:auto` moves to the
     CHEVRON, which is now the first of the two controls to be pushed right.
     It matches the chevron's 30px box rather than keeping its own 24px one: two controls of different
     sizes side by side on the same line read as two different kinds of thing. */
  .cp-titlerow{margin-right:0;}
  .cp-titlerow .cp-close{position:static; margin-left:0; width:30px; height:30px; border-radius:9px; font-size:calc(19px * var(--fs)); color:var(--ink-soft); flex:none;}
  .cp-shut .cp-cols,.cp-shut .cp-tools,.cp-shut .cp-hist{display:none;}
  .cp-shut .cp-head{overflow:visible;}
  /* ---- A SHUT SHEET PUTS THE CHEVRON BEHIND THE NAME (Sep 2026, on request: "on mobile when only the
     name is shown in the popup with the rest collapsed, put the chevron just behind the name instead of
     in the top right next to the X close button") ----
     Open, the chevron is the far end of a full row and belongs with the ×. SHUT, that row IS the whole
     sheet — a name at the left and two buttons at the right, with the control that opens the thing sitting
     against the one that closes it, which is a pair of similar marks a thumb has to tell apart. Letting
     `.cp-titlemain` shrink to its content walks the chevron back to the name it opens; `margin-left:auto`
     moves from the chevron to the ×, so the close stays in the corner where every close is. */
  .cp-shut .cp-titlemain{flex:0 1 auto;}
  .cp-shut .cp-more{margin-left:0;}
  .cp-shut .cp-titlerow .cp-close{margin-left:auto;}
}
/* map editor toolbar (Edit → Timeline → "Edit on globe") */
.map-edit-bar{position:absolute; top:14px; left:50%; transform:translateX(-50%); display:flex; align-items:center; gap:13px; flex-wrap:wrap; max-width:calc(100% - 28px); background:var(--card); border:1px solid var(--rule); border-radius:11px; padding:8px 12px; box-shadow:0 10px 28px -14px rgba(0,0,0,.5); z-index:8;}
.map-edit-bar[hidden]{display:none;}
.meb-title{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink-faint);}
.meb-title b{color:var(--ink); font-weight:700;}
.meb-tools{display:flex; gap:5px;}
.meb-tool,.meb-act,.meb-done{appearance:none; cursor:pointer; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.04em; text-transform:uppercase; font-weight:600; padding:5px 10px; border-radius:7px; border:1px solid var(--rule); background:transparent; color:var(--ink-soft);}
.meb-tool:hover,.meb-act:hover:not(:disabled){color:var(--ink);}
.meb-tool.sel{background:var(--indigo); border-color:var(--indigo); color:#fff;}
.meb-tip{font-size:calc(11px * var(--fs)); color:var(--ink-faint); flex:1; min-width:130px;}
.meb-acts{display:flex; gap:6px; align-items:center;}
.meb-act:disabled{opacity:.4; cursor:default;}
.meb-act.danger{color:#c2516b; border-color:rgba(194,81,107,.4);}
.meb-act.danger:hover:not(:disabled){background:#c2516b; color:#fff; border-color:#c2516b;}
.meb-done{font-weight:700; border-color:var(--indigo); background:var(--indigo); color:#fff;}
.globe-legend{position:absolute; right:clamp(16px,4vw,40px); top:60px; z-index:5; background:color-mix(in srgb, var(--card) 92%, transparent); border:1px solid var(--rule); border-radius:11px; padding:9px 13px; box-shadow:var(--shadow-sm); user-select:none; backdrop-filter:saturate(150%) blur(9px); -webkit-backdrop-filter:saturate(150%) blur(9px);}   /* top-right under the search box — the left edge belongs to the country info panel */
.globe-legend .legend-head{display:flex; align-items:center; justify-content:space-between; gap:16px; cursor:move; margin-bottom:8px; touch-action:none;}
.globe-legend.collapsed .legend-head{margin-bottom:0;}
.globe-legend .legend-title{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint);}
.globe-legend .legend-collapse{border:none; background:transparent; color:var(--ink-faint); font-size:calc(15px * var(--fs)); line-height:1; width:18px; height:18px; padding:0; margin:-2px -4px -2px 0; cursor:pointer; border-radius:5px; display:flex; align-items:center; justify-content:center;}
.globe-legend .legend-collapse:hover{background:color-mix(in srgb, var(--ink) 9%, transparent); color:var(--ink);}
.lc-layers{display:none; width:16px; height:16px;}   /* shown only on the phone's collapsed chip, where a bare dash reads as nothing */
/* The personal atlas's collection switches (Sep 2026). They take the legend's own corner and shape —
   the world atlas's legend is hidden on that tab, so the two are never on screen together and a reader
   who knows where the layer switches live finds these in the same place. The count beside each name is
   how many places that collection has put on the globe, quiet and monospaced like every other figure. */
.atlas-colls{max-width:min(280px, 60vw);}
.atlas-colls .legend-row{justify-content:flex-start; gap:8px;}
.atlas-colls .legend-row span:not(.lr-n){overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
/* THE WARS OF THE YEAR (Oct 2026), above the collections in the same panel — a button per war, its two
   colours as a swatch (hatched where the map hatches it: a narrower war overlaps it), its name and years.
   The swatch colours are the war layer's own TINT_WIN / TINT_LOSE. */
/* the panel's body scrolls rather than growing over the zoom buttons below it — the wars list made it the
   one legend whose length changes with the year */
.atlas-colls .legend-body{max-height:max(150px, calc(100vh - 400px)); overflow-y:auto; overscroll-behavior:contain;}
.atlas-wars{margin-bottom:10px; padding-bottom:9px; border-bottom:1px solid var(--rule);}
.atlas-colls[data-colls="0"] .atlas-wars{margin-bottom:0; padding-bottom:0; border-bottom:none;}
.atlas-wars .aw-title{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint); margin-bottom:6px;}
.atlas-wars .aw-list{display:flex; flex-direction:column; gap:2px;}
.atlas-wars .aw-row{display:flex; align-items:center; gap:8px; width:100%; border:none; background:transparent; color:var(--ink); font:inherit; font-size:calc(12.5px * var(--fs)); line-height:1.2; text-align:left; padding:5px 4px; margin:0 -4px; border-radius:6px; cursor:pointer; min-height:28px;}
.atlas-wars .aw-row:hover, .atlas-wars .aw-row:focus-visible{background:color-mix(in srgb, var(--ink) 8%, transparent);}
.atlas-wars .aw-name{flex:1 1 auto; min-width:0; overflow:hidden; display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;}
.atlas-wars .aw-yr{flex:none; font-family:var(--mono); font-size:calc(9.5px * var(--fs)); color:var(--ink-faint);}
.atlas-wars .aw-sw{flex:none; display:inline-flex; width:16px; height:12px; border-radius:3px; overflow:hidden;}
.atlas-wars .aw-sw i{flex:1;}
.atlas-wars .aw-sw .v{background:#249454;}
.atlas-wars .aw-sw .l{background:#c63630;}
.atlas-wars .aw-row.hatched .aw-sw .v{background:repeating-linear-gradient(135deg, #249454 0 2px, transparent 2px 4px);}
.atlas-wars .aw-row.hatched .aw-sw .l{background:repeating-linear-gradient(135deg, #c63630 0 2px, transparent 2px 4px);}
.atlas-wars .aw-row.hatched .aw-sw{box-shadow:inset 0 0 0 1px var(--rule);}
.lr-n{margin-inline-start:auto; font-family:var(--mono); font-size:calc(9.5px * var(--fs)); color:var(--ink-faint);}
/* your own atlas: found / of the collection's total (Oct 2026) — the total quieter than the found */
.lr-n .lr-of{opacity:.6;}
.atlas-colls .legend-row.lr-solo{cursor:default;}
.globe-legend.collapsed .legend-body{display:none;}
.globe-legend .legend-row{display:flex; align-items:center; gap:8px; font-size:calc(12.5px * var(--fs)); color:var(--ink); cursor:pointer; line-height:1;}
.globe-legend .legend-row + .legend-row{margin-top:6px;}
/* the Heightmap layer's strength — the one legend control that is not a toggle. It sits under the row it
   belongs to, indented to the checkbox's text so it reads as that row's setting, and is hidden outright
   while the layer is off (author display:flex beats the UA [hidden] rule — codebase convention). */
.globe-legend .legend-slider{display:flex; align-items:center; gap:7px; margin:5px 0 2px 22px;}
.globe-legend .legend-slider[hidden]{display:none;}
.globe-legend .legend-slider input[type="range"]{
  flex:1; min-width:0; width:74px; height:3px; appearance:none; -webkit-appearance:none;
  background:var(--rule); border-radius:99px; cursor:pointer; margin:0;
}
.globe-legend .legend-slider input[type="range"]::-webkit-slider-thumb{
  appearance:none; -webkit-appearance:none; width:12px; height:12px; border-radius:50%;
  background:var(--indigo); border:2px solid var(--card); box-shadow:0 1px 3px rgba(0,0,0,.3); cursor:pointer;
}
.globe-legend .legend-slider input[type="range"]::-moz-range-thumb{
  width:12px; height:12px; border-radius:50%; background:var(--indigo); border:2px solid var(--card); cursor:pointer;
}
.globe-legend .legend-slider input[type="range"]:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px;}
.globe-legend .ls-val{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); color:var(--ink-faint); min-width:30px; text-align:right; font-variant-numeric:tabular-nums;}
.globe-legend .legend-row input{cursor:pointer; accent-color:var(--indigo); width:14px; height:14px; margin:0;}
/* work-in-progress notice for years that have no map yet (dismissed only by returning to a mapped year) */
.atlas-wip{position:absolute; top:clamp(14px,3vw,28px); left:clamp(14px,4vw,40px); z-index:6; max-width:310px; display:flex; flex-direction:column; gap:6px; padding:14px 16px; background:color-mix(in srgb, var(--card) 90%, transparent); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-md); backdrop-filter:saturate(140%) blur(8px); -webkit-backdrop-filter:saturate(140%) blur(8px); opacity:0; transform:translateY(-7px); visibility:hidden; transition:opacity .26s var(--ease), transform .26s var(--ease), visibility .26s; pointer-events:none;}
.atlas-wip.show{opacity:1; transform:none; visibility:visible;}
.atlas-wip strong{font-family:var(--sans); font-size:calc(13px * var(--fs)); font-weight:700; color:var(--ink); letter-spacing:.01em;}
.atlas-wip span{font-family:var(--sans); font-size:calc(12px * var(--fs)); line-height:1.5; color:var(--ink-soft);}
/* The personal atlas's empty state wears the same card as the "no map yet" note above it — the same
   thing said in the same place, so a reader meets one box on this page rather than two designs of it.
   It is SHOWN by default and hidden by the `hidden` attribute, where the note above is the other way
   round: this one is drawn once when the page mounts and never toggled, so it needs no transition and
   the attribute is the whole of its state. It takes pointer events, because it carries a button. */
.atlas-empty{position:absolute; top:clamp(56px,8vw,74px); left:clamp(14px,4vw,40px); z-index:6; max-width:310px; display:flex; flex-direction:column; align-items:flex-start; gap:8px; padding:14px 16px; background:color-mix(in srgb, var(--card) 92%, transparent); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-md); backdrop-filter:saturate(140%) blur(8px); -webkit-backdrop-filter:saturate(140%) blur(8px);}
.atlas-empty[hidden]{display:none;}
.atlas-empty strong{font-family:var(--sans); font-size:calc(13px * var(--fs)); font-weight:700; color:var(--ink);}
.atlas-empty span{font-family:var(--sans); font-size:calc(12px * var(--fs)); line-height:1.5; color:var(--ink-soft);}
.atlas-empty .btn{padding:6px 13px; font-size:calc(12px * var(--fs));}

/* …and the timeline, which is a full-width strip at the bottom of the screen and so the likeliest
   place on the Atlas for a thumb to land — see `.globe-stage` above. */
.atlas-timebar{touch-action:pan-y; position:fixed; left:0; right:0; bottom:calc(var(--tabbar-h) + env(safe-area-inset-bottom)); height:var(--timebar-h); z-index:30; display:flex; align-items:center; gap:26px; padding:0 74px 0 clamp(16px,4vw,40px); background:color-mix(in srgb, var(--paper) 88%, transparent); backdrop-filter:saturate(140%) blur(10px); -webkit-backdrop-filter:saturate(140%) blur(10px); border-top:1px solid var(--rule); overflow:visible;}
.tl-play{flex:none; width:34px; height:34px; border-radius:50%; appearance:none; border:1px solid var(--rule); background:var(--card); color:var(--ink-soft); cursor:pointer; display:flex; align-items:center; justify-content:center; margin-right:-8px;}
.tl-play svg{width:16px; height:16px;}
.tl-play:hover{border-color:var(--indigo); color:var(--indigo);}
.tl-play.on{background:var(--indigo); border-color:var(--indigo); color:#fff;}
/* HOW FAR BACK THE PERSONAL RAIL REACHES (Sep 2026, on request) — two chevrons with the start year
   between them, to the left of the rail. It is sized off `.tl-play` beside it rather than given numbers
   of its own, so the three controls on that edge read as one row; the label is `--mono` at the ticks' own
   size, because it names the same kind of thing the ticks under the rail do. A disabled end is dimmed
   rather than removed: a control that vanishes at the end of its range is one a reader has to re-find. */
.tl-range{flex:none; display:flex; align-items:center; gap:2px;}
.tl-rbtn{appearance:none; width:26px; height:26px; border-radius:7px; border:1px solid var(--rule); background:var(--card); color:var(--ink-soft); display:grid; place-items:center; cursor:pointer; padding:0; transition:background .15s, color .15s, border-color .15s;}
.tl-rbtn svg{width:14px; height:14px;}
.tl-rbtn:hover:not(:disabled){background:var(--indigo-wash); color:var(--indigo); border-color:color-mix(in srgb,var(--indigo) 30%,var(--rule));}
.tl-rbtn:disabled{opacity:.34; cursor:default;}
.tl-rlabel{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint); min-width:56px; text-align:center; white-space:nowrap;}
.atlas-timeline{flex:1; min-width:0;}
.tl-track{position:relative; height:32px; display:flex; align-items:center; cursor:pointer;}
.tl-rail{position:absolute; left:0; right:0; height:4px; border-radius:99px; background:var(--rule);}
.tl-fill{position:absolute; left:0; height:4px; border-radius:99px; background:color-mix(in srgb,var(--indigo) 52%, var(--rule));}
.tl-mark{position:absolute; top:50%; transform:translate(-50%,-50%); width:3px; height:12px; border-radius:2px; background:color-mix(in srgb,var(--indigo) 70%, var(--rule)); opacity:.72; appearance:none; -webkit-appearance:none; border:0; padding:0; cursor:pointer;}
.tl-mark:hover{opacity:1; height:15px;}
.tl-mark:focus-visible{opacity:1; outline:2px solid var(--indigo); outline-offset:3px;}
.tl-pin{position:absolute; top:50%; left:100%; transform:translate(-50%,-50%); width:18px; height:18px; border-radius:50%; background:var(--indigo); border:2.5px solid var(--card); box-shadow:var(--shadow-sm); cursor:grab; padding:0; appearance:none; transition:background .15s;}
.tl-pin:hover, .tl-pin.dragging{background:var(--indigo-bright);}
.tl-pin:active, .tl-pin.dragging{cursor:grabbing;}
.tl-pin:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px;}
.tl-tip{position:absolute; bottom:calc(100% + 9px); top:auto; left:50%; transform:translateX(-50%); white-space:nowrap; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.03em; color:var(--ink-soft); background:var(--card); border:1px solid var(--rule); border-radius:6px; padding:2px 8px; box-shadow:var(--shadow-sm); opacity:0; transition:opacity .15s; pointer-events:none;}
.tl-pin:hover .tl-tip, .tl-pin.dragging .tl-tip, .tl-pin:focus-visible .tl-tip{opacity:1;}
/* present-day-only legend layers on a historical era: dimmed + disabled, not hidden (the layer model stays visible) */
.globe-legend .legend-row.legend-na{opacity:.42; cursor:not-allowed;}
.globe-legend .legend-row.legend-na input{cursor:not-allowed;}
/* plate-title cartouche — "THE WORLD · 1938" in the theme display face, top-centre of the globe stage */
.map-cartouche{position:absolute; top:14px; left:50%; transform:translateX(-50%); z-index:4; pointer-events:none; user-select:none;
  font-family:var(--display); font-weight:600; font-size:calc(14px * var(--fs)); letter-spacing:.22em; color:var(--ink); white-space:nowrap;
  padding:7px 18px 6px 20px; background:color-mix(in srgb, var(--card) 88%, transparent); border:1px solid var(--rule); border-radius:3px;
  box-shadow:inset 0 0 0 3px color-mix(in srgb, var(--card) 88%, transparent), inset 0 0 0 4px var(--rule), var(--shadow-sm);}
@media (max-width:640px){.map-cartouche{display:none;}}
/* hover name tag — a DOM chip following the cursor (naming the entity under it); never triggers a canvas redraw */
.globe-hovername{position:absolute; z-index:4; pointer-events:none; user-select:none; display:flex; flex-direction:column; gap:0;
  max-width:280px; padding:4px 10px 5px; background:color-mix(in srgb, var(--card) 90%, transparent); border:1px solid var(--rule);
  border-radius:8px; box-shadow:var(--shadow-sm); backdrop-filter:saturate(150%) blur(6px); -webkit-backdrop-filter:saturate(150%) blur(6px);}
.globe-hovername[hidden]{display:none;}
.ghn-top{font-family:var(--mono); font-size:calc(8.5px * var(--fs)); letter-spacing:.09em; text-transform:uppercase; color:var(--ink-faint); line-height:1.5;}
.ghn-main{font-size:calc(13px * var(--fs)); font-weight:600; color:var(--ink); line-height:1.25;}
@media (hover:none){.globe-hovername{display:none;}}
/* atlas search — top-right of the stage; typeahead over countries, era territories and capitals */
.globe-search{position:absolute; right:clamp(16px,4vw,40px); top:16px; z-index:6; width:min(240px, 38vw);}
.globe-search input{width:100%; padding:7px 12px; font:inherit; font-size:calc(13px * var(--fs)); color:var(--ink);
  background:color-mix(in srgb, var(--card) 92%, transparent); border:1px solid var(--rule); border-radius:9px; box-shadow:var(--shadow-sm);
  backdrop-filter:saturate(150%) blur(9px); -webkit-backdrop-filter:saturate(150%) blur(9px);}
.globe-search input:focus{outline:2px solid color-mix(in srgb, var(--indigo) 55%, transparent); outline-offset:1px;}
.gs-toggle{display:none;}   /* the phone's collapsed chip — on desktop the field itself is small enough to just sit there */
/* ---- the Atlas chrome on a phone ----
   Open, the search box and the legend covered the whole top-right of a 390px screen — the map — before the
   reader had asked for either. Both collapse to a 34px chip in the corner and open on a tap: the legend
   through the toggle it already had, the search across the full width of the stage, because a 38vw field
   fits about four characters. */
@media (max-width:640px){
  /* the top bar is hidden on a phone, so the globe stage now starts at the very top of the screen —
     which on a notched device is under the status bar. These are the only pieces of chrome pinned to
     that edge, so they carry the inset themselves rather than shrinking the map for everyone. */
  .globe-search{top:calc(12px + env(safe-area-inset-top)); right:12px; width:auto;}
  .globe-search input{display:none;}
  .gs-toggle{display:grid; place-items:center; width:34px; height:34px; border-radius:50%; appearance:none; cursor:pointer;
    border:1px solid var(--rule); background:color-mix(in srgb, var(--card) 92%, transparent); color:var(--ink-soft);
    box-shadow:var(--shadow-sm); backdrop-filter:saturate(150%) blur(9px); -webkit-backdrop-filter:saturate(150%) blur(9px);}
  .gs-toggle svg{width:16px; height:16px;}
  .globe-search.open{left:12px; right:12px; width:auto;}
  .globe-search.open .gs-toggle{display:none;}
  .globe-search.open input{display:block;}
  .globe-legend{top:calc(12px + env(safe-area-inset-top)); right:56px;}
  /* …and INTO the corner when there is no search chip to sit beside: Your atlas hides the search, and the
     chip was left 44px short of the edge with nothing in the gap (Oct 2026, on a report) */
  .globe-search[hidden] ~ .globe-legend{right:12px;}
  .globe-legend.collapsed{width:34px; height:34px; padding:0; border-radius:50%; overflow:hidden;}
  .globe-legend.collapsed .legend-head{margin:0; height:100%; justify-content:center; gap:0; cursor:pointer;}
  .globe-legend.collapsed .legend-title{display:none;}
  .globe-legend.collapsed .legend-collapse{width:100%; height:100%; margin:0; border-radius:50%;}
  .globe-legend.collapsed .lc-sign{display:none;}
  .globe-legend.collapsed .lc-layers{display:block;}
  .globe-search.open ~ .globe-legend{display:none;}   /* the open field owns the top edge; the chip beside it would be under it */
}
.globe-search input::placeholder{color:var(--ink-faint);}
.gs-results{position:absolute; top:calc(100% + 6px); left:0; right:0; display:flex; flex-direction:column; overflow:hidden;
  background:var(--card); border:1px solid var(--rule); border-radius:11px; box-shadow:var(--shadow-md, var(--shadow-sm));}
.gs-results[hidden]{display:none;}   /* the author display:flex would otherwise defeat the hidden attribute (codebase convention, cf. .country-pop[hidden]) */
.gs-results:not([hidden]){animation:gsIn .15s var(--ease) both;}
@keyframes gsIn{from{opacity:0; transform:translateY(-5px);} to{opacity:1; transform:none;}}
.gs-row{display:flex; flex-direction:column; align-items:flex-start; gap:1px; padding:7px 12px; border:0; background:none; cursor:pointer; text-align:left; font:inherit; color:var(--ink);}
.gs-row + .gs-row{border-top:1px solid color-mix(in srgb, var(--rule) 55%, transparent);}
.gs-row:hover, .gs-row.active{background:color-mix(in srgb, var(--indigo) 10%, transparent);}
.gs-name{font-size:calc(13px * var(--fs)); font-weight:600; line-height:1.2;}
.gs-kind{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.07em; text-transform:uppercase; color:var(--ink-faint);}
.tl-ticks{position:relative; height:13px; margin-top:8px;}
.tl-tick{position:absolute; transform:translateX(-50%); font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint); white-space:nowrap;}
.tl-tick:first-child{transform:none;}
.tl-tick:last-child{transform:translateX(-100%);}

.atlas-yearbox{display:flex; align-items:center; gap:10px; flex:none;}
.ay-chev{appearance:none; width:34px; height:34px; border-radius:9px; border:1px solid var(--rule); background:var(--card); color:var(--ink-soft); display:grid; place-items:center; cursor:pointer; transition:background .15s, color .15s, border-color .15s; touch-action:none;}
.ay-chev:hover{background:var(--indigo-wash); color:var(--indigo); border-color:color-mix(in srgb,var(--indigo) 30%,var(--rule));}
.ay-chev:active{transform:translateY(1px);}
.ay-chev svg{width:18px; height:18px;}
.ay-display{display:flex; align-items:baseline; gap:5px; min-width:120px; justify-content:center;}
.ay-num{font-family:var(--display); font-weight:600; font-size:calc(34px * var(--fs)); line-height:1; letter-spacing:-.01em; color:var(--ink); font-variant-numeric:tabular-nums;}
.ay-era{font-family:var(--mono); font-size:calc(12px * var(--fs)); letter-spacing:.07em; color:var(--ink-faint); font-weight:600;}
@media (max-width:560px){
  /* One row could not hold a play button, a ~170px year box AND the rail: on a 390px screen the rail was
     squeezed to about 70px, which is why its five year labels piled into an 80px band as an unreadable
     smudge and stopped lining up with the marks on the rail they annotate. Two rows give the rail the whole
     width — the year and the play button above it, the rail beneath — and the stage's bottom inset follows.
     The right padding stays: it is what keeps the rail clear of the corner buttons stacked above it. */
  :root{--timebar-h:118px;}
  /* THE RAIL RUNS THE WHOLE WIDTH AND THE YEAR IS CENTRED (Sep 2026, on request: "on the mobile Atlas
     page, the timeline in the bottom should extend all the way to the right and the year should be
     centered"). Two things had to change together. The bar carried a 74px right padding — written to keep
     the rail clear of the zoom buttons — and it was never needed on this row: `.globe-zoom` is positioned
     inside `.globe-stage`, whose bottom inset IS `--timebar-h`, so those buttons stop where this bar
     begins and the rail had been giving up a fifth of a 390px screen to nothing. And the year could not
     be centred while the grid was two columns, `justify-self:center` in a cell that starts after the play
     button being centred in what is left rather than in the bar: three columns with `1fr` on each side
     put the middle one over the bar's own centre, and the rail spans all three. */
  .atlas-timebar{display:grid; grid-template-columns:1fr auto 1fr; grid-template-areas:"play year range" "rail rail rail";
    align-content:center; gap:4px 8px; padding:6px clamp(12px,4vw,20px) 8px;}
  .tl-play{grid-area:play; margin-right:0; justify-self:start;}
  /* the range control takes the cell that was empty, opposite the play button — on a phone the timebar is
     two rows and there is no "left of the rail" to put it in */
  .tl-range{grid-area:range; justify-self:end;}
  .tl-rlabel{min-width:48px;}
  .atlas-yearbox{grid-area:year; justify-self:center;}
  .atlas-timeline{grid-area:rail;}
  .ay-num{font-size:calc(27px * var(--fs));} .ay-display{min-width:88px;} .ay-chev{width:30px; height:30px;}
  .globe-hint{font-size:calc(9px * var(--fs));}
}

/* ============================================================
   RESPONSIVE
   ============================================================ */
/* ============================================================
   DECK ADD (+) BUTTONS
   ============================================================ */
.collection-actions{display:flex; align-items:center; gap:8px; flex:none;}
.collection-add,.node-add{
  flex:none; appearance:none; cursor:pointer; border-radius:9px;
  border:1px solid var(--rule); background:var(--card); color:var(--ink-faint);
  display:inline-flex; align-items:center; justify-content:center;
  transition:transform .12s var(--ease), background .18s, border-color .18s, color .18s;
}
.collection-add{width:36px; height:36px;}
/* TRY TEN (Sep 2026). A text button rather than another icon: the row already carries a + and a
   chevron, and a third glyph beside them would be a puzzle. It goes on a phone, where the row has
   space for the two controls it already had and not for a third. */
.collection-try{font-family:var(--sans); font-size:calc(11px * var(--fs)); letter-spacing:.02em; white-space:nowrap; padding:5px 10px; border:1px solid var(--rule); border-radius:999px; background:var(--card); color:var(--ink-soft); cursor:pointer;}
.collection-try:hover{color:var(--ink); border-color:var(--ink-faint);}
@media (max-width:560px){ .collection-try{display:none;} }
.node-add{width:30px; height:30px;}
.collection-add svg,.node-add svg{width:16px; height:16px;}
.collection-add:hover,.node-add:hover{border-color:var(--indigo); color:var(--indigo); background:var(--indigo-wash); transform:translateY(-1px);}
.collection-add:active,.node-add:active{transform:translateY(0) scale(.95);}
.collection-add:focus-visible,.node-add:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.collection-add.added,.node-add.added{background:var(--indigo); border-color:var(--indigo); color:#fff;}
.collection-add.added:hover,.node-add.added:hover{background:var(--indigo-bright); border-color:var(--indigo-bright); color:#fff;}

/* ============================================================
   HOME — active decks attached to the Review banner
   ============================================================ */
/* the group carries the Daily-study tile's light blue so the added-deck rows below the banner
   share its coloration (left bar + wash + earned fills) instead of sitting there colourless.
   KEEP IT IN STEP WITH `.banner`'s own --tile — the two exist to state one colour. */
.review-group{display:flex; flex-direction:column; position:relative; --tile:#5AA9DC;}
/* the Ordered/Random pill that used to sit in the banner's corner is gone (Aug 2026) — it lives in the
   banner's own long-press sheet now (openReviewMenu), beside the deck rows' options. */
.review-group.has-active > #b-review{border-bottom-left-radius:0; border-bottom-right-radius:0;}
.active-decks{display:flex; flex-direction:column;}
/* NEVER NAME A CLASS `ad-…` (Aug 2026, on a bug report — and this whole block was renamed `ad-` → `dk-`
   because of it). The parts of a review row were named for the ACTIVE DECK they belong to, and `.ad-body`
   and `.ad-title` are also real advertisement class names, so EasyList and its relatives carry generic
   cosmetic filters for them: on a reader with an ad blocker the deck's NAME and the bar beside it were
   `display:none`, leaving a row of bare numbers with the chevron slid left against them. An ad blocker
   injects those rules as an ORIGIN-LEVEL user stylesheet, so no specificity, no `!important` and no
   inline style can outrank them — renaming is the only fix. It failed in exactly the way that never gets
   reported as a bug in the site: the markup was perfect, the Collections page (which uses no `ad-` names)
   was untouched, nothing threw, and no test could see it, since Playwright runs no extensions. The blocker
   took `.ad-body` and `.ad-title` and left `.ad-counts`, `.ad-grip` and `.ad-chev` alone — being in the
   lists is a matter of which names real ad markup happens to use, so the whole prefix goes rather than the
   two that were caught. `ads-`, `advert…`, `sponsor…`, `promo…` and `banner-ad` are the same trap.
   `.claude/test-layout.js` fails the build on any class name starting `ad-`/`ads-`. */
/* ONE horizontal line (Aug 2026, on request): piles · title · progress · trash, all centred on the same
   baseline row. It was a two-line row — the title on top and the bar under it, indented past the piles —
   which left a band of empty card either side of a short deck name and made the bar read as belonging to
   nothing above it. Everything now sits on one level, and the row's height halves with it. */
.active-deck{
  position:relative; overflow:hidden;
  display:flex; align-items:center; gap:11px;
  /* the row wears its COLLECTION's identity hue (--coll-bg, set per row by PAGES.home from COLL_THEME —
     the same colour the Library banner uses), falling back to the review's bronze for a community deck or
     the Card-of-the-day list, which belong to no collection.
     THE MIX IS THE COLLECTION BANNER'S OWN (Aug 2026, on request): the same 105deg wash at the same 30%
     (22% at night), fading into the card at the same 64%, so a deck's row on the home page states its
     collection's colour exactly as strongly as that collection's banner does one page over. It was 14%
     and 9% — the right HUE at half the strength, which read as a different, washed-out colour rather
     than as the same one. Keep these four figures in step with `body[data-theme="folio"]
     .collection-deco` and its `.night` pair; the two rules exist to say the same thing. */
  --dk-accent:var(--coll-bg, var(--tile, #9A6634));
  background:linear-gradient(105deg, color-mix(in srgb, var(--dk-accent) 30%, transparent), transparent 64%) var(--card);
  border:1px solid var(--rule); border-top:0; border-left:4px solid var(--dk-accent);
  padding:10px 13px; animation:pageIn .3s var(--ease) both;
}
/* like the banner's own wash, weaker on dark themes — the hue sits on a dark card and reads twice as
   strongly there, which is why the banner's own night pair drops from 30% to 22% as well */
body.night .active-deck{background:linear-gradient(105deg, color-mix(in srgb, var(--dk-accent) 22%, transparent), transparent 64%) var(--card);}
/* ---------- A DECK FINISHED FOR THE DAY (Sep 2026, on request) ----------
   "When an active deck has been completed for the day … it should turn green and have a checkmark in
   the right of the deck background, in the same way as a completed minigame, and gold if the review
   cards were completed perfectly." So this is `.game-tile.done` / `.game-tile.won` transposed onto a
   row, and everything that made that small makes this small too — read that block before changing it.

   `--dk-accent` IS WHY THIS IS SMALL, and it is `--gt-accent`'s lesson word for word. The row's hue
   arrives INLINE as `--coll-bg` (PAGES.home writes it per row from COLL_THEME), and an inline
   declaration beats any selector without `!important` — so `.dk-done` cannot simply re-set `--coll-bg`.
   It sets a second property that the wash and the left bar read instead, and which merely DEFAULTS to
   the collection's hue. Two lines for the two states, and nothing derived from it had to be found.

   THE BAR IS DELIBERATELY NOT TOUCHED, and that is the one thing to keep straight here: `.prog-done`
   already turns a row's BAR and its TITLE gold, and it means something else entirely — every card in
   this deck has been studied at some point, which is a fact about the deck's whole life. This is a fact
   about TODAY. They are kept on different parts of the row (the wash, the left bar and the tick against
   the progress bar and the name) precisely so a reader can hold both at once: a deck can be finished
   today without being finished, and finished without being finished today.

   THE TICK IS A MASK, NOT A GLYPH — the same 24-box polyline and the same technique as the tile's, so it
   takes the accent colour with everything else. It sits at the RIGHT, as asked, at z-index 0 with the
   row's own contents lifted over it: a watermark the row is read across, which is what lets it be this
   large on a line only 41 to 47px tall. The lift is written out per child rather than as `> *` because
   `.dk-grip` is `position:absolute` in the row's left padding and giving it `position:relative` would
   drop it back into the flex line.

   THE SUBJECT ICON TAKES THE ROW'S GREEN OR GOLD TOO (Sep 2026, on request, reversing the rule that
   stood here: that the mark names WHICH collection and so should keep its hue). A finished row whose
   icon stays in the collection colour read as half finished, so `.dk-done .coll-ic.dk-ic` reads
   `--dk-accent`. An uploaded PNG icon keeps its own colours — an `<img>` has no `currentColor`.

   THE CORNER MARK IS CLIPPED, NEVER `display:none`, for the tile's reason: `.gt-check` / `.gt-seal` is
   the only thing on the row that STATES the day in words (`role="img"` + aria-label), so it stays in the
   accessibility tree at 1px. A row that has gone green and says nothing about it to a screen reader is
   a row that has told half its readers nothing. */
/* THE ANCESTOR IS SPECIFICITY, NOT DECORATION, and it is `.prog-done`'s own lesson: a group header and a
   language header declare a `--dk-accent` of their own further down this file — their fallback hue is not
   a deck row's — at the same two classes as these, so on source order alone the group's declaration wins
   and a finished COLLECTION goes on painting itself in its collection's colour with a green tick over it.
   Three selectors beats two wherever the rules happen to sit. `!important` is not used. */
.active-decks .active-deck.dk-done{ --dk-accent:var(--good, #4F9D67); }
.active-decks .active-deck.dk-won{ --dk-accent:var(--dk-gold); }
/* the tile's own gold, which is scoped to `.game-tile` — the same pair rather than a second gold, since
   the two marks can be on screen together and a banner's seal sits between them */
.active-deck{ --dk-gold:#B8892A; }
body.night .active-deck{ --dk-gold:#D8B355; }
/* the icon joins the row's state; three classes plus the ancestor outrank the day, night and `body.hc`
   rules on `.coll-ic.dk-ic` up by the row's own block */
.active-decks .active-deck.dk-done .coll-ic.dk-ic{ color:var(--dk-accent); }
.active-deck.dk-done::after{
  content:""; position:absolute; z-index:0; pointer-events:none;
  /* Clear of the chevron's column (26px) and the row's own right padding, so the two marks do not sit on
     top of each other. THE BOX IS TALLER THAN THE ROW ON PURPOSE and the figure is arithmetic rather than
     taste: the polyline runs y 6→17 of a 24 viewBox, so its ink is the middle 46% of a square box — at
     152% of the 41px group header that is a 29px tick on a 41px line, and what overflows into
     `overflow:hidden` is empty space above and below it. Raising it much past this starts cutting the
     top-right arm off on the SHORTEST row kind, which is that header — 7px padding against a deck
     row's 10px. */
  right:46px; top:50%; transform:translateY(-50%); height:152%; aspect-ratio:1;
  background:var(--dk-accent); opacity:.18;
  -webkit-mask:var(--dk-tick) no-repeat center/contain;
  mask:var(--dk-tick) no-repeat center/contain;
}
.active-deck.dk-won::after{ opacity:.30; }
.active-deck{ --dk-tick:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='3.2' stroke-linecap='round' stroke-linejoin='round'><polyline points='20 6 9 17 4 12'/></svg>"); }
/* the row is read OVER the watermark — every child that can reach the right-hand end of the line, and
   NOT `> *`, which would re-position the absolutely-placed grip */
.active-deck.dk-done .dk-counts,
.active-deck.dk-done .dk-chev,
.active-deck.dk-done .dk-chev-gap,
.active-deck.dk-done .coll-ic,
.active-deck.dk-done .dk-dl{ position:relative; z-index:2; }
/* …and `.dk-body` IS RAISED WITHOUT BEING POSITIONED, which is the whole of this rule (Sep 2026, on a bug
   report: a deck finished for the day had its progress bar jump from the row's bottom edge to a line under
   the title). `.dk-prog .track` is `position:absolute; bottom:0` and takes its containing block from the
   nearest POSITIONED ancestor, which is `.active-deck` — the row, whose `overflow:hidden` clips the track
   to the last row's rounded corners. It lives inside `.dk-body`, so the moment that gets `position:relative`
   the bar re-anchors to the body's own bottom and the row's underline is gone.
   `z-index` alone is enough here because `.dk-body` is a FLEX ITEM of `.active-deck`: flexbox applies
   z-index to items as though they were positioned, so this still lifts the whole body over the `::after`
   watermark and still carries its descendants with it, while leaving the row as the track's anchor. */
.active-deck.dk-done .dk-body{ z-index:2; }
.active-deck .gt-check, .active-deck .gt-seal{
  width:1px; height:1px; padding:0; border:0; overflow:hidden;
  clip-path:inset(50%); background:none; box-shadow:none; animation:none; filter:none; flex:none;
}
/* THE SUBDECK FOLD (Aug 2026, on request) — see adSyncFold in app.js.
   `.dk-shut` is a CLASS rather than the `hidden` attribute on purpose: `.active-deck` carries an author
   `display:flex`, which beats the UA `[hidden]` rule outright, so the attribute would have needed an
   override of its own to mean anything (the same trap `.ces-imgpanel[hidden]` and `.gs-results[hidden]`
   already carry). Two classes against one, so this wins on specificity wherever it sits. */
.active-deck.dk-shut{display:none;}
/* The rounded bottom corner belongs to the last VISIBLE row. `:last-child` cannot see a folded row, so with
   the list shut by default it would round a row nobody can see and square off the bottom of the card; both
   are kept, the class for the live page and the structural rule as the answer if adSyncFold never runs. */
.active-deck:last-child, .active-deck.dk-last{border-bottom-left-radius:14px; border-bottom-right-radius:14px;}
/* the deck's own share of the day's three piles, at the far left of its row — the same numbers the banner
   above carries, without the labels it has just spelled out. Tabular figures so the three columns line up
   from row to row rather than jittering with the digit widths. */
.active-deck .dk-counts{
  flex:none; display:flex; align-items:baseline; gap:8px; min-width:46px;
  font-family:var(--mono); font-size:calc(13px * var(--fs)); font-weight:600; font-variant-numeric:tabular-nums;
}
.active-deck .dkc-new{color:var(--indigo-bright);}
.active-deck .dkc-learn{color:var(--zh);}
.active-deck .dkc-rev{color:var(--good, #4F9D67);}
/* a pile at zero is GREY (Aug 2026, on request): the colour says "there is work of this kind here", so it
   has nothing to say on a 0. Written after the three coloured rules — equal specificity, source order. */
.active-deck .dkc.dkc-zero{color:var(--ink-faint); font-weight:500;}
.active-deck .dk-body{flex:1; min-width:0; display:flex; align-items:center; justify-content:space-between; gap:11px;}
/* the deck's name is the one part of the row that can be shortened, so it is the only thing that gives way
   when the row runs short — everything else is a number or an icon and has no shorter form */
.active-deck .dk-line{flex:0 1 auto; min-width:0; display:flex; align-items:baseline; gap:10px;}
.active-deck .dk-title{font-family:var(--display); font-weight:600; font-size:calc(16px * var(--fs)); color:var(--ink); letter-spacing:-.01em; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.active-deck .dk-count{font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-faint); white-space:nowrap; flex:none;}
/* The fold's own control, at the right end of the row — where the Library puts the chevron on these very
   same collections, so the reader meets one disclosure gesture on the site and not two. It shares `.chev`
   for the icon, the 180° open state and the focus ring, and overrides the size and chrome: a 40px bordered
   disc is the Library banner's control and would be taller than this row, which is one tight line. Quiet by
   default — no border, no fill — because it must not compete with the three piles and the deck's own name;
   it takes the indigo on hover like every other control here. These rules MUST stay below `.chev`'s own
   block: `.active-deck .dk-chev:hover` and `.chev:hover` are both two classes, so source order is all that
   decides which of them paints. */
.active-deck .dk-chev{width:26px; height:26px; border:0; background:transparent; color:var(--ink-faint);}
.active-deck .dk-chev svg{width:15px; height:15px; stroke-width:2.4;}
.active-deck .dk-chev:hover{background:var(--indigo-wash); color:var(--indigo); border-color:transparent;}
/* A row with nothing to fold reserves the same width, so the progress bars down the list all stop at one
   place instead of the right edge going ragged wherever a leaf deck sits. */
.active-deck .dk-chev-gap{flex:none; width:26px;}
/* THE DRAG HANDLE (Aug 2026, on request — see setupDeckDrag). It sits ABSOLUTELY in the row's own left
   padding rather than taking a column in the flex line, and the row's inline `padding-left` starts at 22px
   instead of 16 to make room for it: at 390px the row already carries three piles, a name, a bar and a
   chevron, and the deck's NAME is the only part of it with a shorter form — so a handle in the line would
   have been paid for out of the one thing the reader is reading. `inset-inline-start` rather than `left`,
   so Arabic finds it on the side Arabic reads from.
   It stays in ONE GUTTER at the left edge rather than following its row's indent: which handle belongs to
   which row is settled by the horizontal band it sits in, so staircasing them buys nothing and would push
   a depth-3 handle into the counts. The indent is the title's job and it still carries the hierarchy.
   Quiet by default and full strength on hover or focus: it is on every row of the list, and six handles
   competing with six deck names is a list about its own furniture. `touch-action:none` because a finger
   dragging it must not be handed to the page's scroller before pointermove ever fires. */
.active-deck .dk-grip{
  position:absolute; inset-inline-start:0; top:0; bottom:0; width:20px;
  display:grid; place-items:center; padding:0; border:0; background:transparent;
  color:var(--ink-faint); opacity:.5; cursor:grab; touch-action:none;
  transition:opacity .15s var(--ease), color .15s var(--ease);
}
.active-deck .dk-grip svg{width:11px; height:11px;}
.active-deck .dk-grip:hover{opacity:1; color:var(--indigo);}
.active-deck .dk-grip:focus-visible{opacity:1; color:var(--indigo); outline:2px solid var(--indigo-bright); outline-offset:-2px; border-radius:4px;}
.active-deck .dk-grip:active{cursor:grabbing;}
/* The row being carried, and the block of subdecks travelling with it. It is lifted rather than hidden —
   the list underneath opens a gap for it as the others FLIP out of the way, so what the reader is holding
   has to read as being above the page. `.dk-settling` is the same row on release, easing the last few
   pixels into its slot instead of snapping to it (the transform is what JS animates, so only the shadow
   and the lift are transitioned here). */
.active-deck.dk-dragging{z-index:3; box-shadow:0 10px 24px -8px color-mix(in srgb, var(--ink) 40%, transparent); cursor:grabbing;}
.active-deck.dk-settling{transition:transform .2s var(--ease), box-shadow .2s var(--ease);}
/* While a drag is live the rows stop being hit-tested, so none of them lights up as the carried row passes
   over it. `pointer-events:none` rather than an override of the hover wash: the wash is a different mix on
   dark themes, so restating it here would be two figures to keep in step with the resting rule for no gain
   — and an element that is not hit-tested cannot match `:hover` at all. The gesture is unaffected, the
   pointer having been captured by the LIST. */
.active-decks.dk-reordering .active-deck{pointer-events:none;}
.active-decks.dk-reordering{cursor:grabbing; user-select:none; -webkit-user-select:none;}
/* How far through the deck the reader is — the bar took the place of the blue dot that used to sit at the
   left of the row. IT IS THE ROW'S OWN BOTTOM EDGE AT EVERY WIDTH (Aug 2026, on request), where it used to
   be an underline on a phone and an inline track stretched between the name and the figure above 640px.
   Two rules were answering one question. The phone's answer is the better one and the reason it was chosen
   there holds everywhere: an underline runs the whole width of the row and costs the line no width at all,
   where the inline track's length is paid for out of the deck's NAME — the one part of the row with no
   shorter form. It also means a deck reads the same on a laptop and on a phone, which a bar that changes
   both its shape and its place between the two does not.
   The row is `position:relative; overflow:hidden`, so the last row's rounded corners clip the track's ends;
   the figure beside the name stays in the line, that being the part a reader reads rather than glances at. */
/* THE TWO PROGRESS BLUES, shared with the study card's three-day dots (`.crit-pip`) so a card's dots and
   its deck's bar say "seen" and "learned" in the same two colours (Sep 2026, on request). */
body{--prog-seen:var(--indigo); --prog-seen:color-mix(in srgb, var(--indigo) 72%, #fff); --prog-learned:#0A1F9E;}
body.night{--prog-seen:var(--indigo); --prog-seen:color-mix(in srgb, var(--indigo) 85%, #fff); --prog-learned:#2E4BFF;}
.active-deck .dk-prog{flex:none; gap:0; min-width:0;}
.active-deck .dk-prog .track{
  position:absolute; left:0; right:0; bottom:0; flex:none;
  height:3px; min-width:0; border:0; border-radius:0; background:color-mix(in srgb, var(--ink) 7%, transparent);
}
.active-deck .dk-prog .fill{border-radius:0; background:var(--prog-seen);}
/* the LEARNED half, laid over the studied half from the same left edge (see adProg): a deep saturated blue
   over the theme's indigo, so the lighter blue that shows past its end is the cards met but not yet learned.
   Both halves are blue (Sep 2026, on request — the studied half was the undiscovered-term teal); the learned
   one is solved to stand clearly apart from the indigo in both light and dark */
.active-deck .dk-prog .fill-l{position:absolute; left:0; top:0; bottom:0; width:0; background:var(--prog-learned); transition:width .9s var(--ease);}
.active-deck .dk-prog.prog-done .fill-l{background:linear-gradient(90deg, #C39A2E, #E6C765);}
body.night .active-deck .dk-prog.prog-done .fill-l{background:linear-gradient(90deg, #D4AA3C, #F2DC96);}
.active-deck .dk-prog .count{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.02em; color:var(--ink-faint); white-space:nowrap; flex:none;}
/* ---------- A GROUP HEADER (Aug 2026, on request) ----------
   "A group should appear as a thinner banner": shorter than a deck row, in a DEEPER wash of its own colour,
   with the name set smaller and letterspaced so it reads as a heading over the run beneath rather than as
   one more deck. Everything under it takes the same `--coll-bg`, inherited down the container chain by the
   review list's build — only the header is darkened, which is what the request asks for and what keeps the
   decks inside legible.
   An added COLLECTION is drawn this way too: it holds no cards itself, only the decks inside it do. */
.active-deck.deck-group, .active-deck.dk-langhead{
  /* its own fallback hue, which is not the deck row's — set on `--dk-accent` rather than written into the
     gradient, so `.dk-done` can still re-point it (see the dk-done block up by `.active-deck`) */
  --dk-accent:var(--coll-bg, var(--tile, #5AA9DC));
  padding-top:7px; padding-bottom:7px;
  background:linear-gradient(105deg, color-mix(in srgb, var(--dk-accent) 38%, transparent), color-mix(in srgb, var(--dk-accent) 12%, transparent) 78%) var(--card);
}
/* …and its own night pair, for the reason `.active-deck.context` needs one: `body.night .active-deck`
   (0,2,1) outranks this (0,2,0) whatever the source order, so without it a group header on every dark
   theme silently lost the darkening that makes it a header at all. */
body.night .active-deck.deck-group, body.night .active-deck.dk-langhead{
  background:linear-gradient(105deg, color-mix(in srgb, var(--dk-accent) 28%, transparent), color-mix(in srgb, var(--dk-accent) 9%, transparent) 78%) var(--card);
}
.active-deck.deck-group[data-review]:hover{background:linear-gradient(105deg, color-mix(in srgb, var(--dk-accent) 48%, transparent), color-mix(in srgb, var(--dk-accent) 16%, transparent) 78%) var(--card);}
/* …and a LANGUAGE header hovers the same way, its press opening its options rather than a session (Aug
   2026). It must NOT be folded into `.active-deck[data-review]:hover` further down: that one deepens the
   DECK's resting wash, which is weaker than a header's, so hovering a header would LIGHTEN it. The
   attribute is carried only to make this four selectors deep — `body.night .active-deck.dk-langhead` is
   (0,3,1) and would otherwise outrank a plain `.active-deck.dk-langhead:hover` on every dark theme, which
   is the trap the night pair above this already records. */
.active-deck.dk-langhead[data-langhead]:hover{background:linear-gradient(105deg, color-mix(in srgb, var(--dk-accent) 48%, transparent), color-mix(in srgb, var(--dk-accent) 16%, transparent) 78%) var(--card);}
/* THE HEADER IS SET IN THE ROWS' OWN FACE (Aug 2026, on request). It used to take `--display` two sizes
   down, bolder, letterspaced and in capitals — which read as a different KIND of thing from the decks under
   it rather than as the heading of them — so the rule that did all that is gone and `.active-deck .dk-title`
   answers for the group header too. What still marks it as a header is the wash above and the indent below.
   `.dg-count` — the small mono "N cards" it carried where a deck row carries its bar — went at the same
   time: the header now carries the bar, like everything else on the list. */
/* THE DROP TARGET while a row is being carried over the middle of another (see dropTargetAt). It has to
   read as "inside this" rather than "next to this", which is what the inset ring says and a line between
   two rows would not. */
.active-deck.dk-into{
  box-shadow:inset 0 0 0 2px var(--indigo-bright);
  background:linear-gradient(105deg, color-mix(in srgb, var(--indigo) 22%, transparent), transparent 70%) var(--card);
}
/* The bin at the right of every row is GONE (Aug 2026, on request) — Remove now lives in the row's
   options sheet, held down. A permanent column for one command was expensive on a 390px row that also
   carries three counts, a name, a figure and a bar, and the three commands beside it had nowhere to go.
   Holding a row must not select its text on the way to the sheet, and on iOS must not raise the platform's
   own callout. */
.active-deck[data-review], .active-deck[data-langhead], #b-review{-webkit-user-select:none; user-select:none; -webkit-touch-callout:none;}
/* hierarchy: ancestor context rows + indentation */
.active-deck{transition:padding-left .2s var(--ease), background .16s var(--ease);}
/* a deck added to the review is clickable → reviews just that deck. A language HEADER is pressable too,
   though what it opens is its options rather than a session — it has no cards of its own to deal. */
.active-deck[data-review], .active-deck[data-langhead]{cursor:pointer;}
/* hover deepens the same wash rather than introducing a colour of its own — it must stay ABOVE the resting
   mix in both layers, or hovering a row would lighten it */
/* the HOVER wash reads `--dk-accent` too, or a row that has gone green turns back to its collection's
   hue under the pointer — which is the whole reason that property exists (see the dk-done block) */
.active-deck[data-review]:hover{background:linear-gradient(105deg, color-mix(in srgb, var(--dk-accent) 38%, transparent), transparent 64%) color-mix(in srgb, var(--dk-accent) 10%, var(--card));}
.active-deck[data-review]:focus-visible, .active-deck[data-langhead]:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}
/* An ancestor SIGNPOST row (not itself added) carries the same collection wash at the same strength — what
   marks it as context is the paper under it and the quieter title, not a different colour, since it names
   the very collection its children are washed in. */
.active-deck.context{padding-top:7px; padding-bottom:7px; background:linear-gradient(105deg, color-mix(in srgb, var(--dk-accent) 30%, transparent), transparent 64%) var(--paper-2);}
/* …and its own night pair. Without it `body.night .active-deck` (0,2,1) outranks `.active-deck.context`
   (0,2,0) whatever the source order, so on every dark theme a context row silently lost both its weaker
   wash and the --paper-2 under it and rendered identically to an added deck's row. */
body.night .active-deck.context{background:linear-gradient(105deg, color-mix(in srgb, var(--dk-accent) 22%, transparent), transparent 64%) var(--paper-2);}
.active-deck.context .dk-title{font-size:calc(14px * var(--fs)); color:var(--ink-faint); font-weight:600;}
/* the ancestor rows' hollow marker went with the active rows' blue dot: it existed to line the two up, and
   left on its own it would have pushed every parent title 21px right of the deck sitting under it. The
   indent (data-depth padding) carries the hierarchy on its own. */
.active-deck.context .prog .track{background:var(--paper);}
.active-deck.context .prog .fill{opacity:.65;}
/* The earned fill used to run down the added decks as well, so the whole group turned bronze (or gold) as
   one tile. It went with the banner's fill in Aug 2026 — the ribbon on the banner says the day is done, and
   the rows underneath stay legible in their own collection hues. `rv-done` / `rv-won` are still set on
   .review-group; nothing styles them now, and they are left as the hook a future treatment would use. */

/* ============================================================
   WHITEBOARD — draw on the card
   ============================================================ */
.draw-canvas{position:absolute; top:0; left:0; z-index:40; pointer-events:none; touch-action:none;}
/* The BOOK's ink layer: the size of the SCREEN, pinned to it, with the strokes re-drawn at the current
   scroll offset instead of the canvas being as tall as the chapter. See wbResize's `fixed` branch —
   a book of the Republic is ~41,500px tall, which as a backing store is 200 MB. */
.draw-canvas.wb-fixed{position:fixed;}
.draw-canvas.on{pointer-events:auto; cursor:crosshair;}
/* ---- a stylus takes the pen, and fingers go back to scrolling (Aug 2026, on request) ----
   There is deliberately NO `touch-action` override here, and that is the fix rather than an omission. This
   rule used to say `pan-y pinch-zoom` so the browser would scroll under a finger — but `touch-action` is
   CSS and CSS cannot tell a pen from a finger, so the same permission handed the browser the STYLUS's drag
   as well: a stroke drew for the length of the pan slop, the scroller claimed the gesture, `pointercancel`
   killed the stroke and the page moved under it. The canvas therefore keeps `touch-action:none` in every
   state, and the finger's scroll is done by hand in setupWhiteboard — which is also where the decision
   belongs, being the one place that can make it per gesture, by the tool that started it.
   The `.wb-pen-only` class is still set on the canvas: it carries no style now, but it is the state written
   where it can be read — in the DOM — and a rule added here later would be the wrong fix twice over.
   Nor is the CURSOR touched: in this mode a mouse still draws (only `pointerType === "touch"` is handed
   back), so the crosshair `.draw-canvas.on` sets is still telling the truth. */
/* The marker is DRAGGABLE anywhere on the screen (see wbMakeDraggable in app.js), which is why the box is
   exactly the toggle button and the panel hangs off it absolutely rather than sharing a flex column. Two
   things follow from that. The element is positioned by right/bottom, so the corner the drag moves is the
   toggle's own corner whether the panel is open or shut — with `left`, opening the panel would shove the
   button sideways. And the panel, being out of flow, only has to be told which way to open: .wb-flip when
   there is no room above, .wb-left when there is none to the left. */
.wb-tools{
  position:fixed; right:18px; bottom:18px; z-index:62; display:none;
  width:46px; height:46px; transition:bottom .34s var(--ease);
}
.wb-tools.show{display:block;}
/* the bottom transition would lag a finger by a third of a second while dragging — and would fight the
   fling frame for frame afterwards, since that one moves the marker itself, 60 times a second.
   `.wb-pressed` is the same rule one moment earlier: it goes on at POINTERDOWN, so a marker still
   travelling towards the grade bar's corner stops dead under the finger instead of sliding on while the
   press is being classified as a tap. See wbMakeDraggable — the drag re-reads the rect when it starts,
   and this is what makes that rect stand still in the meantime. */
.wb-tools.wb-pressed, .wb-tools.wb-dragging, .wb-tools.wb-flinging{transition:none;}
/* …and the slide back to the default corner needs BOTH axes for its own 200ms (see wbGoHome in app.js:
   the base rule above transitions `bottom` alone, which would slide the marker down into place and jump
   it sideways). Two classes, so it outranks the base rule wherever it lands in the file. Keep the
   duration in step with WB_HOME_MS. */
.wb-tools.wb-homing{transition:right .2s var(--ease), bottom .2s var(--ease);}
body.grading .wb-tools{bottom:108px;}
.wb-tools.on-atlas{bottom:25px; right:16px;}   /* far bottom-right, vertically centred inside the timeline bar */
.wb-panel{
  position:absolute; right:0; bottom:calc(100% + 9px);
  display:none; flex-direction:column; gap:9px; padding:11px;
  background:var(--card); border:1px solid var(--rule); border-radius:13px; box-shadow:var(--shadow-lg);
}
.wb-tools.wb-flip .wb-panel{bottom:auto; top:calc(100% + 9px);}
.wb-tools.wb-left .wb-panel{right:auto; left:0;}
/* it arrives rather than appearing (Aug 2026): display can't be transitioned, so the entrance is an
   animation on the frame the panel becomes `flex`. The killswitch at the foot of the file covers it. */
.wb-tools.active .wb-panel{display:flex; animation:wbPanelIn .17s var(--ease) both; transform-origin:bottom right;}
.wb-tools.wb-flip.active .wb-panel{transform-origin:top right;}
.wb-tools.wb-left.active .wb-panel{transform-origin:bottom left;}
@keyframes wbPanelIn{from{opacity:0; transform:translateY(6px) scale(.96);} to{opacity:1; transform:none;}}
.wb-row{display:flex; gap:7px; align-items:center; justify-content:center;}
.wb-color{
  width:23px; height:23px; border-radius:50%; cursor:pointer; padding:0;
  border:2px solid var(--card); box-shadow:0 0 0 1px var(--rule); background:var(--wc);
  transition:transform .12s var(--ease);
}
.wb-color:hover{transform:scale(1.12);}
.wb-color.sel{box-shadow:0 0 0 2px var(--card), 0 0 0 3px var(--ink);}
/* the reader's own colour, remembered between sessions. Tapping it opens the picker row below; the dashed
   ring says it is the swatch that can be changed. */
.wb-custom{appearance:none; position:relative; flex:none; border-style:dashed; border-color:var(--ink-faint);}
.wb-custom.open{border-style:solid; box-shadow:0 0 0 2px var(--card), 0 0 0 3px var(--indigo);}
/* ---- the custom-colour picker ----
   The ordinary shape: a saturation/brightness field over a hue bar, with the hex underneath. It replaced
   <input type="color"> (Aug 2026, on request), whose platform dialog on a phone is a full-screen sheet of
   sliders covering the very card being annotated.
   The field is two gradients over the hue set by --h, in the standard order — white across, black down —
   so the corners are white, the pure hue, black and black. Both fields need touch-action:none or the
   browser claims a finger drag as a page scroll before pointermove ever fires; the knobs are inert so a
   press always lands on the field under them. */
.wb-pick{flex-direction:column; align-items:stretch; gap:8px; padding:2px 1px 0;}
.wb-pick[hidden]{display:none;}   /* the author display:flex on .wb-row would otherwise defeat it (codebase convention) */
/* Shown only once a stylus has been seen on this device — most readers never have one, and a control for
   hardware that isn't there is clutter. Same [hidden] note as .wb-pick above: .wb-row's author
   display:flex beats the UA rule, so it has to be said again here (codebase convention). */
.wb-stylus-row[hidden]{display:none;}
.wb-stylus{flex:1;}
.wb-sv{
  position:relative; height:88px; border-radius:9px; cursor:crosshair; touch-action:none;
  border:1px solid var(--rule);
  background:linear-gradient(to top, #000, rgba(0,0,0,0)), linear-gradient(to right, #fff, hsl(var(--h,0) 100% 50%));
}
.wb-hue{
  position:relative; height:14px; border-radius:999px; cursor:pointer; touch-action:none;
  border:1px solid var(--rule);
  background:linear-gradient(to right,#f00 0%,#ff0 17%,#0f0 33%,#0ff 50%,#00f 67%,#f0f 83%,#f00 100%);
}
.wb-knob{
  position:absolute; top:50%; width:13px; height:13px; margin:-7px 0 0 -7px; border-radius:50%;
  border:2px solid #fff; box-shadow:0 0 0 1px rgba(0,0,0,.45), 0 1px 3px rgba(0,0,0,.4);
  background:var(--wc, transparent); pointer-events:none;
}
.wb-sv:focus-visible, .wb-hue:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.wb-hex{
  text-align:center; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.08em; color:var(--ink-soft);
  user-select:none;
}
.wb-size{
  appearance:none; cursor:pointer; width:30px; height:30px; border-radius:8px; flex:none;
  border:1px solid var(--rule); background:var(--paper-2); color:var(--ink-soft);
  display:inline-flex; align-items:center; justify-content:center; transition:all .15s var(--ease);
}
.wb-size .dot{background:currentColor; border-radius:50%;}
.wb-size:hover{border-color:var(--indigo); color:var(--indigo);}
/* two marks, two facts: `sel` is the width in use (whatever the tool), `on` is the PEN down at it — the
   Draw button is gone, so these buttons are the pen and have to say so. */
.wb-size.sel{border-color:var(--indigo); color:var(--indigo);}
.wb-size.on{background:var(--indigo); border-color:var(--indigo); color:#fff;}
/* Mark shares the sizes' row (it took the place the Draw button left) and keeps its label readable */
.wb-sizes-row .wb-btn{flex:1 1 auto; min-width:0; padding:0 9px;}
.wb-btn{
  appearance:none; cursor:pointer; height:30px; padding:0 11px; border-radius:8px; flex:1;
  border:1px solid var(--rule); background:var(--paper-2); color:var(--ink-soft);
  font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; font-weight:600;
  display:inline-flex; align-items:center; justify-content:center; gap:5px; transition:all .15s var(--ease);
}
.wb-btn svg{width:13px; height:13px;}
.wb-btn:hover{border-color:var(--indigo); color:var(--indigo);}
.wb-btn:disabled{opacity:.4; cursor:default; pointer-events:none;}
.wb-btn.sel{background:var(--ochre); border-color:var(--ochre); color:#fff;}
.wb-btn.wb-hl.sel{background:var(--ochre); border-color:var(--ochre); color:#fff;}
.wb-btn.wb-eraser.sel{background:var(--ink-soft); border-color:var(--ink-soft); color:#fff;}
/* The stylus switch is lit like a selected tool and is NOT one — see refreshModes in app.js: `.sel` in
   this panel means "the tool being drawn with", and this says who may draw rather than what with. */
.wb-btn.wb-stylus.wb-on{background:var(--indigo); border-color:var(--indigo); color:#fff;}
/* touch-action:none — the handle is dragged with a finger, and without it the browser claims the gesture
   as a page scroll before pointermove ever reaches us */
.wb-toggle{
  appearance:none; cursor:pointer; width:46px; height:46px; border-radius:50%;
  border:1px solid var(--rule); background:var(--card); color:var(--ink-soft);
  display:inline-flex; align-items:center; justify-content:center; box-shadow:var(--shadow-md);
  transition:all .18s var(--ease); touch-action:none;
}
.wb-toggle svg{width:19px; height:19px;}
.wb-toggle:hover{transform:translateY(-1px); box-shadow:var(--shadow-lg);}
/* while it is being dragged it is the thing under the finger, not a button being hovered */
.wb-tools.wb-dragging .wb-toggle{cursor:grabbing; transform:none; transition:none; box-shadow:var(--shadow-lg);}
.wb-toggle:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.wb-toggle.on{background:var(--indigo); border-color:var(--indigo); color:#fff;}

/* ============================================================
   ANSWER BOX — background collapse + TTS player
   ============================================================ */
.bg-head{display:inline-flex; align-items:center; gap:8px; margin:0 0 7px; appearance:none; background:none; border:0; padding:0; cursor:pointer; font:inherit; color:inherit; text-align:left;}
.bg-head .label{margin:0; line-height:1; transition:color .18s;}
.bg-head .bg-toggle{align-self:center;}
.bg-head:hover .label, .bg-head:hover .bg-toggle{color:var(--indigo);}
.bg-head:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px; border-radius:5px;}
.bg-toggle{
  border:0; background:none; color:var(--ink-faint);
  padding:0; display:inline-flex; align-items:center; transition:transform .3s var(--ease), color .18s;
}
.bg-toggle svg{width:15px; height:15px;}
.bg-toggle.collapsed{transform:rotate(-90deg);}
.bg-collapse{overflow:hidden; display:grid; grid-template-rows:1fr; transition:grid-template-rows .42s var(--ease);}
.bg-collapse.collapsed{grid-template-rows:0fr;}
/* min-width:0 — see .reveal-inner, or a wide child clips the whole section. `flow-root` is what CONTAINS
   the floated `.card-imgslot`: without it a picture taller than the prose beside it hangs below this box
   and is cut off by the overflow:hidden above, which is also what the collapse animation needs. */
.bg-collapse-inner{min-height:0; min-width:0; display:flow-root;}

/* ============================================================
   SOURCE FOOTNOTES — the apparatus under a card / gloss popup
   ------------------------------------------------------------
   Deliberately quiet: mono, small, faint, and shut by default. A reader who wants to know where a
   claim comes from goes looking for it; one who doesn't should barely register that it is there.
   The in-prose marker is the one loud part, and only on hover/focus.
   ============================================================ */
.src-note{margin:22px 0 0; padding-top:12px; border-top:1px solid var(--rule);}
/* LEARN MORE — the Wikipedia tile at the very foot of a card, below the citations (see learnMoreHTML in
   app.js). The Answer box's own wash, radius and padding, so the card closes the way it opened; the
   logo sits in a disc of the card's own ground so it reads the same on every theme. */
.learn-more{display:flex; align-items:center; justify-content:space-between; gap:16px; margin:22px 0 0; padding:15px 20px;
  background:var(--indigo-wash); border-radius:8px; color:inherit; text-decoration:none;
  transition:transform .22s var(--ease), box-shadow .22s var(--ease);}
.learn-more:hover{transform:translateY(-1px); box-shadow:var(--shadow-md);}
.learn-more:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px;}
.learn-more-text{min-width:0; display:flex; flex-direction:column; gap:3px;}
.learn-more-eyebrow{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.2em; text-transform:uppercase; color:var(--indigo); font-weight:600; opacity:.6;}
.learn-more-title{font-family:var(--display); font-weight:600; font-size:calc(20px * var(--fs)); line-height:1.2; color:var(--ink); overflow-wrap:anywhere;}
.learn-more-sub{font-family:var(--sans); font-size:calc(13px * var(--fs)); line-height:1.4; color:var(--ink-soft);}
.learn-more-logo{flex:none; width:44px; height:44px; border-radius:50%; background:var(--card); border:1px solid var(--rule); padding:3px; box-sizing:border-box; object-fit:contain;}
@media (prefers-reduced-motion: reduce){ .learn-more{transition:none;} .learn-more:hover{transform:none;} }
/* the Atlas panel's own copy sits inside a .cp-sec, which already draws the rule and the space above it */
.cp-srcnote{margin:0; padding-top:0; border-top:0;}
.src-head{
  display:inline-flex; align-items:center; gap:7px; width:auto; appearance:none; background:none;
  border:0; padding:0; cursor:pointer; font:inherit; color:var(--ink-faint); text-align:left;
}
.src-label{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.13em; text-transform:uppercase; transition:color .18s;}
.src-count{
  font-family:var(--mono); font-size:calc(9.5px * var(--fs)); line-height:1; padding:3px 5px; border-radius:4px;
  background:color-mix(in srgb,var(--ink) 8%,transparent); color:var(--ink-faint);   /* theme-aware: a flat rgba() vanishes on the dark themes */
}
.src-toggle{display:inline-flex; align-items:center; color:var(--ink-faint); transition:transform .3s var(--ease), color .18s;}
.src-toggle svg{width:13px; height:13px;}
.src-toggle.collapsed{transform:rotate(-90deg);}
.src-head:hover .src-label, .src-head:hover .src-toggle{color:var(--indigo);}
.src-head:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:3px; border-radius:5px;}
.src-collapse{overflow:hidden; display:grid; grid-template-rows:1fr; transition:grid-template-rows .38s var(--ease);}
.src-collapse.collapsed{grid-template-rows:0fr;}
.src-collapse-inner{min-height:0; min-width:0;}   /* see .reveal-inner */
/* The number is an ELEMENT rather than a ::marker, because the jump runs both ways and a ::marker can
   never be a control (see srcNumHTML): no tabindex, no accessible name, and a click on it lands on the
   <li>. So list-style goes off and the hanging indent is a flex row instead, which also spares every
   variant below its own padding arithmetic. */
.src-list{margin:10px 0 0; padding:0; list-style:none;}
/* `anywhere`, NOT `break-word`: both break a long URL once the box is sized, but only `anywhere` counts the
   break opportunity towards the box's MIN-CONTENT width. Under `break-word` a citation ending in a long DOI
   still reported the whole URL as its minimum, which on a phone widened every ancestor up to .reveal-inner. */
.src-item{
  display:flex; gap:.55em;
  font-family:var(--sans); font-size:calc(12.5px * var(--fs)); line-height:1.5; color:var(--ink-faint);
  margin:0 0 6px; padding:2px 5px; border-radius:4px; overflow-wrap:anywhere;
}
.src-text{min-width:0; flex:1;}   /* or `anywhere` has nothing to shrink and the DOI widens the row again */
/* Resting, it reads exactly as the decimal marker it replaced. Where a marker in the prose points at it,
   wireFootnotes adds .src-back and it takes the vermilion of the markers themselves — the same number in
   the same colour at both ends of the jump, which is what says the two are one thing. */
.src-n{
  flex:none; min-width:1.4em; text-align:right; font-family:var(--mono);
  font-size:calc(11px * var(--fs)); color:var(--ink-faint); user-select:none;
}
.src-n.src-back{
  color:var(--zh); cursor:pointer; font-weight:600; border-radius:3px;
  transition:background .15s var(--ease), color .15s var(--ease);
}
.src-n.src-back:hover{background:color-mix(in srgb,var(--zh) 18%,transparent); color:var(--ink);}
.src-n.src-back:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:1px;}
.src-item a{color:var(--indigo-bright); text-decoration:none; border-bottom:1px solid rgba(78,76,176,.32);}
/* a marker click lands here — the flash says WHICH entry, since the list is opening at the same moment */
.src-flash{animation:srcFlash 1.5s var(--ease) 1;}
/* …and the same flash the other way round, on the marker a note's number sends the reader back to. The
   marker is two characters of superscript, so it is given a little room to be seen in — without the
   padding the highlight is narrower than the digit it is meant to point out. */
sup.fn.src-flash{padding:0 3px;}
@keyframes srcFlash{
  0%{background:color-mix(in srgb,var(--ochre) 26%,transparent); color:var(--ink);}
  70%{background:color-mix(in srgb,var(--ochre) 18%,transparent); color:var(--ink);}
  100%{background:transparent;}
}
/* the access chip at the end of a citation. Green for a source that opens, amber for one that may not —
   a paywall is a fact about the link, not a mistake, so amber and never red. Both colours are theme
   tokens (--good, --ochre), so the chip follows every theme and both modes without its own overrides;
   --ochre is the same warm accent the in-prose markers use, which reads as one apparatus. */
.src-access{
  display:inline-block; font-family:var(--mono); font-size:calc(9px * var(--fs)); font-weight:600;
  letter-spacing:.07em; text-transform:uppercase; line-height:1.6; white-space:nowrap;
  padding:0 6px; margin-left:5px; border-radius:999px; border:1px solid transparent;
  vertical-align:1px;
}
.src-access-open{
  color:var(--good);
  background:color-mix(in srgb, var(--good) 11%, transparent);
  border-color:color-mix(in srgb, var(--good) 30%, transparent);
}
.src-access-pay{
  color:var(--ochre);
  background:color-mix(in srgb, var(--ochre) 13%, transparent);
  border-color:color-mix(in srgb, var(--ochre) 34%, transparent);
}
/* The language chip (Sep 2026). It takes --geo rather than a colour of its own: green already means
   "free to read" and amber "paywalled" on the very same line, so a third chip in either would be read as
   a third verdict about access. --geo is one of the quiet tokens `body.hc` already re-tones, so the
   high-contrast mode covers this with no rule of its own. */
.src-lang{
  color:var(--geo);
  background:color-mix(in srgb, var(--geo) 12%, transparent);
  border-color:color-mix(in srgb, var(--geo) 32%, transparent);
}
.src-compact .src-access, .cp-src .src-access{font-size:calc(8.5px * var(--fs)); padding:0 5px;}
/* the in-prose marker: a real superscript, clickable, in the SAME VERMILION as the answer term
   (.answer .val). Deliberately not the ochre the blanks use: an unopened glossary link is gold
   (.ttip[data-new]), and a marker in that same gold read as one more undiscovered term. */
sup.fn{
  font-family:var(--mono); font-size:.62em; line-height:0; vertical-align:super; font-weight:600;
  color:var(--zh); cursor:pointer; padding:0 1px; margin-left:1px; border-radius:3px;
  transition:background .15s var(--ease), color .15s var(--ease);
}
/* the author writes the marker EMPTY and wireFootnotes fills in the digit. If that pass never ran, show
   the number the author pointed at rather than leaving an invisible gap mid-sentence — once the pass has
   run the marker is no longer :empty, so the two can never both print. */
sup.fn:empty::before{content:attr(data-fn);}
sup.fn:hover{background:color-mix(in srgb,var(--zh) 18%,transparent); color:var(--ink);}
sup.fn:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:1px;}
/* A BOOK'S MARKERS TAKE THE CARD'S VERMILION, like every other citation on the site (Aug 2026, on
   request — they wore a teal of their own, --bknote, for a fortnight). The argument for separating them
   was that a card's marker points at a work Folio is citing while a book's points at the translator's
   own note, which is a different kind of thing; the argument against, and the one that won, is that a
   reader meets both and one apparatus is easier to learn than two. So there is no .bk-page override here
   at all — the reader inherits sup.fn and .src-n.src-back unchanged, and the teal moved to the mark that
   had been borrowing --ochre (see --newterm and .ttip[data-new]). */
/* the gloss-popup variant: narrower column, so everything steps down a size */
.src-compact{margin:14px 0 0; padding-top:9px; clear:both;}
.src-compact .src-label{font-size:calc(9px * var(--fs)); letter-spacing:.1em;}
.src-compact .src-list{margin-top:7px;}
.src-compact .src-item{font-size:calc(11.5px * var(--fs)); line-height:1.45; margin-bottom:5px;}
.src-compact .src-n{font-size:calc(10px * var(--fs));}
/* the Atlas panel owns its own fold (.cp-sec), so only the list itself lands there */
.cp-src .src-list{margin:0;}
.cp-src .src-item{font-size:calc(11.5px * var(--fs)); line-height:1.5; color:var(--ink-faint);}
.cp-sec[hidden]{display:none;}
/* the editors' sources box — a plain textarea, one citation per line */
.af-sources{min-height:64px; font-family:var(--mono); font-size:calc(12px * var(--fs)); line-height:1.55; white-space:pre-wrap; resize:vertical;}

.tr-play{
  flex:none; appearance:none; cursor:pointer; width:24px; height:24px; border-radius:50%;
  border:1px solid color-mix(in srgb,var(--zh) 32%, var(--rule)); background:var(--card); color:var(--zh);
  display:inline-flex; align-items:center; justify-content:center; transition:all .16s var(--ease);
}
.tr-play svg{width:10px; height:10px; margin-left:1px;}
.tr-play:hover{background:var(--zh); color:#fff; border-color:var(--zh);}
.tr-play.playing, .tr-play.tts-playing{background:var(--zh); color:#fff; border-color:var(--zh); animation:none;}
.tr-play:focus-visible{outline:2px solid var(--zh); outline-offset:2px;}

/* ============================================================
   DECKS PAGE — section headers (in progress / not started)
   ============================================================ */
.collection-group + .collection-group{margin-top:32px;}
.group-head{display:flex; align-items:center; gap:14px; margin:0 0 15px; padding:0 2px;}
.group-label{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.2em; text-transform:uppercase; color:var(--ink-soft); font-weight:600; flex:none;}
.group-label::before{content:""; display:inline-block; width:7px; height:7px; border-radius:50%; background:var(--indigo); margin-right:9px; vertical-align:1px;}
.collection-group + .collection-group .group-label::before{background:var(--ochre);}
.group-line{flex:1; height:1px; background:var(--rule);}
.group-count{font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-quiet); flex:none;}

/* The "Planned" section is a <details> disclosure — the collections still being written outnumber the
   finished ones, and listing them flat made the Library read as empty. It was labelled "Coming soon"
   until Aug 2026 and the CLASS NAMES were deliberately left alone: `soon` is in five test files and in
   `isComingSoon`, and renaming a selector to match a label is how a stylesheet and a suite drift. */
.collection-group-soon{margin-top:32px;}
.collection-group-soon > summary, .collection-group-fold > summary{cursor:pointer; list-style:none; border-radius:8px; transition:opacity .18s var(--ease);}
.collection-group-soon > summary::-webkit-details-marker, .collection-group-fold > summary::-webkit-details-marker{display:none;}
.collection-group-soon > summary:hover, .collection-group-fold > summary:hover{opacity:.72;}
.collection-group-soon > summary:focus-visible, .collection-group-fold > summary:focus-visible{outline:2px solid var(--indigo); outline-offset:4px;}
.group-chev{width:15px; height:15px; flex:none; color:var(--ink-faint); transition:transform .2s var(--ease);}
.collection-group-soon[open] > summary .group-chev, .collection-group-fold[open] > summary .group-chev{transform:rotate(180deg);}
@media (prefers-reduced-motion:reduce){ .group-chev{transition:none;} }
/* The Languages section (Aug 2026, on request — "presented as official curated collections, with the
   same type of banners … as the history collections"). It was seven <details> folds of deck rows and is
   now seven .collection banners, so it INHERITS almost everything: .collection, .collection-row,
   .collection-deco, .coll-ic, .collection-title-row and .deck-prog all style it exactly as they style
   Ancient Greece, and every theme's banner treatment follows the --coll-bg set inline on the row. What
   is left here is only what differs, and each line is a difference rather than a restatement. */
/* The deck rows are the curated .node, whose right-hand cell is sized for a chevron; an Add button is
   wider and must not be squeezed into it. */
.lang-deck .collection-actions{align-items:center; flex:none;}
.lang-add{flex:none; white-space:nowrap;}
/* THE TWO SHELVES ARE ONE ROW NOW, so this sets no box of its own: a language deck and a curated deck
   both carry a title with the count and the size on a line under it, and a row that padded itself
   differently would be the very divergence the request is about. What is left is the alignment, which
   both shelves share for the same reason — the number, the + and the chevron sit against the TITLE
   rather than against the middle of a two-line block. */
.node.lang-deck .node-main{min-width:0;}
/* A DECK'S OWN SUBDECKS (Aug 2026, on request — "I should see the 9 decks inside it, and any subdecks
   if there are, displayed in the same way as History decks and subdecks"). They are the curated .node in
   a .node-children fold, so nothing is needed here but the collection's hue on the left edge. */
.collection .node.lang-sub{border-left-color:var(--coll-bg, var(--indigo));}
.collection .node.branch.lang-sub{border-left-color:var(--ochre);}
/* The deck count beside a language's title is a FACT ABOUT THE COLLECTION rather than a date, so it
   borrows .collection-span's quiet setting and nothing else. */
.lang-coll .collection-title-row .collection-span{font-variant-numeric:tabular-nums;}

/* card count on a live collection — the one number that says "there is something here" */
.collection-count{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.02em; color:var(--ink-faint); white-space:nowrap;}

/* ============================================================
   HOME — daily game grid (6 tiles)
   ============================================================ */
.game-grid{display:grid; grid-template-columns:repeat(3,1fr); gap:14px;}
.game-tile{
  /* the hue every treatment on this tile reads. It DEFAULTS to the game's own inline `--tile` and is
     re-set to green or gold on a played tile — see the completion block above. */
  --gt-accent:var(--tile, var(--indigo));
  position:relative; overflow:hidden; text-align:left; min-height:130px;
  /* two layers: the hue washing in from the left (like the Library banners) OVER the original soft glow in the bottom-right corner */
  background:linear-gradient(105deg, color-mix(in srgb, var(--gt-accent) 20%, transparent), transparent 64%),
             linear-gradient(150deg, var(--card) 56%, color-mix(in srgb, var(--gt-accent) 16%, var(--card)));
  border:1px solid var(--rule); border-left:4px solid var(--gt-accent, var(--rule)); border-radius:14px;
  /* the padding is a pair of properties so a full-bleed child can negate it — the played band below */
  --gt-px:18px; --gt-py:18px; padding:var(--gt-py) var(--gt-px); box-shadow:var(--shadow-md);
  display:flex; flex-direction:column; justify-content:flex-end;
  transition:transform .18s var(--ease), box-shadow .22s var(--ease), border-color .18s;
}
button.game-tile{cursor:pointer;}
button.game-tile:hover{transform:translateY(-3px); box-shadow:var(--shadow-lg); border-color:color-mix(in srgb,var(--gt-accent) 45%, var(--rule)); border-left-color:var(--gt-accent);}
button.game-tile:active{transform:translateY(0) scale(.99);}
.game-tile:focus-visible{outline:2px solid var(--gt-accent, var(--indigo-bright)); outline-offset:3px;}
/* ---------- A TILE TURNS OVER TO ITS RECORD (Aug 2026, on request) ----------
   Held down, the tile shows what this reader has done at the game and what everyone else did at today's.

   IT IS A 2D FLIP, NOT A 3D ONE, and that is forced rather than preferred: the tile carries
   `overflow:hidden` (the glyph bleeds past two of its edges), and an element with a clipped overflow is
   flattened to `transform-style:flat` — so `rotateY` on the tile with `backface-visibility` on its faces
   would simply show both at once. A horizontal squash reads as the same gesture, works inside the clip,
   and needs no extra wrapper to hold a 3D context.
   `.gt-face` wraps what was already there and reproduces the tile's own column, so nothing above it
   moved; it must stay UNPOSITIONED, or the glyph and the played/perfect mark — both absolute against the
   tile — would resolve against it instead.
   Both faces transition, so the global reduced-motion killswitch turns this into an instant swap with no
   rule of its own. The back is `pointer-events:none` until it is up: the tile is one button and the tap
   is handled on the button, but a face that could be the click TARGET while invisible is a hit test
   waiting to go wrong. */
.gt-face{flex:1 1 auto; min-height:0; display:flex; flex-direction:column; justify-content:flex-end;
  transform-origin:left center; transition:transform .26s var(--ease), opacity .16s var(--ease);}
.gt-back{
  position:absolute; inset:0; padding:13px 15px; z-index:3;
  display:flex; flex-direction:column; gap:7px; text-align:left;
  background:linear-gradient(150deg, var(--card) 62%, color-mix(in srgb, var(--tile, var(--indigo)) 14%, var(--card)));
  opacity:0; transform:scaleX(0); transform-origin:right center; pointer-events:none;
  transition:transform .26s var(--ease), opacity .16s var(--ease);
}
.game-tile.gt-flipped .gt-face{opacity:0; transform:scaleX(0);}
.game-tile.gt-flipped .gt-back{opacity:1; transform:scaleX(1);}
/* the glyph is the FRONT's ornament and would otherwise show through the back, which is only 62% card */
.game-tile.gt-flipped .gt-glyph{opacity:0;}
.gtb-head{font-family:var(--display); font-weight:600; font-size:calc(13px * var(--fs)); color:var(--ink); line-height:1.15;}
.gtb-cols{display:grid; grid-template-columns:1fr 1fr; gap:10px; flex:1; min-height:0;}
.gtb-col{display:flex; flex-direction:column; gap:3px; min-width:0;}
.gtb-lbl{font-family:var(--mono); font-size:calc(8.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--tile, var(--indigo));}
.gtb-grid{display:grid; grid-template-columns:1fr auto; gap:1px 6px; align-content:start;}
.gtb-k{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.04em; color:var(--ink-faint); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.gtb-v{font-family:var(--display); font-weight:600; font-size:calc(11.5px * var(--fs)); color:var(--ink); white-space:nowrap;}
.gtb-none{font-family:var(--sans); font-size:calc(10px * var(--fs)); line-height:1.35; color:var(--ink-faint); margin:0;}
.gtb-tap{font-family:var(--mono); font-size:calc(8.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint);}
/* the one-line site figure is the PHONE's form of the column above it — see the note in gameBackHTML.
   Declared here, above the ≤640px block, because a media query adds no specificity: a base rule sitting
   after it would silently win and both forms would be drawn at every width. */
.gtb-brief{display:none; font-family:var(--display); font-weight:600; font-size:calc(9.5px * var(--fs)); color:var(--ink); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.gt-glyph{position:absolute; right:-8px; top:-20px; font-family:var(--han); font-weight:700; font-size:calc(98px * var(--fs)); line-height:1; color:var(--tile, var(--indigo)); opacity:.12; pointer-events:none; user-select:none;}
.gt-glyph-svg{top:-14px; right:-14px;}
.gt-glyph-svg svg{display:block; width:112px; height:112px;}
.gt-body{position:relative;}
.gt-eyebrow{display:flex; align-items:center; gap:6px; font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.13em; text-transform:uppercase; color:var(--ink-faint); font-weight:600; margin-bottom:7px;}
.gt-title{display:block; font-family:var(--display); font-weight:600; font-size:calc(18px * var(--fs)); letter-spacing:-.01em; line-height:1.1; color:var(--ink);}
.gt-sub{display:block; color:var(--ink-soft); font-size:calc(12.5px * var(--fs)); margin-top:4px; line-height:1.35;}
.gt-eyebrow .live-dot{background:var(--tile, var(--zh));}
.game-tile.blank{box-shadow:var(--shadow-sm); opacity:.92;}
.game-tile.blank .gt-title{color:var(--ink-faint);}
.game-tile.blank .gt-glyph{opacity:.16;}
/* ---------- game-tile daily states ----------
   The tile no longer FILLS when played (Aug 2026, on request) — see the .gt-check / .gt-seal block above.
   What is left here is the idle treatment, which is what every tile now wears all day: the hue washing in
   from the left, the theme colour in the left bar, a faint corner icon. Completion is a mark in the
   top-right corner, a gold seal on a perfect score. Daylight needs a far stronger mix than night; night keeps
   the original whisper strength. */
button.game-tile{
  background:linear-gradient(105deg, color-mix(in srgb, var(--gt-accent) 26%, transparent), transparent 64%),
             linear-gradient(150deg, var(--card) 56%, color-mix(in srgb, var(--gt-accent) 16%, var(--card)));
  box-shadow:var(--shadow-sm);
}
body.night button.game-tile,
body.night .game-tile{
  background:linear-gradient(105deg, color-mix(in srgb, var(--gt-accent) 12%, transparent), transparent 64%),
             linear-gradient(150deg, var(--card) 56%, color-mix(in srgb, var(--gt-accent) 16%, var(--card)));
}
button.game-tile .gt-title{color:color-mix(in srgb, var(--gt-accent) 45%, var(--ink));}
button.game-tile .gt-glyph{opacity:.14;}
@keyframes gt-gold-shine{
  0%, 14%   {background-position:110% 0, 0 0;}   /* the shine band waits off the left edge */
  60%, 100% {background-position:-10% 0, 0 0;}   /* sweeps across the gold, then rests until the next glint */
}
/* ---------- read-aloud (TTS) controls ---------- */
.tts-mute{position:absolute; top:12px; right:12px; z-index:3; width:32px; height:32px; border:1px solid var(--rule); border-radius:9px; background:var(--card); color:var(--ink-faint); cursor:pointer; display:inline-flex; align-items:center; justify-content:center; padding:0; transition:color .15s, border-color .15s;}
.tts-mute:hover{color:var(--ink); border-color:var(--ink-faint);}
.tts-mute.muted{color:var(--zh); border-color:color-mix(in srgb, var(--zh) 40%, var(--rule));}
.tts-mute svg{width:17px; height:17px;}
/* tiny play triangle behind the section titles (Question / Answer / Background) + the gloss-popup title */
.tts-play{appearance:none; border:none; background:transparent; padding:2px; margin-left:7px; color:var(--ink-faint); cursor:pointer; display:inline-flex; align-items:center; justify-content:center; vertical-align:-1px; opacity:.8; border-radius:4px;}
.tts-play svg{width:9px; height:9px; fill:currentColor; display:block;}
.tts-play:hover{color:var(--indigo); opacity:1;}
.tts-play.tts-playing{color:var(--indigo); opacity:1; animation:tts-pulse 1.1s ease-in-out infinite;}
@keyframes tts-pulse{50%{opacity:.45;}}
.gloss-bar .tts-play{margin-left:2px; flex:none;}
.gloss-bar .tts-play svg{width:10px; height:10px;}
/* settings: reading-voice pickers */
.ctl-col{display:flex; flex-direction:column; gap:8px; align-items:stretch; min-width:0;}
.voice-row{display:flex; align-items:center; gap:8px;}
.voice-lab{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint); width:52px; flex:none;}
.voice-row .set-sel{flex:1; min-width:0; max-width:280px;}
.voice-row .btn{padding:6px 12px; font-size:calc(12px * var(--fs)); flex:none;}
/* selection context menu (Copy / Read aloud) on the background paragraph */
.ctx-menu{position:fixed; z-index:9500; min-width:136px; background:var(--card); border:1px solid var(--rule); border-radius:10px; box-shadow:var(--shadow-lg); padding:5px; display:flex; flex-direction:column; gap:1px;}
.ctx-menu button{appearance:none; border:none; background:transparent; text-align:left; font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink); padding:7px 11px; border-radius:7px; cursor:pointer;}
.ctx-menu button:hover{background:var(--paper-2);}
/* A row of colours where a command would be (the book's Highlight — see wireBookCtxMenu). Drawn in place
   rather than as a submenu: five swatches take less room than the words naming them, and a menu inside a
   menu is a target inside a target on a phone. The label sits above them in the menu's own type so the
   row reads as one item and not as five unexplained dots. */
.ctx-swatches{padding:5px 11px 7px;}
.ctx-swlabel{display:block; font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink); margin-bottom:6px;}
.ctx-swrow{display:flex; gap:6px;}
.ctx-sw{
  appearance:none; width:22px; height:22px; flex:none; padding:0; border-radius:50%;
  border:1px solid var(--rule); cursor:pointer; transition:transform .12s var(--ease), box-shadow .12s;
}
.ctx-sw:hover{transform:scale(1.14);}
.ctx-sw:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
/* ---------- Mission page (about: spaced repetition, credits, changelog) ---------- */
.mission{display:flex; flex-direction:column; gap:18px; max-width:760px;}
.msn-card{position:relative; background:var(--card); border:1px solid var(--rule); border-radius:14px; padding:26px 30px; box-shadow:var(--shadow-sm);}   /* relative: hosts the intro's absolute read-aloud button */
.msn-card h2{font-family:var(--display); font-weight:600; font-size:calc(21px * var(--fs)); letter-spacing:-.01em; margin:0 0 14px; color:var(--ink);}
.msn-prose p{font-family:var(--serif); font-size:calc(15.5px * var(--fs)); line-height:1.72; color:var(--ink-soft); margin:0 0 13px;}
.msn-prose p:last-child{margin-bottom:0;}
.msn-prose b{color:var(--ink); font-weight:600;}
.credits-list{list-style:none; margin:0; padding:0; display:flex; flex-direction:column; gap:9px;}
.credits-list li{font-family:var(--serif); font-size:calc(14px * var(--fs)); line-height:1.55; color:var(--ink-soft); padding-left:18px; position:relative;}
.credits-list li::before{content:"—"; position:absolute; left:0; color:var(--ink-faint);}
.credits-list a{color:var(--indigo); text-decoration:none; border-bottom:1px solid color-mix(in srgb, var(--indigo) 35%, transparent);}
.credits-list a:hover{border-bottom-color:var(--indigo);}
.cr-lic{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; color:var(--ink-faint); border:1px solid var(--rule); border-radius:5px; padding:1px 6px; white-space:nowrap; vertical-align:1px;}
.cr-note{color:var(--ink-faint); font-style:italic;}
.clog{display:flex; flex-direction:column; gap:8px;}
.clog-head{appearance:none; width:100%; display:flex; align-items:baseline; gap:12px; background:none; border:0; padding:10px 2px; cursor:pointer; font:inherit; color:inherit; text-align:left; border-bottom:1px solid var(--rule);}
.clog-date{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--indigo); font-weight:600; white-space:nowrap;}
.clog-title{flex:1; font-family:var(--display); font-weight:600; font-size:calc(15px * var(--fs)); color:var(--ink);}
.clog-chev{color:var(--ink-faint); align-self:center;}
.clog-chev svg{width:14px; height:14px; display:block; transition:transform .18s var(--ease);}
.clog-day.open .clog-chev svg{transform:rotate(180deg);}
/* ON A PHONE THE DAY'S TITLE GOES UNDER ITS DATE (Sep 2026, on request: "on mobile view in the Changelog,
   put the date updates title on the row below the date rather than in a column right of it"). The head is
   one flex line — date, then title, then the chevron — and on a 390px screen the date chip takes about
   half of it, so a 72-character title (which is the ceiling `check-style.js` rule 5 holds them to) had a
   column of eight or nine characters to wrap in and ran to five or six lines beside a two-line date.
   A grid rather than `flex-wrap`: the chevron has to stay up on the DATE's row, and a wrap would take it
   to the bottom of whichever line the title ended on. */
@media (max-width:640px){
  .clog-head{display:grid; grid-template-columns:1fr auto; grid-template-areas:"date chev" "title title"; align-items:center; gap:5px 12px;}
  .clog-date{grid-area:date; justify-self:start;}
  .clog-title{grid-area:title;}
  .clog-chev{grid-area:chev;}
}
/* …and the changelog's days, the same way */
/* A 0fr grid row only collapses a child whose own MINIMUM height is zero, and an element's padding counts
   towards that minimum whatever `min-height` says about its content box. With the padded <ul> as the grid
   item, a shut day therefore stayed ~16px tall and showed a clipped line of its first entry through it —
   which is what "the collapsible sections don't collapse fully" was (Aug 2026, reported from a phone).
   `.clog-in` is an unpadded wrapper that clips: it is what the row sizes, and it really does go to 0. */
.clog-body{display:grid; grid-template-rows:0fr; overflow:hidden; transition:grid-template-rows .32s var(--ease);}
.clog-day.open .clog-body{grid-template-rows:1fr;}
.clog-body > *{min-height:0; overflow:hidden;}
.clog-body ul{margin:0; padding:10px 2px 6px 20px;}
.clog-body li{font-family:var(--serif); font-size:calc(14px * var(--fs)); line-height:1.6; color:var(--ink-soft); margin-bottom:7px;}
/* about page: per-card accent hue + icon chip, forgetting-curve diagram, how-to steps, FAQ */
.msn-card{border-top:3px solid var(--msn-accent, var(--rule));}
.msn-intro{--msn-accent:var(--indigo);}
.msn-howto{--msn-accent:#4F9D67;}
.msn-faq{--msn-accent:#8257C2;}
.msn-clog{--msn-accent:#C39A2E;}
.msn-credits{--msn-accent:#2BA6A0;}
/* the AI-prompt section (Aug 2026). Its accent is a terracotta: the five already placed run indigo,
   green, purple, gold and teal, so red is the quarter left free — and the chip is decoration, so the
   only thing it has to clear is its neighbours rather than the contrast bar. */
.msn-ai{--msn-accent:#C25A4E;}
.ai-intro{font-family:var(--serif); font-size:calc(15px * var(--fs)); line-height:1.68; color:var(--ink-soft); margin:0 0 6px;}
.ai-steps{margin-bottom:18px;}
.ai-prompts{display:flex; flex-direction:column; gap:14px;}
.ai-prompt{border:1px solid var(--rule); border-radius:12px; overflow:hidden; background:var(--paper-2);}
.ai-ph{display:flex; align-items:flex-start; gap:12px; padding:12px 14px;}
.ai-pt{flex:1; min-width:0; display:flex; flex-direction:column; gap:3px;}
.ai-pt b{font-family:var(--display); font-size:calc(15px * var(--fs)); color:var(--ink);}
.ai-pt span{font-size:calc(12.5px * var(--fs)); line-height:1.5; color:var(--ink-faint);}
.ai-copy{flex:none;}
/* The prompt itself. It is text to be COPIED, so it is set in the mono face and wrapped rather than
   scrolled sideways — a prompt with a horizontal scrollbar is one a reader cannot read before they
   paste it. `pre-wrap` keeps the line breaks the prompt is written with. */
.ai-pre{margin:0; padding:12px 14px 14px; border-top:1px solid var(--rule); background:var(--card);
  font-family:var(--mono); font-size:calc(11.5px * var(--fs)); line-height:1.6; color:var(--ink-soft);
  white-space:pre-wrap; word-break:break-word; max-height:280px; overflow-y:auto;}
.ai-foot{margin:16px 0 0; font-size:calc(12.5px * var(--fs)); line-height:1.6; color:var(--ink-faint);}
/* the Studio's way in — a line above the card list, not one of its commands */
.studio-aihint{padding:0 2px 8px; font-size:calc(12px * var(--fs)); color:var(--ink-faint);}
.sa-link{background:none; border:0; padding:0; font:inherit; color:var(--indigo); text-decoration:underline;
  text-underline-offset:2px; cursor:pointer;}
.sa-link:hover{color:var(--ink);}
.msn-head{display:flex; align-items:center; gap:12px; margin-bottom:14px;}
.msn-head h2{margin:0;}
.msn-chip{flex:none; width:34px; height:34px; border-radius:10px; display:flex; align-items:center; justify-content:center;
  background:color-mix(in srgb, var(--msn-accent, var(--indigo)) 12%, transparent); color:var(--msn-accent, var(--indigo));}
.msn-chip svg{width:18px; height:18px; display:block;}
.msn-curve{margin:22px 0 4px; padding:18px 16px 14px; background:color-mix(in srgb, var(--indigo) 5%, transparent); border-radius:12px;}
.msn-curve svg{display:block; width:100%; height:auto;}
.msn-curve figcaption{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint); text-align:center; margin-top:10px;}
/* editorial drop cap on the intro's first paragraph (suppressed while that paragraph is being edited) */
.msn-intro .msn-prose > p:first-of-type::first-letter{
  font-family:var(--display); font-weight:600; font-size:calc(46px * var(--fs)); line-height:.8; float:left; padding:5px 9px 0 0; color:var(--indigo);}
.msn-intro .msn-prose > p.msn-editing::first-letter{
  font-family:inherit; font-weight:inherit; font-size:inherit; line-height:inherit; float:none; padding:0; color:inherit;}
/* the changelog reads as a timeline: a thin rail with a gold dot per day */
.msn-clog .clog{position:relative; padding-left:22px;}
.msn-clog .clog::before{content:""; position:absolute; left:6px; top:16px; bottom:14px; width:2px; border-radius:2px;
  background:color-mix(in srgb, var(--msn-accent) 24%, transparent);}
.msn-clog .clog-day{position:relative;}
.msn-clog .clog-day::after{content:""; position:absolute; left:-19px; top:17px; width:8px; height:8px; border-radius:50%;
  background:var(--msn-accent); opacity:.45; box-shadow:0 0 0 3px color-mix(in srgb, var(--msn-accent) 14%, transparent); transition:opacity .2s;}
.msn-clog .clog-day.open::after{opacity:1;}
.msn-clog .clog-date{background:color-mix(in srgb, var(--msn-accent) 12%, transparent); border-radius:6px; padding:2px 8px;}
.msn-steps{list-style:none; margin:0 0 18px; padding:0; display:flex; flex-direction:column; gap:15px;}
.msn-steps li{display:flex; gap:14px; align-items:flex-start;}
.ms-body{display:flex; flex-direction:column; gap:2px; min-width:0;}
.ms-body b{font-family:var(--display); font-weight:600; font-size:calc(15.5px * var(--fs)); color:var(--ink);}
.ms-body span{font-family:var(--serif); font-size:calc(14.5px * var(--fs)); line-height:1.6; color:var(--ink-soft);}
.ms-body span b{color:var(--ink); font-weight:600;}
.msn-feats{border-top:1px solid var(--rule); padding-top:15px; display:flex; flex-direction:column; gap:9px;}
.mf-row{font-family:var(--serif); font-size:calc(14.5px * var(--fs)); line-height:1.6; color:var(--ink-soft);}
.mf-row b{color:var(--ink); font-weight:600;}
.faq{display:flex; flex-direction:column; gap:6px;}
.faq-q{appearance:none; width:100%; display:flex; align-items:center; gap:12px; background:none; border:0; border-bottom:1px solid var(--rule); padding:11px 2px; cursor:pointer; font-family:var(--display); font-weight:600; font-size:calc(15px * var(--fs)); color:var(--ink); text-align:left;}
.faq-q .clog-chev{margin-left:auto;}
.faq-item.open .clog-chev svg{transform:rotate(180deg);}
/* The fold OPENS rather than appearing (Aug 2026). Same technique as the sources fold above — a grid row
   eased from 0fr to 1fr — which is the only way to animate "as tall as the content is" without measuring
   it in JS. The padding rides on the inner element, or it would be there while the row is still zero and
   the shut state would carry an 18px gap — which is exactly what it DID until Aug 2026: the padded `<p>`
   was the grid item itself, so the comment was right and the markup was not. `.faq-in` is that inner
   element now; see `.clog-body` above for the whole of it. */
.faq-a{display:grid; grid-template-rows:0fr; overflow:hidden; transition:grid-template-rows .32s var(--ease);}
.faq-item.open .faq-a{grid-template-rows:1fr;}
.faq-a > *{min-height:0; overflow:hidden;}
.faq-a p{font-family:var(--serif); font-size:calc(14.5px * var(--fs)); line-height:1.65; color:var(--ink-soft); margin:0; padding:10px 2px 8px;}
.faq-a b{color:var(--ink); font-weight:600;}
/* admin: the Mission title/paragraphs are click-to-edit */
.msn-editable{cursor:text; border-radius:6px;}
.msn-editable:hover{outline:1px dashed color-mix(in srgb, var(--indigo) 45%, transparent); outline-offset:6px;}
.msn-editing, .msn-editing:hover{outline:1px solid var(--indigo); outline-offset:6px; background:color-mix(in srgb, var(--indigo) 5%, transparent);}
.msn-editing .ttip{all:unset;}

/* ============================================================
   TIMELINE GAME (chronological ordering)
   ============================================================ */
.chrono-shell{max-width:640px; margin:0 auto;}
.chrono-scale{display:flex; justify-content:space-between; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; color:var(--ink-faint); margin:0 2px 9px;}
.chrono-list{display:flex; flex-direction:column; gap:9px;}
.chrono-item{
  display:flex; align-items:center; gap:12px;
  background:var(--card); border:1px solid var(--rule); border-radius:11px;
  padding:13px 14px; box-shadow:var(--shadow-sm); user-select:none;
  transition:border-color .18s, box-shadow .18s, background .18s, transform .08s;
}
/* THE CARRIED ROW (Aug 2026, on request — the drag was rewritten; see setupChronoDrag). It stays in
   the flow and is moved by transform, so `transform` here must be left alone: the JS owns it for the
   length of the drag, and a `scale()` in this rule would be overwritten by the first pointermove. It is
   lifted with shadow and z-index instead, which is what "picked up" actually looks like.
   `transition:none` is the load-bearing line — the row follows the pointer frame by frame, and a
   transition on transform would make it lag behind the finger by its own duration. */
.chrono-item.dragging{
  z-index:5; position:relative; cursor:grabbing;
  box-shadow:var(--shadow-lg), 0 0 0 2px var(--indigo-wash); border-color:var(--indigo);
  transition:none;
}
/* …and let go, it eases into its slot rather than snapping to it */
.chrono-item.settling{z-index:5; position:relative; transition:transform .2s var(--ease), box-shadow .2s, border-color .2s;}
.chrono-item.correct{background:color-mix(in srgb,var(--good) 12%, var(--card)); border-color:color-mix(in srgb,var(--good) 45%, var(--rule));}
.chrono-item.wrong{background:color-mix(in srgb,var(--zh) 8%, var(--card)); border-color:color-mix(in srgb,var(--zh) 32%, var(--rule));}
/* a bigger target than the icon: this is the one part of the row a finger has to find, and 18px of
   svg is under the 44px a touch guideline asks for */
.ci-grip{flex:none; cursor:grab; color:var(--ink-faint); display:inline-flex; align-items:center; justify-content:center; touch-action:none; padding:8px; margin:-8px 0 -8px -6px;}
.ci-grip:active{cursor:grabbing;}
.ci-grip svg{width:18px; height:18px;}
.ci-name{flex:1; min-width:0; font-family:var(--serif); font-size:calc(16px * var(--fs)); color:var(--ink); line-height:1.3;}
/* A LITERARY WORK'S AUTHOR, after the italic title (Sep 2026, on request) — see CHRONO_WORKS in app.js.
   It is quieter than the title and never a second line of its own: the row is a thing to be ordered, and
   the by-line is there to say what kind of thing it is rather than to be read. */
.ci-name .ci-by{color:var(--ink-faint); font-style:normal; font-size:calc(14px * var(--fs)); white-space:nowrap;}
/* THE REVEALED DATE IS TWO LINES SINCE SEP 2026 (on request: each year "should also say what that
   starting date is based on") — the year, and under it the date line's own label for the row it came
   from. Ranged RIGHT, because the row's left is the term and the two numbers should line up down the
   edge whatever length the words either side happen to be; the label is quiet and small, so the year
   stays the thing being read and the basis reads as a caption on it. A card that cannot say honestly
   which row it sorted on emits no caption at all and the cell is one line, exactly as it always was. */
.ci-year{flex:none; display:flex; flex-direction:column; align-items:flex-end; gap:1px; font-family:var(--mono); font-size:calc(13px * var(--fs)); font-weight:600; color:var(--ink-faint); line-height:1.15;}
.ci-year .ciy-b{font-size:calc(9px * var(--fs)); font-weight:500; letter-spacing:.07em; text-transform:uppercase; opacity:.72;}
.chrono-item.correct .ci-year{color:var(--good);}
.chrono-item.wrong .ci-year{color:var(--zh);}
.ci-arrows{flex:none; display:flex; flex-direction:column; gap:3px;}
.ci-up,.ci-dn{appearance:none; cursor:pointer; width:26px; height:20px; border-radius:6px; border:1px solid var(--rule); background:var(--paper-2); color:var(--ink-soft); display:inline-flex; align-items:center; justify-content:center; padding:0; transition:all .14s var(--ease);}
.ci-up svg,.ci-dn svg{width:13px; height:13px;}
.ci-up:hover,.ci-dn:hover{border-color:var(--indigo); color:var(--indigo); background:var(--indigo-wash);}
/* ANSWERED: the rows stop being a puzzle and become the record of one (Aug 2026, with the one-play-a-day
   rule). The controls that reorder them are faded and made inert rather than removed — a row that loses its
   grip and its arrows changes shape and width the moment the answer lands, which reads as the page redrawing
   itself under the reader instead of as the puzzle closing. The JS refuses the gesture as well as the CSS:
   pointer-events alone would still leave the keyboard able to press an arrow. */
.chrono-list.chrono-done .ci-grip,.chrono-list.chrono-done .ci-arrows{opacity:.32; pointer-events:none;}
.chrono-list.chrono-done .ci-grip{cursor:default;}
.chrono-actions{display:flex; gap:12px; margin-top:18px;}
.chrono-result{overflow:hidden; max-height:0; opacity:0; transition:max-height .35s var(--ease), opacity .3s, margin .3s; text-align:center; margin-top:0;}
.chrono-result.show{max-height:220px; opacity:1; margin-top:18px;}
.chrono-result .cr-title{font-family:var(--display); font-weight:600; font-size:calc(20px * var(--fs)); color:var(--ink);}
.chrono-result.win .cr-title{color:var(--good);}
.chrono-result .cr-sub{color:var(--ink-soft); font-size:calc(14px * var(--fs)); margin-top:5px;}

/* ============================================================
   BOTTOM TAB BAR (phones)
   The top bar held nine icon-only controls in a scrolling strip at the top of a 390px screen — the four
   places a reader actually goes, plus theme, edit, account, settings and language — all of them out of the
   thumb's arc and none of them named. Every destination now lives down here instead, reachable and
   labelled; light/dark and the language picker moved to the Settings page, where a preference belongs.
   That empties the top bar completely, so on a phone it is hidden outright and --bar-h goes to 0 —
   .globe-stage and every other rule written against it follows with no change of its own.
   The bar is hidden above the breakpoint, and hidden again while STUDYING: the grade bar owns the bottom
   of the screen there, the session has its own way out, and a study session is a place you finish, not
   browse from.
   It is a FLEX row rather than a fixed column count: Edit is admin-only (applyMode hides it) and Library
   left for the home page's own banner, so the row is five items for a reader and six for an editor, and
   whatever is on it has to close the gap.
   ============================================================ */
.tabbar{display:none;}
@media (max-width:640px){
  :root{--tabbar-h:58px; --bar-h:0px;}
  .topbar{display:none;}
  .tabbar{
    display:flex; align-items:stretch; gap:0;
    position:fixed; left:0; right:0; bottom:0; z-index:55; height:calc(var(--tabbar-h) + env(safe-area-inset-bottom));
    padding-bottom:env(safe-area-inset-bottom);
    background:color-mix(in srgb, var(--paper) 92%, transparent); border-top:1px solid var(--rule);
    backdrop-filter:saturate(150%) blur(12px); -webkit-backdrop-filter:saturate(150%) blur(12px);
    box-shadow:0 -6px 22px rgba(27,26,23,.06);
  }
  .tabbar .tab{
    flex:1 1 0; min-width:0;
    display:flex; flex-direction:column; align-items:center; justify-content:center; gap:3px;
    padding:0; margin:0; border:0; border-radius:0; background:none; height:100%;
    color:var(--ink-faint); position:relative;
  }
  .tabbar .tab-ic{width:20px; height:20px; opacity:1;}
  /* Every tab is named all the time here, which is the point of moving them: a row of bare icons was the
     problem, and there is no hover on a phone to unfold one with. (The top bar names its tabs outright too
     since Aug 2026 — it used to unfold them from the icon — but it sets the name BESIDE the icon, where
     these sit UNDER it.) At five or six to a row a cell is ~65–78px wide, so the label may not wrap: a
     second line would push the icons off centre.
     The descendant `.tab` in the selector is SPECIFICITY, not decoration: the top bar's own rule sets
     `margin-inline-start:8px`, and at two classes this one lost to it whatever the source order, so the
     SELECTED tab's name sat 8px right of the icon it belongs under — the label is centred in the cell here,
     and any inline margin breaks that. */
  .tabbar .tab .tab-label{display:block; max-width:100%; margin-inline-start:0; margin-inline-end:0;
    font-family:var(--mono); font-size:calc(8.5px * var(--fs)); letter-spacing:.03em; text-transform:uppercase; line-height:1;
    text-align:center; white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
  .tabbar .tab.active{color:var(--indigo);}
  .tabbar .tab.active::before{content:""; position:absolute; top:0; left:50%; transform:translateX(-50%); width:26px; height:2.5px; border-radius:0 0 3px 3px; background:var(--indigo);}
  /* the phone bar draws its own active mark, so the admin colour is repeated here — with an extra class
     for specificity, since a media query adds none and the rule above it is otherwise a tie */
  .tabbar .tab.tab-admin.active{color:var(--admin);}
  .tabbar .tab.tab-admin.active::before{background:var(--admin);}
  .tabbar .tab:active{background:color-mix(in srgb, var(--ink) 5%, transparent);}
  body.grading .tabbar{display:none;}
  /* everything anchored to the bottom of the viewport moves up by the bar's height. The Atlas needs no rule
     here: .globe-stage and .atlas-timebar are already written against --tabbar-h, which is 0 above this
     breakpoint and 58px below it. */
  .stage{padding-bottom:calc(var(--tabbar-h) + 18px);}
  #toast{bottom:calc(var(--tabbar-h) + 22px);}
  body.admin-mode .admin{padding-bottom:var(--tabbar-h);}
}

@media (max-width:640px){
  body{font-size:calc(16px * var(--fs));}
  /* centre page content on phones instead of hugging the top-left. (The top bar is hidden here — see the
     BOTTOM TAB BAR block above — so this padding is now all that separates the page from the screen edge,
     and it has to clear the status bar / notch on a viewport-fit=cover device.) */
  .stage{padding-top:calc(20px + env(safe-area-inset-top));}
  .page-head{text-align:center;}
  .page-head::after{margin-left:auto; margin-right:auto; background:linear-gradient(90deg, transparent, color-mix(in srgb, var(--indigo) 55%, transparent), transparent);}
  .page-head p{margin-left:auto; margin-right:auto;}
  .banner{flex-direction:column; align-items:flex-start; gap:16px; padding:22px 20px;}
  .banner .body{width:100%;}   /* column + align-items:flex-start shrinks children — stretch the body so the CTA's right-alignment reaches the banner edge */
  .study-card{padding:22px 18px 20px;}
  .study-card .num{font-size:calc(26px * var(--fs));}
  .answer{flex-direction:column; align-items:stretch; gap:0;}
  .answer-main{flex-direction:column; align-items:flex-start; gap:5px; width:100%;}
  .av-row{flex-direction:column; align-items:flex-start; gap:10px;}
  /* the rule is the ordinary `--rule` grey here, not the Chinese ink (Sep 2026, on request): on a phone
     it runs the full width of the card, where a red hairline reads as part of the answer rather than as
     the line dividing it from the name below. */
  .ans-cn{align-self:stretch; padding:10px 0 0; margin-top:8px; border-left:0; border-top:1px solid var(--rule);}
  /* .answer stacks here, so "on the right" has nowhere to be — the figures go back under the answer and
     take the full width. Two columns still, which at 390px is what the tiles were sized for anyway. */
  .card-facts{max-width:none; width:100%; margin-top:12px;}
  .grades{grid-template-columns:repeat(2,1fr);}
  .grade-wrap{grid-template-columns:auto auto 1fr; grid-template-areas:"grades grades grades" "help undo suspend"; gap:10px;}
  .grade-wrap .grades{grid-area:grades; justify-self:stretch; grid-template-columns:repeat(2,1fr);}
  .grade-wrap .grade-help{grid-area:help; justify-self:start;}
  .grade-wrap .gb-undo{grid-area:undo; justify-self:start; align-self:center;
    display:inline-flex; align-items:center; gap:5px; white-space:nowrap;
    border:1px dashed var(--rule); background:var(--card); border-radius:9px; padding:7px 11px; margin:0;
    font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; font-weight:600;
    color:var(--ink-faint);}
  .grade-wrap .gb-undo svg{width:12px; height:12px;}
  .grade-wrap .suspendbtn{grid-area:suspend; justify-self:end;}
  /* the study bar's copy steps aside while the grade bar carries one, so a card never shows two Undos */
  body.grading .study-shell .undobtn{display:none;}
  .study-shell .undobtn{margin-left:0;}

  /* (The shortcut digits and the ? bubble's key line are hidden across the whole TOUCH range — phone and
     tablet alike — in the ≤1024px block below this one. They used to be hidden here; a tablet has no
     keyboard either, so the rule had to widen, and it lives in ONE place so the two cannot disagree.) */

  /* ---------- the grade bar's two heights (Aug 2026, on request) ----------
     Drag the grip down and the bar halves: the four grades go side by side as bare COLOURS, and the ?,
     Undo and Suspend shrink to their icons beside them, all on one row. Nothing is taken away — a phone
     reader who wants the card back has the whole bar's function in half its height — and nothing is
     re-rendered: the same markup, laid out differently, so a grade never has to survive a rebuild. */
  /* It is a quiet control on a bar of loud ones — four saturated colours directly under it — so it is
     dimmed further than --ink-faint alone (Aug 2026, on request). Full strength on hover and focus, where
     it has been reached for deliberately. */
  .gb-fold{
    display:flex; align-items:center; justify-content:center; width:100%; height:20px;
    appearance:none; border:0; background:none; padding:0; margin:0 0 -3px; cursor:pointer;
    color:var(--ink-faint); opacity:.5; transition:color .15s var(--ease), opacity .15s var(--ease);
  }
  /* the chevron turns on the SAME clock as the fold it announces — see GB_FOLD_MS in app.js */
  .gb-fold svg{width:20px; height:20px; transition:transform .28s var(--ease);}
  /* while the fold runs, the bar's CONTENTS are inert — see gbFoldingFor in app.js. A press into boxes
     that are mid-animation is what leaves the buttons painted in one place and hit-tested in another,
     which is the reported jam; the chevron sits outside .gradebar-inner and stays live throughout. */
  body.gb-folding .gradebar-inner{pointer-events:none;}
  /* it points the way it will GO: down while the bar is tall (press to fold it away), up once it is short */
  body.gb-compact .gb-fold svg{transform:rotate(180deg);}
  .gb-fold:hover, .gb-fold:focus-visible{color:var(--indigo); opacity:1;}
  .gb-fold:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px; border-radius:6px;}
  /* the bar's own padding shrinks with it, and has to be TRANSITIONED or the fold ends on a 5px jump the
     instant everything else settles — the one part of the movement nothing was animating */
  #gradebar{padding-top:2px; transition:transform .34s var(--ease), padding-bottom .28s var(--ease);}
  body.gb-compact #gradebar{padding-bottom:max(8px, env(safe-area-inset-bottom));}
  /* one row: [?] [four colours] [undo] [suspend] */
  body.gb-compact .grade-wrap{grid-template-columns:auto 1fr auto auto; grid-template-areas:"help grades undo suspend"; gap:8px; align-items:center;}
  body.gb-compact .grade-wrap .grades{grid-template-columns:repeat(4,1fr); gap:6px;}
  /* The SIZES transition in CSS while the POSITIONS are FLIPped in JS (see gbSetCompact): between them
     the fold moves instead of cutting. The two halves have to be written against each other — a button
     whose height snaps under a sliding position looks worse than either doing it alone — so the tall
     state states its height explicitly, `auto` being a value nothing can transition from. Keep the
     280ms in step with GB_FOLD_MS in app.js. */
  .grade{height:calc(56px * var(--fs)); padding:0; overflow:hidden;
    display:flex; flex-direction:column; align-items:center; justify-content:center;
    transition:transform .12s var(--ease), box-shadow .2s, filter .2s,
               height .28s var(--ease), border-radius .28s var(--ease);}
  /* font-size is deliberately NOT transitioned (Aug 2026, on a report that the fold ran roughly). Easing a
     font down to 0 relayouts the text on every frame, and it did so on three buttons that flipMove was
     translating at the same time — the two passes fighting over the same boxes is what the roughness was.
     The labels now go at once and the buttons' geometry eases, which is the half a reader is watching. */
  .grade-help,.grade-wrap .gb-undo,.grade-wrap .suspendbtn{transition:width .28s var(--ease), height .28s var(--ease), padding .28s var(--ease);}
  body.gb-compact .grade{position:relative; padding:0; height:32px; border-radius:8px;}
  /* the label is CLIPPED, not removed: display:none would take it out of the accessibility tree too, and
     then the four buttons would be four unnamed colours to a screen reader as well as to the eye */
  body.gb-compact .grade .gl{position:absolute; width:1px; height:1px; overflow:hidden; clip-path:inset(50%); white-space:nowrap;}
  body.gb-compact .grade .gi, body.gb-compact .grade .gk{display:none;}
  body.gb-compact .grade-help{width:26px; height:26px; font-size:calc(15px * var(--fs));}
  /* icon-only: font-size:0 hides the bare text node beside the svg, which no selector can reach, while the
     svg keeps its own px size — and the text stays in the accessibility tree, as above */
  body.gb-compact .grade-wrap .gb-undo, body.gb-compact .grade-wrap .suspendbtn{font-size:0; gap:0; padding:6px 7px;}
  body.gb-compact.grading .stage{padding-bottom:96px;}
  body.gb-compact.grading .wb-tools{bottom:76px;}
  /* ---- the game tiles, three to a row under their heading ----
     They were two to a row on the pane they had to themselves; that pane is gone (Aug 2026, on request) and
     the six tiles now sit under the review as a 3×2 block. Three to a row leaves about 86px of text column,
     which is why the taglines are dropped at the source rather than here — see gameSub in PAGES.home. */
  .game-grid{grid-template-columns:repeat(3,1fr); gap:10px;}
  /* the padding is 9px, not 14, because the title has to fit on ONE line (Aug 2026, on request): three
     tiles to a 390px row leave about 89px of text at 14px of padding, and "Multiple Choice" needs 95. Nine
     gives it 97, and the type below drops with it. */
  .game-tile{min-height:112px; --gt-px:9px; --gt-py:14px;}
  .gt-title{font-size:calc(12.5px * var(--fs)); line-height:1.15;}   /* one line per tile — see the padding note above */
  .gt-glyph-svg svg{width:78px; height:78px;}   /* a 112px ornament fills a 110px tile edge to edge */
  .gt-glyph-svg{top:-10px; right:-10px;}
  /* ---- THE TILE'S BACK, AT A THIRD OF A 390px ROW (Aug 2026, on a bug report) ----
     Two columns of three rows in 90px of usable width is 34px a column, which is not a layout — it is
     overlapping half-words clipped by the tile's own `overflow:hidden`, 167px of content in a 110px box.
     So the columns STACK, the type comes down a step, the site half falls back to its one-line form
     (`.gtb-brief`, emitted beside the grid by gameBackHTML) and "Tap to play" goes: a phone reader knows
     a tile is tapped, and it was the least informative line of the six. What is left is the reader's own
     three figures and today's site average, which is what the back is for. */
  .gt-back{padding:9px 9px 8px; gap:3px;}
  .gtb-head{font-size:calc(10.5px * var(--fs)); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
  .gtb-cols{grid-template-columns:1fr; gap:4px; align-content:start;}
  .gtb-lbl{font-size:calc(7px * var(--fs)); letter-spacing:.06em;}
  .gtb-grid{gap:0 5px;}
  .gtb-k{font-size:calc(8px * var(--fs)); letter-spacing:.02em;}
  .gtb-v{font-size:calc(9.5px * var(--fs));}
  .gtb-site .gtb-grid, .gtb-site .gtb-none{display:none;}
  .gtb-site .gtb-brief{display:block;}
  .gtb-tap{display:none;}
  /* the offer's two controls go under its prose rather than beside it — at 390px "Take the tour" and
     "No thanks" on the same line as the sentence leave the sentence four words wide */
  .to-acts{flex:1 1 100%; justify-content:flex-start;}
  .tour-card{padding:17px 18px 15px; border-radius:13px; max-height:calc(100dvh - 32px);}
  .tour-card h3{font-size:calc(18px * var(--fs));}
  .folio-tour,.page-help{padding:14px;}
  /* ---- the walkthrough DOCKS to the bottom on a phone (Aug 2026, on a bug report) ----
     Centred, the card takes half to two thirds of a 640px screen and lands squarely on the thing it is
     describing — and there is nowhere to nudge it to, so on most steps the target ended up UNDER it and
     the step pointed at something the reader could not see. Docked, the whole upper half of the screen is
     free, and `tourPlace` scrolls the target into it. Nothing above the breakpoint changes: there the card
     is centred and the nudge has room to work.
     `tourPlace` reads this back off the overlay's own computed style rather than repeating the breakpoint
     in JS — see the `dock` line there. */
  .folio-tour{align-items:flex-end;}
  .folio-tour.tour-corner{align-items:flex-end; justify-content:center; padding-top:14px; padding-bottom:calc(14px + var(--tour-above, 0px));}
  .folio-tour.tour-corner .tour-card{width:100%;}
  .td-g b{font-size:calc(11px * var(--fs));}
  .td-g{padding:7px 2px;}
  .chrono-actions{flex-direction:column;} .chrono-actions .btn{width:100%;}
  body.grading .stage{padding-bottom:206px;}
  body.grading .wb-tools{bottom:170px;}
  /* ---- the review banner's three piles keep the button's line on a phone ----
     Three figures over three labels plus a 148px button do not fit 300px, and the button dropped to a line
     of its own — leaving the piles floating over nothing. The button gives up the width (it is one short
     phrase, and the banner is a button in its own right, so this one is a repetition of the obvious) and
     the row is held to ONE line: the labels are the only thing that could wrap, and a wrapped "LEARNING"
     would break the column it sits under. */
     The button is CENTRED against the piles (Aug 2026, on request), not sat on their baseline: a figure
     over a label is a two-line column, and a one-line button aligned to the bottom of it reads as having
     slipped down. */
  .review-group .banner .meta{flex-wrap:nowrap; gap:11px; align-items:center;}
  .review-group .banner .stat span{font-size:calc(8.5px * var(--fs)); letter-spacing:.06em; white-space:nowrap;}
  .review-group .banner .cta .btn{padding:11px 13px; font-size:calc(10.5px * var(--fs)); letter-spacing:.05em; white-space:nowrap;}
  /* clear of the bottom tab bar, not sitting on top of it: the marker is a control, and a control that
     covers the row of destinations reads as a bug however useful it is. (While grading the tab bar is gone
     and the grade bar owns that edge — hence the rule above, and the 430px one further down.) */
  .wb-tools{right:12px; bottom:calc(var(--tabbar-h) + 12px);}
  /* On the Atlas the timebar owns the strip above the tab bar, so the marker clears BOTH — and steps left
     of the zoom column (34px wide at a 16px inset), which occupies the same corner. */
  .wb-tools.on-atlas{right:62px; bottom:calc(var(--tabbar-h) + var(--timebar-h) + 10px);}
  .collection-row{padding:18px 16px; gap:14px;}
  .collection-title{font-size:calc(20px * var(--fs));}
  .counts{width:100%; margin-left:0; margin-top:4px;}
  body{--crit-slot:bar;}
  .dp-name{width:120px;}
  .profile{flex-direction:column; text-align:center;}
  .namefield{text-align:center;}
  /* the profile stacks here, so the account buttons take the full width in a row rather than hanging off
     a right edge that no longer exists — the desktop's 2×2 grid is for the space beside the name, and
     there is no beside once the card is a column */
  /* THE FOUR STAY A 2x2 GRID ON A PHONE (Aug 2026, on a bug report: the labels were neither centred nor
     inside their boxes). They were a flex ROW that wrapped, so each button took whatever width its
     neighbours left it — "Change password" and "Email address" are long enough that at 390px they were
     laid out narrower than their own text and overflowed the border. A grid gives every one of them the
     same half-width box, and the label is then centred and wrapped INSIDE it rather than spilling out.
     `width:100%` on the block (it is a flex item of the profile card, which does not stretch it) and
     `white-space:normal` on the buttons, whose base rule keeps them on one line. */
  .acct-idacts{width:100%; grid-template-columns:repeat(2, minmax(0, 1fr));}
  .acct-idacts .ghost-btn{
    width:100%; min-width:0; padding-inline:10px;
    white-space:normal; text-align:center; justify-content:center;
    font-size:calc(12px * var(--fs)); line-height:1.25;
  }
  .acct-syncnote{margin:-10px 0 16px; text-align:center; padding-left:0;}
}

/* ---- NOTHING TOUCH-SIZED NAMES A KEY IT HAS NOT GOT (Aug 2026, on request; widened to tablets Sep 2026,
   on request: "on tablet, ensure the grading buttons don't mention their keyboard shortcuts") ----
   The digit under each grade and the shortcut line in the ? bubble both describe a keyboard. A phone has
   none and a tablet has none either, so on both they are furniture explaining a control that cannot be
   reached: the digits cost the four buttons a line of height each, and the shortcut line costs the bubble
   a third of its own. They are hidden rather than dropped from the markup, because the same markup is what
   a desktop reader gets — and there the keys are real and worth saying.

   THE BREAKPOINT IS 1024px, WHICH IS THE SITE'S OWN TABLET LINE — the width `--cp-sheet` already uses to
   decide that the Atlas place panel is a sheet rather than a desktop column, so a tablet is one thing here
   and not two. And it is ONE rule at ONE width rather than a copy inside each narrower block: the ≤640px
   and ≤430px blocks both used to carry a `.gk` line of their own, which is exactly how two rules about one
   decision come to disagree. */
@media (max-width:1024px){
  .grade .gk{display:none;}
  .grade-help-bubble .ghb-keys{display:none;}
}

/* ---- narrow phones: the grade bar in ONE row ----
   Two rows of two, plus a row for help and suspend, took about a quarter of a 390px screen — over a card
   whose background already runs several screens. Four columns fit once the shortcut digits go (they name
   keys a phone does not have) and the interval sits closer under the label. */
@media (max-width:430px){
  /* Three tiles to a row on a 390px screen is about 110px each, so the display title comes down again:
     at 18px "Multiple Choice" breaks to three lines. */
  .game-grid{gap:8px;}
  .game-tile{--gt-px:8px; --gt-py:12px;}
  .gt-title{font-size:calc(12px * var(--fs)); line-height:1.15;}
  .gt-sub{font-size:calc(11px * var(--fs)); line-height:1.3; margin-top:3px;}
  .gt-eyebrow{margin-bottom:5px;}
  .grade-wrap .grades{grid-template-columns:repeat(4,1fr); gap:6px;}
  .grade{padding:9px 3px 8px; border-radius:9px;}
  .grade .gl{font-size:calc(10px * var(--fs)); letter-spacing:.02em;}
  .grade .gi{font-size:calc(9px * var(--fs)); margin-top:4px;}
  /* (.grade .gk is hidden across the whole TOUCH range, phone and tablet — see the ≤1024px block above) */
  .grade-wrap{gap:8px;}
  body.grading .stage{padding-bottom:150px;}
  body.grading .wb-tools{bottom:120px;}
  /* "studied 3h 07m today" — the longest this line gets — runs into the "+ Add decks" lip on a 320px
     screen, and at Very large text on a 390px one, and ellipsises, which says less than the shorter
     sentence does. Nothing is lost: the block it sits in is called Daily study, so the day is not what
     the line is carrying. Measured at both, in the medium and largest text sizes. */
}

/* ---------- the global motion killswitch ----------
   TWO ways in and one behaviour (Aug 2026, on request: "in the settings, a user should be able to turn off
   all animations, since they may cause lag on some devices"). The media query is the operating system's
   request; `body.no-anim` is Folio's own switch, written by applyTheme from S.settings.animations, and
   prefersReducedMotion() in app.js reads that same setting — so the JS-driven motion (the page ghost, the
   sheets' exits, the smooth scroll, the globe's camera) stops with the CSS-driven motion rather than
   half of it carrying on.
   Everything in the file lands on its END state, which is why entrance animations need no per-rule
   handling; the two targeted opt-outs below (the confetti, the gold shine) are the cases where the end
   state is itself wrong. */
@media (prefers-reduced-motion: reduce){
  *,*::before,*::after{animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important;}
}
body.no-anim *,body.no-anim *::before,body.no-anim *::after{animation-duration:.001ms !important; animation-iteration-count:1 !important; transition-duration:.001ms !important;}
body.no-anim .page > *{animation-delay:0s !important;}
body.no-anim .lu-conf,body.no-anim .lu-burst{display:none;}
body.no-anim .gt-seal{animation:none;}   /* the tile's own marks no longer animate — only the review banner's seal */
body.no-anim .data-loading .dl-globe,body.no-anim .data-loading .dl-globe::before{animation:none; border-color:var(--rule); border-top-color:var(--indigo);}

/* ============================================================
   GLOSSARY TOOLTIP TERMS (background text)
   ============================================================ */
.ttip{
  color:var(--indigo); cursor:pointer;
  text-decoration:underline; text-decoration-style:dotted; text-decoration-thickness:1px;
  text-underline-offset:2px;
  text-decoration-color:color-mix(in srgb,var(--indigo) 45%,transparent);
  transition:color .15s var(--ease), text-decoration-color .15s var(--ease), background .15s var(--ease);
  border-radius:3px;
}
/* An UNDISCOVERED term wears --newterm, a teal of its own (Aug 2026, on request — it wore --ochre, the
   gold of the blank in a card's question, until the Library books gave that teal up). A term already
   read carries no attribute and keeps the ordinary glossary-link styling above, unchanged — the mark is
   the invitation, not a record of what is finished. (markTtipNew sets data-new; deck terms never get it.) */
.ttip[data-new]{
  color:var(--newterm);
  text-decoration-color:color-mix(in srgb,var(--newterm) 55%,transparent);
}
.ttip:hover,.ttip:focus-visible{
  color:var(--indigo-bright); text-decoration-color:var(--indigo-bright);
  background:var(--indigo-wash); outline:none;
}
.night .ttip:hover,.night .ttip:focus-visible{ color:var(--indigo); background:color-mix(in srgb,var(--indigo) 16%,transparent); }
/* An undiscovered term keeps its own colour under the cursor — jumping to the indigo hover would read as
   the term changing state before it has been opened. These sit AFTER the rules above: equal specificity,
   so source order decides (and the .night pair below outranks .night .ttip:hover outright). */
.ttip[data-new]:hover,.ttip[data-new]:focus-visible{
  color:var(--newterm); text-decoration-color:var(--newterm);
  background:color-mix(in srgb,var(--newterm) 13%,transparent); outline:none;
}
.night .ttip[data-new]:hover,.night .ttip[data-new]:focus-visible{
  color:var(--newterm); background:color-mix(in srgb,var(--newterm) 18%,transparent);
}
.ttip b,.ttip i,.ttip strong{ color:inherit; }
/* THE DAILY QUOTE'S AUTHOR AND WORK ARE LINKED WITHOUT AN UNDERLINE (Sep 2026, on request). The caption
   is a set line of small capitals under a quotation, and a dotted rule under the speaker's name reads as
   decoration on a signature rather than as a link; the colour alone says it can be pressed, and the
   hover wash still marks it under the cursor. */
.daily-quote figcaption .ttip{ text-decoration:none; }

/* ---- discovery chip (shared: the glossary popup's bar and the Atlas info panel) ----
   Shown on the FIRST opening of a term or a place and never again, carrying the running count, because
   the moment of discovery is where the progress means something — not the account page nobody is looking
   at just then. It takes the same gold as the level badges, so a find reads in the reward language the
   site already speaks. */
.disc-chip{
  position:relative; flex:none; display:inline-flex; align-items:center; gap:6px;
  font-family:var(--mono); font-size:calc(9px * var(--fs)); font-weight:600; letter-spacing:.11em; text-transform:uppercase;
  padding:3px 8px; border-radius:999px; white-space:nowrap;
  color:#7A5E10; background:color-mix(in srgb,#C39A2E 22%,transparent);
  border:1px solid color-mix(in srgb,#C39A2E 46%,transparent);
  animation:discPop .5s var(--ease) both, discRing .9s var(--ease) both;
}
/* the second ring of the splash, a beat behind the first */
.disc-chip::after{
  content:""; position:absolute; inset:0; border-radius:inherit; pointer-events:none;
  animation:discRing .9s .22s var(--ease) both;
}
.disc-chip b{font-weight:700;}
.disc-n{opacity:.72; letter-spacing:.04em;}
.night .disc-chip{ color:#E6C765; background:color-mix(in srgb,#E6C765 15%,transparent); border-color:color-mix(in srgb,#E6C765 40%,transparent); }
/* Pop with a little overshoot, then two rings splashing outwards.
   The rings are BOX-SHADOW spread, not a scaled pseudo-element: the chip sits inside .gloss-win
   (overflow:hidden) and inside .cp-cols (overflow-y:auto), where a transformed ring would be clipped by
   the one and could add a scrollbar to the other. A box-shadow never enters the scrollable overflow area.
   Both animations end fully transparent, so the reduced-motion killswitch at the foot of this file —
   which lands every animation on its END state — correctly leaves no ring behind. */
@keyframes discPop{ 0%{opacity:0; transform:scale(.72);} 55%{opacity:1; transform:scale(1.12);} 100%{opacity:1; transform:scale(1);} }
@keyframes discRing{
  from{ box-shadow:0 0 0 0 color-mix(in srgb,#C39A2E 62%,transparent); }
  to  { box-shadow:0 0 0 15px color-mix(in srgb,#C39A2E 0%,transparent); }
}
/* the chip runs two animations, the ::after only one — so the name lists differ */
.night .disc-chip{ animation-name:discPop, discRingNight; }
.night .disc-chip::after{ animation-name:discRingNight; }
@keyframes discRingNight{
  from{ box-shadow:0 0 0 0 color-mix(in srgb,#E6C765 55%,transparent); }
  to  { box-shadow:0 0 0 15px color-mix(in srgb,#E6C765 0%,transparent); }
}
.cp-new{margin:0;}   /* it sits on the title's own row now (see .cp-titlerow) — the spacing is the row's */
.cp-new[hidden]{display:none;}

/* ---- glossary windows (desktop) ---- */
.gloss-win{
  position:fixed; width:375px; max-width:calc(100vw - 24px);
  background:var(--card); border:1px solid var(--rule); border-radius:14px;
  box-shadow:var(--shadow-lg); display:flex; flex-direction:column; overflow:hidden;
  opacity:0; transform:translateY(6px) scale(.98);
  transition:opacity .16s var(--ease), transform .16s var(--ease);
}
.gloss-win.show{ opacity:1; transform:none; }
.gloss-win.closing{ opacity:0; transform:translateY(6px) scale(.98); pointer-events:none; }
.gloss-win.dragging{ transition:none; box-shadow:var(--shadow-lg), 0 0 0 2px var(--indigo-wash); }
.gloss-win.flash{ animation:glossFlash .55s var(--ease); }
@keyframes glossFlash{ 0%,100%{box-shadow:var(--shadow-lg);} 35%{box-shadow:var(--shadow-lg), 0 0 0 3px color-mix(in srgb,var(--indigo) 45%,transparent);} }
.gloss-bar{
  position:relative;   /* the phone window's grab mark is a ::before on this bar */
  display:flex; align-items:center; gap:8px; padding:13px 11px 5px 16px;
  background:linear-gradient(90deg, color-mix(in srgb, var(--indigo) 9%, transparent), transparent 78%); border-bottom:0;
  cursor:grab; user-select:none; touch-action:none;
}
.gloss-bar:active{ cursor:grabbing; }
.gloss-title{
  flex:1; min-width:0; font-family:var(--display); font-weight:600; font-size:calc(15px * var(--fs));
  color:var(--zh); letter-spacing:-.01em; white-space:normal; overflow-wrap:break-word; line-height:1.25;
}
/* It may run to SEVERAL lines (Aug 2026, on request) — see glossDatesHTML — so it needs a line-height of
   its own: the default leading on 11px mono sets two dates almost touching. */
.gloss-dates{
  display:block; margin:0 0 8px; font-family:var(--mono); font-weight:500; font-size:calc(11px * var(--fs));
  letter-spacing:.03em; color:var(--ink-faint); line-height:1.55;
}
.gloss-close{
  flex:none; appearance:none; cursor:pointer; width:26px; height:26px; border-radius:7px;
  border:1px solid transparent; background:none; color:var(--ink-faint);
  display:inline-flex; align-items:center; justify-content:center; transition:all .15s var(--ease);
}
.gloss-close svg{ width:15px; height:15px; }
.gloss-close:hover{ background:var(--zh-wash); color:var(--zh); border-color:color-mix(in srgb,var(--zh) 30%,transparent); }
.gloss-close:focus-visible{ outline:2px solid var(--zh); outline-offset:1px; }
.gloss-edit{ flex:none; appearance:none; cursor:pointer; width:26px; height:26px; border-radius:7px; border:1px solid transparent; background:none; color:var(--ink-faint); display:inline-flex; align-items:center; justify-content:center; transition:all .15s var(--ease); }
.gloss-edit svg{ width:14px; height:14px; }
.gloss-edit:hover{ background:var(--indigo-wash); color:var(--indigo); border-color:color-mix(in srgb,var(--indigo) 30%,transparent); }
.gloss-edit:focus-visible{ outline:2px solid var(--indigo); outline-offset:1px; }
/* the map marker beside the close button: shown only on a term the Atlas can actually show — a country it
   draws, or a place with a published coordinate (see glossPlace). It is OCHRE rather than indigo, because
   what it opens is gold on the globe, and the two should read as the same gesture. */
.gloss-map{ flex:none; appearance:none; cursor:pointer; width:26px; height:26px; border-radius:7px; border:1px solid transparent; background:none; color:var(--ink-faint); display:inline-flex; align-items:center; justify-content:center; transition:all .15s var(--ease); }
.gloss-map svg{ width:15px; height:15px; }
.gloss-map:hover{ background:color-mix(in srgb,var(--ochre) 16%, transparent); color:var(--ochre); border-color:color-mix(in srgb,var(--ochre) 40%,transparent); }
.gloss-map:focus-visible{ outline:2px solid var(--ochre); outline-offset:1px; }
.gloss-body{ padding:4px 16px 16px; max-height:48vh; overflow-y:auto; }
.gloss-desc{ margin:0; font-family:var(--sans); font-size:calc(13.5px * var(--fs)); line-height:1.58; color:var(--ink-soft); }
/* a term's illustration: the same frame + fullscreen viewer as a card image, but FLOATED to the top-right of
   the popup body (it is first in the DOM, which is what a float needs) rather than sized by a fixed 16:9 box.
   The opening sentences run down its left, and the description resumes the popup's full width below it.
   The 150px height and the half-popup width are the picture's MAXIMUM, not its shape (Aug 2026, on request):
   within those limits the whole picture is shown rather than cropped to fill them. It used to be a fixed
   150px height with `object-fit:cover`, which gave every popup one silhouette at the cost of cutting the
   sides off anything wider than half the popup — and a map, a diagram or a wide landscape is exactly the
   kind of picture a glossary term carries. A tall picture is now narrow and a wide one short, and both are
   whole. */
.gloss-imgslot{ float:right; max-width:50%; margin:0 0 10px 12px; }
.gloss-imgslot[hidden]{ display:none; }
/* …and the words wait for it. A floated picture with no intrinsic size yet is a float of no width, so the
   description lays out across the whole popup and then re-wraps the instant the file arrives — the jump
   this hides. The body is revealed once the picture's size is known (see GLOSS_IMG_WAIT in app.js), and
   the slot holds its maximum height in the meantime so the popup's own box barely moves when it is. */
.gloss-body{ transition:opacity .14s var(--ease); }
.gloss-win[data-imgwait] .gloss-body{ opacity:0; }
.gloss-win[data-imgwait] .gloss-imgslot{ min-height:150px; }
.gloss-imgslot .card-img{
  margin:0; border-radius:8px; line-height:0;
  aspect-ratio:auto; width:auto; max-width:100%;
}
.gloss-imgslot .card-img img{ width:auto; height:auto; max-height:150px; max-width:100%; object-fit:contain; }
/* a video keeps its 16:9 shape at that same height — a term shows one frame, a picture or a clip */
.gloss-imgslot .card-vid{ aspect-ratio:16/9; height:150px; width:auto; }
.gloss-imgslot .ci-zoom{ right:8px; bottom:8px; width:24px; height:24px; border-radius:7px; }
.gloss-imgslot .card-vid .ci-zoom{ top:8px; bottom:auto; }
.gloss-imgslot .ci-zoom svg{ width:13px; height:13px; }

/* ---- glossary bottom sheet (mobile) ----
   `will-change:transform` is not decoration here, it is the fix for a reported flicker: the sheet
   blinked out for a fraction of a second the instant its slide-up finished. Nothing in the markup or
   the styles changes at that moment — a per-frame probe of the sheet shows opacity 1, visibility
   visible and one .gloss-win throughout — so the gap is the browser DISCARDING the compositing layer
   it created for the transform animation and repainting the sheet back into the page, one or two
   frames later than it removed it. The bottom of the screen is the worst place for that to happen: the
   tab bar underneath is a backdrop-filter element, so the repaint drags a blur re-sample along with it.
   Declaring the promotion up front means the layer is never created and never thrown away, and the
   transition ends without a repaint at all. Keep it on the sheet only — a permanent layer per element
   is cheap once and expensive by the dozen, and the desktop popups fade rather than slide. */
/* A CENTRED FLOATING WINDOW on a phone, not a bottom sheet (Aug 2026, on request). It rose out of the
   bottom edge for months; a definition met mid-sentence belongs in the middle of the screen, where the
   reader's eye already is, and the sheet was also the furthest point on the page from the word tapped.
   What a centred window loses is the sheet's implicit "there is a page behind me", so its bar can be HELD
   and dragged up or down to uncover the sentence the term came from — vertically only, since the window is
   as wide as the screen allows and a horizontal drag would only fight the page swipe.
   `--gs-dy` is that offset, and it rides INSIDE the centring transform rather than replacing it with a
   `top`: mixing the two would mean measuring the window's height on every move. It is not remembered — a
   new term always opens in the middle — so it lives on the element and dies with it. */
.gloss-win.gloss-sheet{
  --gs-dy:0px;
  left:50%; right:auto; top:50%; bottom:auto;
  width:calc(100% - 24px); max-width:460px;
  border-radius:16px;
  box-shadow:0 18px 54px rgba(0,0,0,.28);
  opacity:0; transform:translate(-50%,calc(-50% + var(--gs-dy) + 14px)) scale(.97);
  transition:transform .26s var(--ease), opacity .2s var(--ease);
  will-change:transform; backface-visibility:hidden; -webkit-backface-visibility:hidden;
  z-index:9600;
}
.gloss-win.gloss-sheet.show{ opacity:1; transform:translate(-50%,calc(-50% + var(--gs-dy))) scale(1); }
.gloss-win.gloss-sheet.closing{ opacity:0; transform:translate(-50%,calc(-50% + var(--gs-dy) + 10px)) scale(.97); }
.gloss-win.gloss-sheet.dragging{ transition:none; }
/* THE WHOLE WINDOW IS THE HANDLE (Aug 2026, on request — it was the title bar, then a grab bar at each
   end, and both marks are gone with them). `touch-action:none` on the window is what lets a vertical
   drag be claimed here instead of being taken as a page scroll; the BODY gets it back, because when
   there is more description than fits, that box has to scroll. See makeGlossSheetDraggable, which reads
   the same distinction at pointerdown so the two can never both act on one gesture. */
.gloss-sheet{ touch-action:none; cursor:grab; }
.gloss-sheet.dragging{ cursor:grabbing; }
.gloss-sheet .gloss-body{ touch-action:pan-y; }
.gloss-sheet .gloss-bar{ padding:13px 12px 13px 18px; cursor:inherit; }
.gloss-sheet .gloss-title{ font-size:calc(16.5px * var(--fs)); }
.gloss-sheet .gloss-dates{ font-size:calc(12px * var(--fs)); }
.gloss-sheet .gloss-body{ padding:15px 18px 20px; max-height:58vh; }
.gloss-sheet .gloss-desc{ font-size:calc(15px * var(--fs)); }
.gloss-sheet .gloss-imgslot{ margin-left:15px; }
.gloss-sheet .gloss-imgslot .card-img{ border-radius:10px; }
.gloss-sheet .gloss-imgslot .card-img img{ height:auto; max-height:170px; }
.gloss-sheet[data-imgwait] .gloss-imgslot{ min-height:170px; }   /* the sheet's taller ceiling — see the base rule */
.gloss-sheet .gloss-imgslot .card-vid{ height:170px; border-radius:10px; }

/* THE SCRIM — everything behind the phone's popup goes soft while it is up (Aug 2026, on request).
   ONE element with backdrop-filter, not a filter over a list of the page's own containers: that list
   would need keeping in step with every fixed thing on the site, and a filter on an ancestor becomes the
   containing block for its position:fixed descendants, which would move the very bars it was blurring.
   IT TAKES THE PRESSES (Aug 2026, on request: nothing behind a popup should be clickable until it closes).
   It began as focus alone and is now a modal, which one property buys outright — at z-index 9590 it is
   above every bar and control on the site, so nothing has to be disabled by name. touch-action:none goes
   with it: a scrim that swallows a tap must not leave the browser free to treat the same tap as a scroll
   or a double-tap zoom. A single tap on it still does nothing; a DOUBLE tap closes (see glossDoubleTap). */
.gloss-scrim{
  position:fixed; inset:0; z-index:9590; pointer-events:none; touch-action:none; opacity:0;
  backdrop-filter:blur(7px) saturate(88%); -webkit-backdrop-filter:blur(7px) saturate(88%);
  background:color-mix(in srgb, var(--paper) 22%, transparent);
  transition:opacity .22s var(--ease);
}
/* The blocking rides on `.on`, not on the base rule: the scrim is faded out for 220ms before it is removed
   from the document, and for those 220ms the popup is already gone — a scrim still eating taps there would
   leave the page dead to the touch just after the reader closed the thing that made it so. */
.gloss-scrim.on{ opacity:1; pointer-events:auto; }

/* glossary editor: fields on the left, a live popup preview on the right */
.gloss-edit-cols{ display:flex; gap:0; align-items:flex-start; flex-wrap:wrap; }
/* draggable divider that resizes the card/glossary preview column (the ~28px gap is provided by its margins) */
.ed-resizer{ flex:none; align-self:stretch; width:8px; margin:0 10px; cursor:col-resize; position:relative; z-index:3; touch-action:none; }
.ed-resizer::after{ content:""; position:absolute; left:3.5px; top:0; bottom:0; width:1px; background:var(--rule); transition:background .12s; }
.ed-resizer:hover::after, .ed-resizer.dragging::after{ background:var(--indigo-bright); width:2px; left:3px; }
.gloss-edit-fields{ flex:1 1 320px; min-width:0; display:flex; flex-direction:column; gap:4px; }
.gloss-edit-fields .admin-field{ margin-top:10px; }
.gloss-edit-fields .admin-field:first-child{ margin-top:0; }
.gloss-edit-preview{ flex:0 0 var(--gloss-preview-w, 250px); min-width:0; position:sticky; top:0; }
.gloss-preview-label{ font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint); margin-bottom:10px; }
.gloss-win.gloss-preview-win{ position:static; opacity:1; transform:none; transition:none; box-shadow:var(--shadow-md); max-width:none; }
.gloss-preview-win .gloss-bar{ cursor:default; }
.gloss-preview-win .gloss-close{ pointer-events:none; }
.gloss-preview-win .gloss-body{ max-height:none; }

/* card editor: fields on the left, a live card preview on the right */
.card-edit-cols{ display:flex; gap:0; align-items:flex-start; flex-wrap:wrap; }
.card-edit-fields{ flex:1 1 360px; min-width:0; }
.card-edit-preview{ flex:0 0 var(--card-preview-w, 360px); min-width:0; position:sticky; top:0; }
.card-edit-preview .admin-card-pvbox{ border:1px solid var(--rule); border-radius:14px; background:var(--card); padding:6px 4px; box-shadow:var(--shadow-sm); max-height:calc(100vh - 230px); overflow-y:auto; }
.card-edit-preview .admin-pv-card{ width:100%; max-width:none; margin:0; box-shadow:none; border:none; }

/* ============================================================
   ADMIN — back-end card & glossary editor
   ============================================================ */
body.admin-mode{overflow:hidden;}
body.admin-mode .stage{max-width:none; margin:0; padding:0;}
body.admin-mode .page{animation:none;}
.admin{display:flex; height:calc(100vh - var(--bar-h)); background:var(--paper); font-family:var(--sans); color:var(--ink); text-align:left;}
.admin section, .admin aside{min-height:0;}

/* sidebar (tabs + tree) */
.admin-side{width:var(--side-w,250px); flex:none; display:flex; flex-direction:column; background:var(--paper-2); border-right:1px solid var(--rule); overflow:hidden;}
.admin-side.collapsed{width:0; border-right:0;}
.admin-tabs{flex:none; display:grid; grid-template-columns:1fr 1fr; gap:8px; padding:14px;}
.admin-tab{display:flex; align-items:center; justify-content:center; min-height:54px; appearance:none; border:1px solid transparent; border-radius:11px; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; font-weight:700; cursor:pointer; transition:transform .12s ease, box-shadow .15s, background .15s, color .15s;}
.admin-tab:hover{transform:translateY(-1px);}
/* Dashboard is the editor's front page — the tab a fresh session opens on, and the only one that
   describes the whole site rather than a kind of content. So it spans BOTH columns and sits above the
   four kinds of work as a header rather than as a fifth peer (Aug 2026, on request). `1 / -1` rather
   than `span 2`, so it stays full width if the grid ever gains a third column. */
.admin-tab[data-atab="dashboard"]{grid-column:1 / -1; min-height:46px; color:#7a4fd0; background:rgba(122,79,208,.13); border-color:rgba(122,79,208,.32);}
.admin-tab[data-atab="dashboard"].active{color:#fff; background:#7a4fd0; border-color:#7a4fd0; box-shadow:0 5px 15px -7px rgba(122,79,208,.9);}
.admin-tab[data-atab="cards"]{color:#4f6bed; background:rgba(79,107,237,.12); border-color:rgba(79,107,237,.30);}
.admin-tab[data-atab="cards"].active{color:#fff; background:#4f6bed; border-color:#4f6bed; box-shadow:0 5px 15px -7px rgba(79,107,237,.9);}
.admin-tab[data-atab="glossary"]{color:#1f9d63; background:rgba(38,158,107,.13); border-color:rgba(38,158,107,.32);}
.admin-tab[data-atab="glossary"].active{color:#fff; background:#2e9e6b; border-color:#2e9e6b; box-shadow:0 5px 15px -7px rgba(38,158,107,.9);}
/* Quotes and Artefacts shipped without a colour of their own and fell through to the bare `.admin-tab`
   rule — transparent, in the inherited ink — so two of the seven tabs read as disabled beside five that
   are lit (Aug 2026, on a bug report). Both hues are placed rather than picked: the four that existed took
   blue (cards), green (glossary), amber (timeline) and red (feedback), with purple over them all for the
   dashboard, so what was left free was TEAL and MAGENTA. Quotes takes the teal and Artefacts the magenta,
   which also puts the two content tabs that write into app.js's own literals at opposite ends of the wheel
   from each other. */
.admin-tab[data-atab="quotes"]{color:#0f8a91; background:rgba(15,138,145,.13); border-color:rgba(15,138,145,.32);}
.admin-tab[data-atab="quotes"].active{color:#fff; background:#118e96; border-color:#118e96; box-shadow:0 5px 15px -7px rgba(15,138,145,.9);}
.admin-tab[data-atab="artefacts"]{color:#a8478f; background:rgba(168,71,143,.13); border-color:rgba(168,71,143,.32);}
.admin-tab[data-atab="artefacts"].active{color:#fff; background:#a8478f; border-color:#a8478f; box-shadow:0 5px 15px -7px rgba(168,71,143,.9);}
.admin-tab[data-atab="themes"]{color:#6d8f1f; background:rgba(109,143,31,.14); border-color:rgba(109,143,31,.33);}
.admin-tab[data-atab="themes"].active{color:#fff; background:#6d8f1f; border-color:#6d8f1f; box-shadow:0 5px 15px -7px rgba(109,143,31,.9);}
.admin-tab[data-atab="timeline"]{color:#bd831f; background:rgba(201,138,43,.14); border-color:rgba(201,138,43,.33);}
.admin-tab[data-atab="timeline"].active{color:#fff; background:#c98a2b; border-color:#c98a2b; box-shadow:0 5px 15px -7px rgba(201,138,43,.9);}
.admin-tab[data-atab="feedback"]{color:#c0463a; background:rgba(192,70,58,.13); border-color:rgba(192,70,58,.32);}
.admin-tab[data-atab="feedback"].active{color:#fff; background:#c0463a; border-color:#c0463a; box-shadow:0 5px 15px -7px rgba(192,70,58,.9);}
.admin-tree{flex:1; overflow-y:auto; padding:2px 8px 12px;}
.admin-tree-note{padding:14px 12px; color:var(--ink-faint); font-size:calc(12.5px * var(--fs)); line-height:1.55;}
.admin-tree-note b{color:var(--ink-soft);}
/* glossary tab: the left bar lists every tag with its term count; clicking one filters the term list */
.admin-tag-list{display:flex; flex-direction:column; gap:1px; padding:0 4px 14px;}
.admin-tag{width:100%;}
.admin-tag .an-label{text-transform:lowercase;}
.admin-node-row{display:flex; align-items:stretch; padding-left:calc(var(--depth, 0) * 13px);}
.an-twisty{flex:none; width:22px; appearance:none; border:0; background:none; color:var(--ink-faint); cursor:pointer; display:flex; align-items:center; justify-content:center; border-radius:5px;}
.an-twisty svg{width:12px; height:12px; transition:transform .2s var(--ease);}
.an-twisty.open svg{transform:rotate(90deg);}
.an-twisty:hover{color:var(--ink); background:color-mix(in srgb, var(--ink) 6%, transparent);}
.an-twisty-spacer{flex:none; width:22px;}
.admin-node{flex:1; min-width:0; display:flex; align-items:center; gap:8px; appearance:none; border:0; background:none; cursor:pointer; padding:6px 9px; border-radius:6px; color:var(--ink-soft); text-align:left; transition:background .14s, color .14s;}
.admin-node:hover{background:color-mix(in srgb, var(--ink) 5%, transparent); color:var(--ink);}
.admin-node.active{background:var(--indigo-wash); color:var(--indigo);}
.admin-node.branch .an-label{font-weight:600;}
.an-label{flex:1; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; font-size:calc(13px * var(--fs));}
.an-count{flex:none; font-family:var(--mono); font-size:calc(10px * var(--fs)); color:var(--ink-faint); opacity:.7;}
.admin-node.active .an-count{color:var(--indigo); opacity:.8;}
.admin-node.node-edited .an-count::before{content:"\25CF"; color:var(--ochre); font-size:calc(7px * var(--fs)); margin-right:5px; vertical-align:middle;}
.node-actions{flex:none; display:none; align-items:center; gap:1px; padding-right:2px;}
.admin-node-row:hover .node-actions, .admin-node-row:focus-within .node-actions{display:flex;}
.admin-node-row:hover .an-count, .admin-node-row:focus-within .an-count{display:none;}
.na-btn{appearance:none; border:0; background:none; color:var(--ink-faint); cursor:pointer; width:23px; height:23px; display:flex; align-items:center; justify-content:center; border-radius:5px; padding:0; transition:color .14s, background .14s;}
.na-btn svg{width:13px; height:13px;}
.na-btn:hover{color:var(--indigo); background:color-mix(in srgb, var(--indigo) 12%, transparent);}
.na-btn.danger:hover{color:var(--zh); background:var(--zh-wash);}
.na-btn.on{color:var(--indigo);}
.admin-newcol{width:calc(100% - 8px); margin:7px 4px 2px; appearance:none; border:1px dashed var(--rule); background:none; color:var(--ink-faint); font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; font-weight:600; padding:8px 0; border-radius:7px; cursor:pointer; transition:color .15s, border-color .15s;}
.admin-newcol:hover{color:var(--indigo); border-color:var(--indigo);}
.admin-node[draggable="true"]{cursor:grab;}
.admin-node.dragging{opacity:.4; cursor:grabbing;}
.admin-node.drop-ok{background:color-mix(in srgb, var(--good) 18%, transparent); box-shadow:inset 0 0 0 1.5px var(--good); color:var(--ink);}
.admin-node.drop-top{background:color-mix(in srgb, var(--indigo) 14%, transparent); box-shadow:inset 0 0 0 1.5px var(--indigo);}
.admin-node-all{margin:4px 4px 8px; padding:8px 9px; width:auto;}
.admin-side-foot{flex:none; display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:8px; padding:10px 14px 14px; border-top:1px solid var(--rule);}
.admin-edit-count{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.04em; color:var(--ink-faint);}
.admin-export,.admin-autosave{appearance:none; border:1px solid var(--rule); background:var(--card); color:var(--ink-soft); font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; font-weight:600; padding:7px 10px; border-radius:7px; cursor:pointer; white-space:nowrap; transition:color .15s, border-color .15s, background .15s;}
.admin-export:hover,.admin-autosave:hover{color:var(--indigo); border-color:var(--indigo);}
.admin-autosave.on{color:#fff; background:var(--indigo); border-color:var(--indigo);}
.admin-autosave.on:hover{color:#fff; opacity:.9;}
.admin-autosave.warn{color:#fff; background:var(--ochre); border-color:var(--ochre);}
.admin-autosave.warn:hover{color:#fff; border-color:var(--ochre); opacity:.9;}

/* collapse handle */
.admin-collapse{flex:none; width:15px; appearance:none; border:0; border-right:1px solid var(--rule); background:var(--paper-2); color:var(--ink-faint); cursor:pointer; position:relative; transition:background .15s, color .15s;}
.admin-collapse::before{content:"‹"; position:absolute; top:16px; left:50%; transform:translateX(-50%); font-size:calc(13px * var(--fs)); line-height:1;}
.admin-collapse.on::before{content:"›";}
.admin-collapse:hover{background:var(--rule); color:var(--ink);}

/* card / term list */
.admin-list{width:var(--list-w,332px); flex:none; display:flex; flex-direction:column; background:var(--paper); border-right:1px solid var(--rule); min-width:0; position:relative;}
.admin-search{flex:none; position:relative; padding:14px 14px 8px;}
.admin-search .as-ic{position:absolute; left:25px; top:23px; width:15px; height:15px; color:var(--ink-faint); pointer-events:none;}
.admin-search input{width:100%; box-sizing:border-box; appearance:none; border:1px solid var(--rule); background:var(--card); border-radius:9px; padding:9px 12px 9px 35px; font-family:var(--sans); font-size:calc(13.5px * var(--fs)); color:var(--ink);}
.admin-search input::placeholder{color:var(--ink-faint);}
.admin-search input:focus{outline:none; border-color:var(--indigo-bright); box-shadow:0 0 0 3px var(--indigo-wash);}
.admin-list-count{flex:none; padding:2px 18px 8px; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint);}
.admin-list-items{flex:1; overflow-y:auto; padding:0 8px 18px;}
.admin-card-row{display:block; width:100%; box-sizing:border-box; text-align:left; appearance:none; border:0; border-radius:8px; background:none; cursor:pointer; padding:9px 28px 9px 11px; margin-bottom:2px; position:relative; transition:background .12s;}
.admin-card-row:hover{background:color-mix(in srgb, var(--ink) 5%, transparent);}
.admin-card-row.active{background:var(--indigo-wash);}
.acr-id{display:block; font-family:var(--mono); font-size:calc(9px * var(--fs)); color:var(--ink-faint); letter-spacing:.05em; margin-bottom:1px; text-transform:uppercase;}
.acr-title{display:block; font-family:var(--serif); font-size:calc(14.5px * var(--fs)); color:var(--ink); line-height:1.25; font-weight:500;}
.admin-card-row.active .acr-title{color:var(--indigo);}
.acr-sub{display:block; font-size:calc(11px * var(--fs)); color:var(--ink-faint); overflow:hidden; text-overflow:ellipsis; white-space:nowrap; margin-top:1px;}
.admin-card-row.edited::after{content:""; position:absolute; right:11px; top:50%; transform:translateY(-50%); width:6px; height:6px; border-radius:50%; background:var(--ochre);}
.admin-card-row.reorderable.edited::after{right:32px;}   /* clear the right-side drag grip */
.admin-empty{padding:26px 18px; color:var(--ink-faint); font-size:calc(13px * var(--fs)); text-align:center; line-height:1.5;}
/* draggable dividers: resize the panels (side / list) + the glossary title column */
.admin-resizer{flex:none; align-self:stretch; width:8px; margin:0 -4px; cursor:col-resize; position:relative; z-index:6; touch-action:none;}
.admin-resizer::after{content:""; position:absolute; left:3.5px; top:0; bottom:0; width:1px; background:transparent; transition:background .12s;}
.admin-resizer:hover::after, .admin-resizer.dragging::after{background:var(--indigo-bright); width:2px; left:3px;}
/* the divider strip itself passes pointer events through to the rows; only the narrow grip is interactive */
.gloss-col-divider{display:none; position:absolute; top:0; bottom:0; left:0; width:14px; margin-left:-7px; z-index:7; pointer-events:none;}
.admin-list.gloss-cols .gloss-col-divider{display:block;}
.gcd-grip{position:absolute; left:4px; top:0; bottom:0; width:6px; cursor:col-resize; pointer-events:auto; touch-action:none;}
.gcd-grip::after{content:""; position:absolute; left:2.5px; top:0; bottom:0; width:1px; background:var(--rule); transition:background .12s;}
.gcd-grip:hover::after, .gcd-grip.dragging::after{background:var(--indigo-bright); width:2px;}
.admin-card-row.gloss-row{display:grid; grid-template-columns:var(--gloss-col,150px) 1fr; column-gap:12px; align-items:baseline; padding:9px 12px 9px 11px;}
.gloss-row .acr-title{white-space:nowrap; overflow:hidden; text-overflow:ellipsis; min-width:0;}
.gloss-row .acr-sub{margin-top:0;}
.acr-tags{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.01em;}   /* the glossary row's second column shows the term's tags */

/* field editor */
.admin-editor{flex:1; min-width:0; overflow-y:auto; padding:22px 28px 64px; background:var(--card);}
.admin-editor-empty{display:flex; height:100%; align-items:center; justify-content:center; color:var(--ink-faint); font-size:calc(14px * var(--fs)); text-align:center; padding:24px;}
.admin-ed-head{display:flex; align-items:flex-start; justify-content:space-between; gap:18px; margin-bottom:20px; padding-bottom:16px; border-bottom:1px solid var(--rule);}
.admin-ed-headinfo{min-width:0;}
.admin-ed-title{font-family:var(--display); font-size:calc(21px * var(--fs)); font-weight:600; color:var(--ink); margin:0; line-height:1.2;}
.admin-ed-key{font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-faint); margin-top:6px; letter-spacing:.02em; word-break:break-word;}
.admin-ed-actions{flex:none; display:flex; align-items:center; gap:11px; padding-top:3px;}
.admin-saved{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--good); opacity:0; transition:opacity .2s; white-space:nowrap;}
.admin-saved.show{opacity:1;}
.admin-revert{appearance:none; border:1px solid var(--rule); background:var(--paper); color:var(--ink-soft); font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; font-weight:600; padding:8px 12px; border-radius:7px; cursor:pointer; white-space:nowrap; transition:color .15s, border-color .15s, background .15s;}
.admin-revert[hidden]{display:none;}
.admin-revert:hover{color:var(--zh); border-color:color-mix(in srgb, var(--zh) 40%, var(--rule)); background:var(--zh-wash);}
.admin-preview{appearance:none; border:1px solid var(--indigo); background:none; color:var(--indigo); font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; font-weight:600; padding:8px 12px; border-radius:7px; cursor:pointer; white-space:nowrap; transition:color .15s, background .15s;}
.admin-preview:hover{background:var(--indigo); color:#fff;}
/* the way back to a study session the editor was opened from (see studyHold in app.js). FILLED where
   Preview is outlined: it is the primary way out of this trip, and the reader is here because a card in
   front of them was wrong. It exists only while a session is being held, so it never sits there inert. */
.admin-tostudy{appearance:none; border:1px solid var(--indigo); background:var(--indigo); color:#fff; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; font-weight:600; padding:8px 12px; border-radius:7px; cursor:pointer; white-space:nowrap; transition:filter .15s, transform .12s var(--ease);}
.admin-tostudy:hover{filter:brightness(1.12);}
.admin-tostudy:active{transform:scale(.97);}
/* in-pane card preview (keeps the tree + card list visible while previewing) */
.admin-pv{display:flex; flex-direction:column; gap:18px;}
.admin-pv-bar{display:flex; align-items:center; gap:14px; min-width:0;}
.admin-pv-where{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink-faint); min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.admin-pv-card{max-width:680px;}
.admin-pv-grades{max-width:680px;}
.night .admin-preview:hover{color:var(--paper);}
.admin-fields{display:flex; flex-direction:column; gap:15px; max-width:780px;}
.admin-field{display:flex; flex-direction:column; gap:6px;}
.af-label{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint); font-weight:600;}
.af-input{width:100%; box-sizing:border-box; appearance:none; border:1px solid var(--rule); background:var(--paper); border-radius:8px; padding:9px 12px; font-family:var(--sans); font-size:calc(13.5px * var(--fs)); color:var(--ink); line-height:1.5;}
.af-input:focus{outline:none; border-color:var(--indigo-bright); box-shadow:0 0 0 3px var(--indigo-wash); background:var(--card);}
textarea.af-input{resize:vertical; min-height:42px;}
.af-area{font-family:var(--mono); font-size:calc(12px * var(--fs)); line-height:1.65;}
/* WYSIWYG (contenteditable) rich fields — render formatting like the card */
.af-rich{font-family:var(--serif); font-size:calc(14.5px * var(--fs)); line-height:1.62; color:var(--ink); min-height:44px; max-height:460px; overflow-y:auto; cursor:text; white-space:normal; word-break:normal; overflow-wrap:anywhere;}
.af-rich:empty:before{content:"Empty — click to edit"; color:var(--ink-faint); font-style:italic;}
.af-rich-abstract{min-height:230px;}
.af-rich-question{min-height:84px;}
.af-rich-glossdesc{min-height:120px;}
.af-rich b,.af-rich strong{font-weight:700;}
.af-rich i,.af-rich em{font-style:italic;}
.af-rich .ttip{color:var(--indigo-bright); border-bottom:1px solid color-mix(in srgb,var(--indigo) 34%,transparent); cursor:text; background:none; padding:0;}
.af-rich .blank{display:inline-block; box-sizing:border-box; min-width:2.4em; text-align:center; color:var(--ink-faint); border-bottom:2px solid var(--ochre); margin:0 3px; padding:0 3px; line-height:1.2;}
.af-rich .dt{font-family:var(--mono);}
.af-rich ul,.af-rich ol{margin:.3em 0; padding-left:1.5em;}
/* WYSIWYG formatting ribbon (shared, acts on the focused rich field) */
/* The ribbon follows the text down the page — it is the one control an author needs at every scroll depth,
   and on a phone the background field alone is several screens long. Which scrollport it pins to depends on
   the surface: the desktop editor pane scrolls inside itself (top:0 is that pane's own top), while the Studio
   and the phone scroll the whole PAGE, where it has to clear the sticky top bar or it slides underneath it.
   Below the bar's z-index (50), so the bar always wins where they meet. */
.rt-ribbon{position:sticky; top:calc(var(--bar-h) + 6px); z-index:30; display:flex; flex-wrap:wrap; align-items:center; gap:2px; padding:6px 7px; margin:0 0 12px; background:var(--card); border:1px solid var(--rule); border-radius:9px; box-shadow:0 2px 9px rgba(0,0,0,.06);}
.admin-editor .rt-ribbon{top:0;}
.rt-btn{appearance:none; border:1px solid transparent; background:none; color:var(--ink); border-radius:6px; min-width:28px; height:28px; padding:0 7px; font-family:var(--sans); font-size:calc(13px * var(--fs)); line-height:1; cursor:pointer; display:inline-flex; align-items:center; justify-content:center; gap:1px;}
.rt-btn:hover{background:var(--indigo-wash); color:var(--indigo);}
.rt-btn:active{background:color-mix(in srgb,var(--indigo) 18%,transparent);}
.rt-btn b{font-weight:800;} .rt-btn i{font-style:italic;} .rt-btn u{text-decoration:underline;} .rt-btn s{text-decoration:line-through;}
.rt-btn sup,.rt-btn sub{font-size:.66em; font-weight:600;}
.rt-wide{font-size:calc(11.5px * var(--fs)); padding:0 9px; font-family:var(--serif);}
.rt-sep{width:1px; height:18px; background:var(--rule); margin:0 4px;}
.rt-color-btn{flex-direction:column; gap:0;}
.rt-colA{font-weight:800; font-size:calc(13px * var(--fs)); line-height:1;}
.rt-colBar{display:block; width:15px; height:3px; background:var(--ochre); border-radius:1px; margin-top:1px;}
.rt-ribbon:not(.on-bg) .rt-link,.rt-ribbon:not(.on-bg) .rt-link-sep{display:none;}   /* gloss-link tools only for the background field */
/* footnoting is a background-field act too — there is nothing to cite in a question or a date line */
.rt-ribbon:not(.on-bg) .rt-fn,.rt-ribbon:not(.on-bg) .rt-fn-sep{display:none;}
.rt-fn{color:var(--zh); font-weight:600;}   /* the vermilion the markers themselves are drawn in (sup.fn) */
.rt-fn:hover{background:color-mix(in srgb,var(--zh) 12%,transparent); color:var(--zh);}
.rt-colormenu{position:fixed; z-index:330; display:grid; grid-template-columns:repeat(6,1fr); gap:5px; padding:8px; background:var(--card); border:1px solid var(--rule); border-radius:9px; box-shadow:0 12px 30px rgba(0,0,0,.24);}
.rt-sw{width:22px; height:22px; border:1px solid rgba(0,0,0,.18); border-radius:5px; cursor:pointer; padding:0; transition:transform .1s;}
.rt-sw:hover{transform:scale(1.12);}
.rt-sw-clear{grid-column:1 / -1; width:100%; height:24px; background:var(--paper); color:var(--ink-faint); display:inline-flex; align-items:center; justify-content:center; font-size:calc(13px * var(--fs));}
/* per-field collapsible HTML-source view (below each rich field, Anki-style) */
.af-src-toggle{display:inline-flex; align-items:center; gap:5px; align-self:flex-start; margin-top:5px; appearance:none; border:0; background:none; color:var(--ink-faint); font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; cursor:pointer; padding:2px 0;}
.af-src-toggle:hover{color:var(--indigo);}
.afs-chev{display:inline-block; transition:transform .15s; font-size:calc(8px * var(--fs));}
.af-src-toggle.open .afs-chev{transform:rotate(90deg);}
.af-src{display:block; width:100%; box-sizing:border-box; margin-top:5px; border:1px solid var(--rule); border-radius:7px; background:var(--paper); color:var(--ink); font-family:var(--mono); font-size:calc(11.5px * var(--fs)); line-height:1.55; padding:8px 11px; min-height:84px; resize:vertical; outline:none; white-space:pre-wrap; word-break:break-word;}
.af-src[hidden]{display:none;}   /* the author display:block would otherwise defeat the hidden attribute — the source box must stay collapsed until toggled */
.af-src:focus{border-color:var(--indigo-bright); box-shadow:0 0 0 3px var(--indigo-wash);}
/* gloss-link editing bubble (click a blue gloss word in a rich field) */
.gloss-bubble{position:fixed; z-index:340; display:inline-flex; align-items:center; gap:6px; padding:5px 7px 5px 11px; background:var(--ink); color:var(--paper); border-radius:999px; box-shadow:0 8px 24px rgba(0,0,0,.34); font-family:var(--sans); font-size:calc(12px * var(--fs)); white-space:nowrap;}
.gloss-bubble::after{content:""; position:absolute; left:18px; bottom:-5px; width:10px; height:10px; background:var(--ink); transform:rotate(45deg);}
.gb-arrow{opacity:.6; font-size:calc(10px * var(--fs)); letter-spacing:.04em;}
.gb-term{appearance:none; border:0; background:none; color:var(--paper); font-family:var(--serif); font-size:calc(13px * var(--fs)); font-weight:600; cursor:pointer; text-decoration:underline; text-underline-offset:2px; padding:0;}
.gb-term:hover{color:#fff; opacity:.85;}
.gb-remove{appearance:none; border:0; background:color-mix(in srgb,var(--paper) 18%,transparent); color:var(--paper); width:20px; height:20px; border-radius:50%; cursor:pointer; font-size:calc(10px * var(--fs)); line-height:1; display:inline-flex; align-items:center; justify-content:center; position:relative; z-index:1;}
.gb-remove:hover{background:var(--bad,#c0392b); color:#fff;}
/* rich-text toolbars on the editor fields + the glossary-term picker */
.af-toolbar{display:flex; align-items:center; gap:4px; flex-wrap:wrap;}
.af-tbtn{appearance:none; border:1px solid var(--rule); background:var(--card); color:var(--ink); border-radius:6px; min-width:26px; height:24px; padding:0 6px; font-family:var(--sans); font-size:calc(12px * var(--fs)); line-height:1; cursor:pointer; display:inline-flex; align-items:center; justify-content:center;}
.af-tbtn:hover{border-color:var(--indigo); color:var(--indigo);}
.af-tbtn b{font-weight:800;} .af-tbtn i{font-style:italic;}
.af-twide{font-size:calc(11px * var(--fs)); letter-spacing:.02em;}
.af-tsep{width:1px; height:16px; background:var(--rule); margin:0 3px;}
.gloss-picker{position:fixed; z-index:320; width:244px; background:var(--card); border:1px solid var(--rule); border-radius:10px; box-shadow:0 12px 34px rgba(0,0,0,.24); padding:8px; display:flex; flex-direction:column; gap:6px;}
.gp-search{width:100%; box-sizing:border-box; appearance:none; border:1px solid var(--rule); background:var(--paper); border-radius:7px; padding:7px 9px; font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink);}
.gp-search:focus{outline:none; border-color:var(--indigo-bright); box-shadow:0 0 0 3px var(--indigo-wash);}
.gp-list{max-height:236px; overflow-y:auto; display:flex; flex-direction:column; gap:1px;}
.gp-item{appearance:none; border:0; background:none; width:100%; text-align:left; padding:7px 9px; border-radius:6px; font-family:var(--serif); font-size:calc(13.5px * var(--fs)); color:var(--ink); cursor:pointer;}
.gp-item:hover{background:var(--indigo-wash); color:var(--indigo);}
.gp-tags{margin-left:7px; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:var(--ink-faint);}   /* the term's tags, small + grey after the title */
.gp-item:hover .gp-tags{color:inherit; opacity:.7;}
.gp-empty{padding:12px 9px; color:var(--ink-faint); font-size:calc(12.5px * var(--fs)); text-align:center;}
.af-readonly{color:var(--ink-faint); background:var(--paper-2); cursor:not-allowed;}
.admin-field-note{font-size:calc(12px * var(--fs)); color:var(--ink-faint); line-height:1.5;}
/* single-surface card editor: the live card IS the editor (double-click a section to edit in place).
   The column (and the header above it) is centred within the editor pane. */
.card-edit-single{display:flex; flex-direction:column; gap:14px; max-width:780px; width:100%; margin-left:auto; margin-right:auto;}
.card-edit-single .admin-live-card{margin-left:auto; margin-right:auto; width:100%;}   /* the card caps at 680px (.admin-pv-card) inside the 780px column — auto margins keep it centred under the toolbar */
#adminEditor:has(.card-edit-single) .admin-ed-head{max-width:780px; margin-left:auto; margin-right:auto;}
.ces-top{display:flex; flex-direction:column; gap:12px;}
.ces-meta{display:flex; gap:12px; flex-wrap:wrap;}
.ces-m{display:flex; flex-direction:column; gap:4px; flex:1 1 150px; min-width:130px;}
.ces-m > span{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
.ces-m-wide{flex:2 1 280px;}
/* the difficulty picker. `.af-input` sets appearance:none, which on a <select> also removes the caret —
   so the arrow is drawn back on, or the control reads as a text box that ignores typing. */
select.af-sel{cursor:pointer; padding-right:30px; background-image:linear-gradient(45deg,transparent 50%,currentColor 50%),linear-gradient(135deg,currentColor 50%,transparent 50%); background-position:calc(100% - 16px) calc(50% + 1px),calc(100% - 11px) calc(50% + 1px); background-size:5px 5px,5px 5px; background-repeat:no-repeat;}
.ces-m:has(> select.af-sel){flex:1 1 210px; min-width:190px;}
/* the "no single date" tick (Timeline's own filter — see cardUndatable in app.js). The row is given the
   height of the inputs beside it so the meta row's controls sit on one line rather than stepping up.
   It is written `.ces-m > span.ces-chk` and undoes the caption's own type: this is a direct child span of
   `.ces-m`, so the caption rule above matches it too and outranks a bare class — which set the label of a
   thing you CLICK in the uppercase mono of the labels above it, and left it under that rule's ellipsis. */
.ces-m-check{flex:0 1 175px; min-width:160px;}
.ces-m > span.ces-chk{display:flex; align-items:center; gap:7px; min-height:calc(40px * var(--fs)); font-family:var(--sans); font-size:calc(12.5px * var(--fs)); letter-spacing:0; text-transform:none; white-space:normal; overflow:visible; color:var(--ink-soft); cursor:pointer;}
.ces-chk input{flex:none; margin:0; accent-color:var(--indigo); width:15px; height:15px; cursor:pointer;}
.ces-decks-head{appearance:none; align-self:flex-start; display:flex; align-items:center; gap:7px; background:none; border:0; padding:2px 0; cursor:pointer; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.06em; color:var(--ink-soft);}
.ces-decks-head:hover{color:var(--indigo);}
.ces-decks-head .afs-chev{transition:transform .18s var(--ease); display:inline-block;}
.ces-decks-head.open .afs-chev{transform:rotate(90deg);}
.ces-decks-body{margin-top:8px;}
.admin-live-card .ces-field{border-radius:6px; outline:1.5px dashed transparent; outline-offset:5px; transition:outline-color .15s; min-height:1.3em; cursor:default;}
.admin-live-card .ces-field:hover{outline-color:color-mix(in srgb, var(--indigo) 40%, transparent);}
.admin-live-card .ces-field.editing{outline:1.5px solid var(--indigo); background:color-mix(in srgb, var(--indigo) 4%, transparent); cursor:text;}
.admin-live-card .ces-field.is-empty::before{content:attr(data-ph); color:var(--ink-faint); font-style:italic; font-size:calc(14px * var(--fs)); font-family:var(--sans); letter-spacing:0;}
/* question phrasings: the chevrons flanking the question box cycle a card's alternate questions
   (the study page asks one at random); the counter sits in the Question label */
.ces-qpos{font-family:var(--sans); font-size:calc(10.5px * var(--fs)); font-weight:600; color:var(--indigo); letter-spacing:.04em; margin-left:6px;}
.ces-qpos[hidden]{display:none;}
.ces-qrow{display:flex; align-items:stretch; gap:10px;}
.ces-qcol{flex:1; min-width:0;}
.ces-qchev{
  flex:0 0 auto; align-self:center; width:26px; min-height:44px; display:flex; align-items:center; justify-content:center;
  border:1px solid var(--rule); border-radius:8px; background:var(--paper-2); color:var(--ink-faint); cursor:pointer; padding:0;
  transition:border-color .15s, color .15s;
}
.ces-qchev svg{width:15px; height:15px;}
.ces-qchev:hover:not(:disabled){border-color:color-mix(in srgb, var(--indigo) 50%, var(--rule)); color:var(--indigo);}
.ces-qchev:disabled{opacity:.35; cursor:default;}
.ces-qtools{display:flex; gap:10px; margin:8px 0 2px;}
.ces-qtool{
  padding:4px 11px; border:1px dashed var(--rule); border-radius:999px; background:none; color:var(--ink-faint);
  font-family:var(--sans); font-size:calc(11.5px * var(--fs)); cursor:pointer; transition:border-color .15s, color .15s;
}
.ces-qtool:hover{border-color:color-mix(in srgb, var(--indigo) 50%, var(--rule)); color:var(--indigo);}
.ces-qtool-del:hover{border-color:color-mix(in srgb, var(--zh) 50%, var(--rule)); color:var(--zh);}
.ces-qtool[hidden]{display:none;}
.ces-img{cursor:pointer;}
.ces-img-edit{position:absolute; right:10px; top:10px; font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:#fff; background:rgba(0,0,0,.55); border-radius:7px; padding:5px 9px; opacity:0; transition:opacity .18s; pointer-events:none;}
.ces-img:hover .ces-img-edit{opacity:1;}
.ces-img-ph{display:flex; flex-direction:column; align-items:center; justify-content:center; gap:8px; aspect-ratio:16/9; border:1.5px dashed var(--rule); border-radius:10px; background:var(--paper-2); color:var(--ink-faint); cursor:pointer; margin:2px 0 18px; text-align:center; padding:12px; transition:border-color .15s, color .15s;}
.ces-img-ph:hover{border-color:color-mix(in srgb, var(--indigo) 50%, var(--rule)); color:var(--indigo);}
.ces-img-ph svg{width:30px; height:30px;}
.ces-img-ph span{font-family:var(--sans); font-size:calc(13px * var(--fs));}
.ces-img-ph small{display:block; font-size:calc(11px * var(--fs)); color:var(--ink-faint); margin-top:2px;}
/* the video slot in the editor: the real player, with an edit chip where the image's zoom hint sits */
.ces-vid .ces-vid-edit{opacity:.72; pointer-events:auto; border:0; padding:0; cursor:pointer;}
.ces-vid:hover .ces-vid-edit{opacity:1;}
.ces-vid-ph svg{width:30px; height:30px;}
.af-vidnote{font-family:var(--sans); font-size:calc(11.5px * var(--fs)); color:var(--ink-faint); margin:-4px 0 0;}
.af-vidnote.bad{color:var(--zh);}
.af-vidnote[hidden]{display:none;}
.ces-imgpanel{display:flex; flex-direction:column; gap:10px; border:1px dashed color-mix(in srgb, var(--indigo) 35%, var(--rule)); border-radius:10px; padding:12px 14px; margin:-6px 0 16px; background:var(--paper-2);}
/* required, codebase convention: the author display:flex above beats the UA [hidden] rule, so without this
   the card editor's image/video panels are permanently open and the click-to-edit toggle does nothing.
   (The glossary editors' copies carry no hidden attribute and stay open, which is what they want.) */
.ces-imgpanel[hidden]{display:none;}
#cesMediaMeta{display:flex; flex-direction:column; gap:10px;}
#cesMediaMeta[hidden]{display:none;}
/* ---- the sources panel: citations edited AS THEY READ ----
   A Chicago note is mostly italicised title, and editing it as <i>…</i> in a textarea meant proof-reading
   tags. Each citation is its own rich line here, numbered by the <ol> exactly as the card numbers them, so
   what an author sees while writing is what a reader sees at the foot of the card. The URL and the
   [Open access] / [Paywalled] label stay as PLAIN TEXT: the card turns the one into a link and the other
   into a chip at render time, and showing them already converted would leave nothing left to edit. */
.ces-srclist{margin:0; padding:0 0 0 1.9em; list-style:decimal;}
.ces-srclist > li{margin-bottom:7px;}
.ces-srclist > li:last-child{margin-bottom:0;}
/* the flex row sits INSIDE the <li> — an <li> whose own display is flex is no longer a list-item and
   loses its ::marker, and the number is the point (it is the one the card's footnotes point at) */
.ces-srclist > li::marker{font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-faint);}
.ces-srcrow{display:flex; align-items:flex-start; gap:7px;}
.ces-srcitem{
  flex:1; min-width:0; font-family:var(--serif); font-size:calc(12.5px * var(--fs)); line-height:1.5; color:var(--ink-soft);
  background:var(--card); border:1px solid var(--rule); border-radius:8px; padding:7px 10px;
  outline:none; transition:border-color .15s, background .15s; word-break:break-word;
}
.ces-srcitem:focus{border-color:var(--indigo); background:color-mix(in srgb, var(--indigo) 4%, transparent); color:var(--ink);}
.ces-srcitem:empty::before{content:attr(data-ph); color:var(--ink-faint); font-style:italic;}
.ces-srcdel{
  flex:none; width:26px; height:26px; margin-top:3px; display:flex; align-items:center; justify-content:center;
  appearance:none; border:1px solid transparent; border-radius:7px; background:none; color:var(--ink-faint);
  font-size:calc(12px * var(--fs)); cursor:pointer; transition:color .15s, border-color .15s;
}
.ces-srcdel:hover{color:var(--zh); border-color:color-mix(in srgb, var(--zh) 40%, var(--rule));}
.ces-srcadd{
  align-self:flex-start; padding:4px 11px; border:1px dashed var(--rule); border-radius:999px; background:none;
  color:var(--ink-faint); font-family:var(--sans); font-size:calc(11.5px * var(--fs)); cursor:pointer; transition:border-color .15s, color .15s;
}
.ces-srcadd:hover{border-color:color-mix(in srgb, var(--indigo) 50%, var(--rule)); color:var(--indigo);}
/* the same panel in the glossary editors (curated + Studio), where it is always open rather than toggled */
.gloss-imgpanel{margin:14px 0 0;}
.gloss-imgpanel .admin-field{margin-top:0;}
.gloss-imgpanel #adminGlossImgMeta, .gloss-imgpanel #stImgMeta{display:flex; flex-direction:column; gap:10px;}
.gloss-imgpanel #adminGlossImgMeta[hidden], .gloss-imgpanel #stImgMeta[hidden]{display:none;}
/* the source gate: a URL typed without a credit line is staged, not saved, and says so where it is typed */
.af-reqnote{display:flex; align-items:center; gap:10px; flex-wrap:wrap; font-family:var(--sans); font-size:calc(11.5px * var(--fs)); line-height:1.45;
  color:var(--zh); background:color-mix(in srgb, var(--zh) 9%, transparent); border:1px solid color-mix(in srgb, var(--zh) 34%, transparent);
  border-radius:8px; padding:7px 10px; margin:8px 0 0;}
.af-reqnote[hidden]{display:none;}
.afr-btn{font-family:var(--sans); font-size:calc(11px * var(--fs)); font-weight:600; letter-spacing:.02em; color:var(--paper); background:var(--zh);
  border:0; border-radius:6px; padding:4px 10px; cursor:pointer;}
.afr-btn:hover{filter:brightness(1.1);}
/* …and the frame it belongs to is shown, so the author sees what they pasted, but flagged as unkept */
.ces-media-pending{outline:2px dashed color-mix(in srgb, var(--zh) 55%, transparent); outline-offset:2px;}
.ces-media-pending > img, .ces-media-pending > .cv-media{opacity:.55;}
.ces-media-flag{position:absolute; left:8px; bottom:8px; z-index:3; font-family:var(--sans); font-size:calc(10.5px * var(--fs)); font-weight:600;
  letter-spacing:.02em; color:var(--paper); background:var(--zh); border-radius:6px; padding:3px 8px; pointer-events:none;}
.ces-src-ta{width:100%; min-height:240px; resize:vertical; font-family:var(--mono); font-size:calc(12px * var(--fs)); line-height:1.55;}
/* admin tree: same-parent drop = reorder (insert-before indicator) */
.admin-node.drop-before{box-shadow:inset 0 3px 0 -1px var(--indigo-bright);}
/* card editor: language tabs (EN = base card, the rest edit the i18n translations) */
.admin-langbar{display:flex; align-items:center; gap:5px; flex-wrap:wrap; margin:2px 0 10px;}
.al-lab{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; color:var(--ink-faint); margin-right:4px;}
.al-btn{appearance:none; cursor:pointer; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); font-weight:600; letter-spacing:.05em; color:var(--ink-soft); background:var(--paper-2); border:1px solid var(--rule); border-radius:7px; padding:5px 9px; transition:border-color .15s, background .15s, color .15s;}
.al-btn:hover{border-color:var(--ink-faint);}
.al-btn.active{background:var(--indigo); border-color:var(--indigo); color:#fff;}
.al-btn.missing:not(.active){color:var(--ink-faint); border-style:dashed;}

/* list tools: count + new-card + sort */
.admin-list-tools{flex:none; display:flex; align-items:center; justify-content:space-between; gap:10px; padding:2px 14px 8px;}
.admin-list-tools .admin-list-count{padding:0;}
.alt-right{display:flex; align-items:center; gap:8px;}
.admin-new{appearance:none; border:1px solid var(--indigo); background:var(--indigo); color:#fff; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; font-weight:600; padding:6px 11px; border-radius:7px; cursor:pointer; white-space:nowrap; transition:opacity .15s;}
.night .admin-new{color:var(--paper);}
.admin-new:hover{opacity:.85;}
.admin-sort{display:flex; align-items:center; gap:6px; font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink-faint);}
.admin-sort select{appearance:none; border:1px solid var(--rule); background:var(--card); color:var(--ink-soft); font-family:var(--sans); font-size:calc(12px * var(--fs)); padding:5px 8px; border-radius:7px; cursor:pointer;}
.admin-sort select:focus{outline:none; border-color:var(--indigo-bright); box-shadow:0 0 0 3px var(--indigo-wash);}

/* multi-select rows */
.admin-card-row{display:flex; align-items:stretch; gap:0; padding:0;}
.admin-card-row.selected{background:color-mix(in srgb, var(--indigo) 8%, transparent);}
.acr-check{flex:none; display:flex; align-items:center; padding:0 5px 0 10px; cursor:pointer;}
.acr-check input{position:absolute; opacity:0; width:0; height:0;}
.acr-box{width:15px; height:15px; border:1.5px solid var(--rule); border-radius:4px; display:block; position:relative; transition:background .12s, border-color .12s;}
.acr-check:hover .acr-box{border-color:var(--indigo);}
.acr-check input:checked + .acr-box{background:var(--indigo); border-color:var(--indigo);}
.acr-check input:checked + .acr-box::after{content:""; position:absolute; left:4.5px; top:1.5px; width:4px; height:8px; border:solid #fff; border-width:0 2px 2px 0; transform:rotate(45deg);}
.acr-open{flex:1; min-width:0; text-align:left; appearance:none; border:0; background:none; cursor:pointer; padding:9px 28px 9px 7px; border-radius:8px;}
/* drag-to-reorder cards (study order) */
.acr-grip{flex:none; display:flex; align-items:center; justify-content:center; width:16px; margin-left:2px; color:var(--ink-faint); cursor:grab; opacity:.4; font-size:calc(13px * var(--fs)); line-height:1; user-select:none; transition:opacity .12s;}
.acr-grip:hover{opacity:.85;}
.acr-grip:active{cursor:grabbing;}
.acr-grip-r{margin-left:auto; margin-right:7px; width:18px; letter-spacing:-1px; color:var(--indigo); opacity:.5;}   /* drag notch on the right (deck card-order / home "Chrono")*/
.acr-grip-r:hover{opacity:.95;}
.admin-card-row.dragging{opacity:.4;}
.admin-card-row.drop-before{box-shadow:inset 0 2px 0 0 var(--indigo);}
.admin-card-row.drop-after{box-shadow:inset 0 -2px 0 0 var(--indigo);}
/* source coverage chip on the card row's id line — how many citations the card carries of the bar it is
   held to. Amber = under the bar and not yet researched; RED = researched and could not be brought to it.
   Deliberately NOT the same channel as the admin colour mark (a left stripe), which the editor sets by
   hand for whatever they like: one is derived from the data, the other is somebody's private marker. */
.acr-src{display:inline-block; margin-left:6px; padding:0 4px; border-radius:4px; font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.03em; vertical-align:baseline;}
.acr-src.short{background:color-mix(in srgb, #e0982b 22%, transparent); color:#96631a;}
.acr-src.blocked{background:color-mix(in srgb, #d8453c 22%, transparent); color:#a52a22;}
/* on a glossary row the chip LEADS the tags column rather than trailing it: that column truncates with an
   ellipsis, so a chip at its end would be the first thing lost on a term with many tags */
.gloss-row .acr-src{margin-left:0; margin-right:6px;}
body.night .acr-src.short{color:#e0a94e;}
body.night .acr-src.blocked{color:#e8776e;}
.alc-src{color:var(--ink-faint); cursor:help;}
/* admin-only colour mark (left stripe) + right-click colour menu */
.admin-card-row.colored::before{content:""; position:absolute; left:0; top:4px; bottom:4px; width:3px; border-radius:0 2px 2px 0; background:var(--acr-col);}
.acr-colortag{position:absolute; left:0; top:0; bottom:0; width:7px; z-index:3; cursor:help;}   /* transparent hover target over the colour bar — its title shows the count */
.admin-colormenu{position:fixed; z-index:300; display:flex; gap:5px; align-items:center; padding:6px 8px; background:var(--card); border:1px solid var(--rule); border-radius:9px; box-shadow:0 10px 30px rgba(0,0,0,.24);}
.acm-sw{width:18px; height:18px; border-radius:50%; border:1px solid rgba(0,0,0,.18); cursor:pointer; padding:0; appearance:none; transition:transform .1s;}
.acm-sw:hover{transform:scale(1.18);}
.acm-sw.on{box-shadow:0 0 0 2px var(--card), 0 0 0 3.5px var(--ink);}
.acm-clear{width:20px; height:20px; border-radius:50%; border:1px solid var(--rule); background:var(--paper-2); color:var(--ink-faint); cursor:pointer; font-size:calc(11px * var(--fs)); line-height:1; padding:0; appearance:none; margin-left:2px;}
.acm-clear:hover{color:var(--ink); border-color:var(--ink-soft);}

/* selection action bar */
.admin-sel-bar{display:none; flex:none;}
.admin-sel-bar.show{display:flex; align-items:center; justify-content:space-between; gap:10px; flex-wrap:wrap; margin:0 14px 8px; padding:9px 12px; border:1px solid var(--indigo); background:var(--indigo-wash); border-radius:9px;}
.asb-count{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--indigo); font-weight:700;}
.asb-actions{display:flex; align-items:center; gap:7px; flex-wrap:wrap;}
.asb-select{appearance:none; border:1px solid var(--rule); background:var(--card); color:var(--ink); font-family:var(--sans); font-size:calc(12px * var(--fs)); padding:5px 8px; border-radius:7px; cursor:pointer; max-width:190px;}
.asb-btn{appearance:none; border:1px solid var(--rule); background:var(--card); color:var(--ink-soft); font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; font-weight:600; padding:6px 10px; border-radius:7px; cursor:pointer; transition:color .15s, border-color .15s, background .15s;}
.asb-btn:hover{color:var(--indigo); border-color:var(--indigo);}
.asb-btn.danger:hover{color:var(--zh); border-color:color-mix(in srgb,var(--zh) 40%,var(--rule)); background:var(--zh-wash);}
.asb-btn.ghost{border-color:transparent;}
.asb-btn.ghost:hover{color:var(--ink); border-color:var(--rule); background:none;}

/* deck-membership picker (above the editor fields) */
.deck-pick{border:1px solid var(--rule); border-radius:10px; background:var(--paper); margin-bottom:20px; overflow:hidden; max-width:780px;}
.deck-pick-head{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint); font-weight:600; padding:10px 12px; border-bottom:1px solid var(--rule); background:var(--card);}
.deck-pick-body{max-height:210px; overflow-y:auto; padding:9px 10px; display:flex; flex-direction:column; gap:11px;}
.dpg-title{font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint); margin:0 0 4px 3px;}
.deck-pick-item{display:flex; align-items:center; gap:9px; padding:5px 8px; border-radius:7px; cursor:pointer; transition:background .12s;}
.deck-pick-item:hover{background:color-mix(in srgb,var(--ink) 4%,transparent);}
.deck-pick-item.on{background:var(--indigo-wash);}
.deck-pick-item input{position:absolute; opacity:0; width:0; height:0;}
.dpi-box{flex:none; width:15px; height:15px; border:1.5px solid var(--rule); border-radius:4px; position:relative; transition:background .12s, border-color .12s;}
.deck-pick-item input:checked + .dpi-box{background:var(--indigo); border-color:var(--indigo);}
.deck-pick-item input:checked + .dpi-box::after{content:""; position:absolute; left:4.5px; top:1.5px; width:4px; height:8px; border:solid #fff; border-width:0 2px 2px 0; transform:rotate(45deg);}
/* ONE DECK PER CARD (Aug 2026): the picker is radios now, so its mark is round with a dot — the shape is
   what tells a reader the choice is exclusive before they click anything. */
.deck-pick-item input[type="radio"] + .dpi-box{border-radius:50%;}
.deck-pick-item input[type="radio"]:checked + .dpi-box::after{left:3.5px; top:3.5px; width:6px; height:6px; border:0; border-radius:50%; background:#fff; transform:none;}
.deck-pick-note{font-family:var(--sans); font-size:calc(11.5px * var(--fs)); line-height:1.45; color:var(--ochre-ink,var(--ink-soft)); background:color-mix(in srgb,var(--ochre) 12%,transparent); border:1px solid color-mix(in srgb,var(--ochre) 30%,transparent); border-radius:8px; padding:7px 10px; margin:0 0 8px;}
.dpi-name{font-family:var(--serif); font-size:calc(13.5px * var(--fs)); color:var(--ink); line-height:1.2;}
.dpi-path{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); color:var(--ink-faint); margin-left:auto; white-space:nowrap;}

/* delete card button */
.admin-delete{appearance:none; border:1px solid color-mix(in srgb,var(--zh) 35%,var(--rule)); background:var(--paper); color:var(--zh); font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; font-weight:600; padding:8px 12px; border-radius:7px; cursor:pointer; white-space:nowrap; transition:color .15s, border-color .15s, background .15s;}
.admin-delete:hover{background:var(--zh); color:#fff; border-color:var(--zh);}
.night .admin-delete:hover{color:var(--paper);}

/* edit FAB on study cards (bottom-left) */
.admin-edit-fab{position:fixed; left:24px; bottom:24px; z-index:45; display:inline-flex; align-items:center; gap:7px; appearance:none; border:1px solid var(--rule); background:var(--card); color:var(--ink-soft); font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; font-weight:600; padding:10px 14px; border-radius:11px; cursor:pointer; box-shadow:var(--shadow-md); transition:color .15s, border-color .15s, transform .15s;}
.admin-edit-fab svg{width:15px; height:15px;}
.admin-edit-fab:hover{color:var(--indigo); border-color:var(--indigo); transform:translateY(-1px);}
body.grading .admin-edit-fab{bottom:108px;}
/* The home page's copy exists only where the tab bar replaced the top bar — above that breakpoint the top
   bar's Edit tab is still there and a second way in beside it is clutter. The study card's copy (which
   opens THAT card) shows at every width, as it always has. */
.aef-plain{display:none;}
/* On a phone Edit moves to the TOP-RIGHT (Aug 2026, on request): it left the tab bar — six destinations a
   reader shares, and this is one person's tool — and became a button on the page. The top bar is hidden at
   this width, so the corner is free, and the inset keeps it clear of a notch.
   This block must stay BELOW the base rules above: media queries add no specificity, so the phone offset
   that used to live up in the tab-bar block was silently overridden by `.admin-edit-fab{bottom:24px}` and
   never applied at all. */
@media (max-width:640px){
  .aef-plain{display:inline-flex;}
  .admin-edit-fab{left:auto; bottom:auto; right:12px; top:calc(10px + env(safe-area-inset-top));}
  body.grading .admin-edit-fab{bottom:auto;}   /* the grade bar owns the other end of the screen; this one no longer moves for it */
}

@media (min-width:641px) and (max-width:860px){ body.grading .admin-edit-fab{bottom:170px;} }
@media (max-width: 860px){
  body.admin-mode{overflow:auto;}
  .admin{flex-direction:column; height:auto; min-height:calc(100vh - var(--bar-h));}
  .admin-side{width:auto; border-right:0; border-bottom:1px solid var(--rule);}
  .admin-side.collapsed{width:auto; height:0; border-bottom:0;}
  .admin-tree{max-height:230px;}
  .admin-collapse{display:none;}
  .admin-list{width:auto; border-right:0; border-bottom:1px solid var(--rule);}
  .admin-list-items{max-height:300px;}
  /* Every tab that takes over the whole admin area needs the panel cap above lifted, or its entire page is
     trapped in a 300px scroll box with the rest of the screen left empty below it — which is exactly what
     the Quotes tab looked like on a phone (Aug 2026, on a bug report): the edit form filled the box, its
     Save button sat at the fold, and the running order under it was cut off mid-row. Timeline and Feedback
     were listed here from the day they were built; the Dashboard and Quotes arrived later and were not,
     which is the whole of the bug. SIX tabs take the area over — keep this list in step with the
     `*-mode` classes adminRefresh() sets, or the next one added will look broken the same way. */
  .admin.timeline-mode .admin-list-items,.admin.feedback-mode .admin-list-items,
  .admin.dash-mode .admin-list-items,.admin.quotes-mode .admin-list-items,
  .admin.artefacts-mode .admin-list-items,.admin.themes-mode .admin-list-items{max-height:none;}
  /* ---- Admin → Artefacts on a phone (Sep 2026, on a bug report: "on mobile, the artefact list on the
     admin page isn't formatted very well"). The row is a flex line of a rarity bar, the name and FIVE
     meta pills — the rarity, whether it has a picture, its word and sentence count, its citation count
     and its edited state — and at 390px those five wrap onto three lines while squeezing the name, which
     is the one thing on the row with no shorter form, into two or three characters. So the row wraps
     properly instead: the bar and the name take the first line whole, and the pills take the next,
     RANGED LEFT rather than right, since right-aligned pills under a left-aligned name read as a second
     column that is not there. The swatch shortens with the line it is now marking. */
  .a-main{flex-wrap:wrap; row-gap:8px; padding:10px; gap:10px;}
  .a-swatch{height:26px;}
  .a-name{flex:1 1 140px;}
  .a-meta{flex:1 1 100%; justify-content:flex-start; gap:5px;}
  .tl-edit{padding:18px 14px;}
  .tl-era{flex-wrap:wrap; row-gap:8px;}
  /* the pane stops scrolling inside itself here (the admin stacks into one column and the PAGE scrolls), and
     a scroll container that never scrolls is a scrollport its sticky ribbon can never leave — so hand the
     scrolling back to the page and pin the ribbon under the top bar instead */
  .admin-editor{padding:18px 16px 64px; overflow:visible;}
  .admin-editor .rt-ribbon{top:calc(var(--bar-h) + 6px);}
}

/* ============================================================
   SUSPENDED (set-aside) CARDS — collapsible window on the account page
   ============================================================ */
.suspbox{border:1px solid var(--rule); border-radius:12px; background:var(--card); overflow:hidden;}
.suspbox-head{width:100%; display:flex; align-items:center; justify-content:space-between; gap:12px; appearance:none; border:0; background:none; cursor:pointer; padding:15px 18px; text-align:left; color:var(--ink); font-family:var(--sans);}
.suspbox-head:hover{background:color-mix(in srgb,var(--ink) 3%,transparent);}
.suspbox-title{font-family:var(--display); font-size:calc(16px * var(--fs)); font-weight:600;}
.suspbox-count{font-family:var(--mono); font-size:calc(12px * var(--fs)); color:var(--ink-faint); font-weight:400; margin-left:4px;}
.suspbox-chev{display:flex; color:var(--ink-faint);}
.suspbox-chev svg{width:17px; height:17px; transition:transform .3s var(--ease);}
.suspbox-head.open .suspbox-chev svg{transform:rotate(180deg);}
.suspbox-collapse{display:grid; grid-template-rows:1fr; transition:grid-template-rows .35s var(--ease); border-top:1px solid var(--rule);}
.suspbox-collapse.collapsed{grid-template-rows:0fr; border-top-color:transparent;}
.suspbox-collapse-inner{min-height:0; min-width:0; overflow:hidden;}   /* see .reveal-inner */
.susplist{padding:6px 10px 10px;}
.susp-empty{padding:16px 12px; color:var(--ink-faint); font-size:calc(13.5px * var(--fs)); line-height:1.5;}
.susp-row{display:flex; align-items:center; justify-content:space-between; gap:14px; padding:11px 12px; border-radius:9px; transition:background .12s;}
.susp-row:hover{background:color-mix(in srgb,var(--ink) 4%,transparent);}
.susp-row + .susp-row{border-top:1px solid var(--rule);}
.susp-info{min-width:0; display:flex; flex-direction:column; gap:3px;}
.susp-name{font-family:var(--serif); font-size:calc(15.5px * var(--fs)); color:var(--ink); line-height:1.25; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.susp-meta{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:var(--ink-faint); letter-spacing:.02em;}
.susp-restore{flex:none; appearance:none; border:1px solid var(--rule); background:var(--paper); color:var(--ink-soft); font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; font-weight:600; padding:7px 12px; border-radius:7px; cursor:pointer; transition:color .15s, border-color .15s, background .15s;}
.susp-restore:hover{color:var(--good); border-color:color-mix(in srgb,var(--good) 45%,var(--rule)); background:color-mix(in srgb,var(--good) 10%,transparent);}

/* ---- SYNTH — neon synthwave, magenta + cyan + purple ---- */
body[data-theme="synth"]{
  --paper:#F2EEFB; --paper-2:#E6DEF7; --card:#FFFFFF;
  --ink:#1A1230; --ink-soft:#3E3060; --ink-faint:#8576B0; --rule:#DFD3F2;
  --indigo:#7C2DFF; --indigo-bright:#9A5BFF; --indigo-wash:#EAE0FF; --ochre:#FF3DAE;
  --zh:#FF2D7A; --zh-wash:#FFE0EE; --geo:#00C8DC; --good:#14C8A0;
  --serif:"Sora", ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
  --display:"Sora", ui-sans-serif, sans-serif;
  --mono:"JetBrains Mono", ui-monospace, Menlo, Consolas, monospace;
  --sans:"Sora", ui-sans-serif, -apple-system, "Segoe UI", Roboto, sans-serif;
}
body[data-theme="synth"].night{
  --paper:#0B0716; --paper-2:#130C24; --card:#1A1132;
  --ink:#ECE4FF; --ink-soft:#C2B0F0; --ink-faint:#8A78C0; --rule:#2E2050;
  --indigo:#B07BFF; --indigo-bright:#CDA8FF; --indigo-wash:#241646; --ochre:#FF6FC8;
  --zh:#FF5C9A; --zh-wash:#2E1428; --geo:#3DE0F0; --good:#3DE0B8;
}

/* —— SYNTH · synthwave neon: glow, grid, gradient nameplate —— */
body[data-theme="synth"]::before{
  background:
    linear-gradient(color-mix(in srgb,var(--indigo) 16%, transparent) 1px, transparent 1px) 0 0/36px 36px,
    linear-gradient(90deg, color-mix(in srgb,var(--zh) 13%, transparent) 1px, transparent 1px) 0 0/36px 36px,
    radial-gradient(70% 55% at 50% 120%, color-mix(in srgb,var(--ochre) 34%, transparent), transparent 66%),
    radial-gradient(50% 50% at 6% 4%, color-mix(in srgb,var(--geo) 24%, transparent), transparent 70%);
  filter:none; opacity:.6;
}
body[data-theme="synth"] .topbar{ backdrop-filter:none; -webkit-backdrop-filter:none; background:color-mix(in srgb,var(--card) 88%, transparent); border-bottom:1px solid color-mix(in srgb,var(--indigo) 55%, var(--rule)); box-shadow:0 3px 30px color-mix(in srgb,var(--ochre) 26%, transparent); }
body[data-theme="synth"] .brand .mark{ font-family:"Orbitron",var(--display); font-weight:700; font-size:calc(18px * var(--fs)); letter-spacing:.04em; background:linear-gradient(90deg,var(--ochre),var(--geo)); -webkit-background-clip:text; background-clip:text; color:transparent; -webkit-text-fill-color:transparent; }
body[data-theme="synth"] .brand .dot{ color:var(--geo); -webkit-text-fill-color:var(--geo); }
body[data-theme="synth"] .page-head h1{ font-family:"Orbitron",var(--display); font-weight:700; letter-spacing:.01em; font-size:calc(clamp(23px,4.4vw,33px) * var(--fs)); text-shadow:0 0 26px color-mix(in srgb,var(--indigo) 55%, transparent); }
body[data-theme="synth"] .collection, body[data-theme="synth"] .study-card, body[data-theme="synth"] .card,
body[data-theme="synth"] .placard{ border-radius:5px; border:1px solid color-mix(in srgb,var(--indigo) 42%, var(--rule)); box-shadow:0 0 0 1px color-mix(in srgb,var(--indigo) 12%, transparent), 0 10px 34px color-mix(in srgb,var(--indigo) 24%, transparent); }
body[data-theme="synth"] .btn, body[data-theme="synth"] .grade{ border-radius:4px; box-shadow:0 0 22px color-mix(in srgb,var(--indigo) 48%, transparent); }
body[data-theme="synth"] .btn.zh{ box-shadow:0 0 22px color-mix(in srgb,var(--zh) 52%, transparent); }
body[data-theme="synth"] .tab{ border-radius:4px; }
body[data-theme="synth"] a{ text-shadow:0 0 12px color-mix(in srgb,var(--indigo-bright) 45%, transparent); }

/* —— SYNTH · neon panels: glow frame, glowing glyph + title —— */
body[data-theme="synth"] .game-tile{ border-radius:5px; border:1px solid color-mix(in srgb,var(--gt-accent) 48%, var(--rule)); border-left:3px solid var(--gt-accent); box-shadow:0 0 22px color-mix(in srgb,var(--gt-accent) 24%, transparent), inset 0 0 20px color-mix(in srgb,var(--gt-accent) 8%, transparent); }
body[data-theme="synth"] button.game-tile:hover{ box-shadow:0 0 34px color-mix(in srgb,var(--gt-accent) 40%, transparent); }
body[data-theme="synth"] .gt-glyph{ opacity:.22; text-shadow:0 0 18px color-mix(in srgb,var(--tile, var(--indigo)) 55%, transparent); }
body[data-theme="synth"] .gt-title{ text-shadow:0 0 16px color-mix(in srgb,var(--gt-accent) 38%, transparent); }
body[data-theme="synth"] .banner{ border-radius:5px; border:1px solid color-mix(in srgb,var(--indigo) 42%, var(--rule)); box-shadow:0 0 0 1px color-mix(in srgb,var(--indigo) 12%, transparent), 0 10px 34px color-mix(in srgb,var(--indigo) 26%, transparent); }
body[data-theme="synth"] .banner h2.review-title{ text-shadow:0 0 24px color-mix(in srgb,var(--indigo) 48%, transparent); }
body[data-theme="synth"] .banner .glyph{ text-shadow:0 0 30px color-mix(in srgb,var(--indigo) 45%, transparent); }

/* ============================================================
   THEME BANNER ART — a unique, invented, animated treatment for
   each non-default theme's library banner (.collection-deco), the
   home review banner (.banner) and the home tiles (.game-tile).
   The accent hue comes from --coll-bg / --tile / theme tokens; the
   *look* is the theme's own. Default (folio) is left untouched.
   ============================================================ */

/* plumbing: art layer behind, content above (home banner + tiles) */
body:not([data-theme="folio"]) .banner::before,
body:not([data-theme="folio"]) .game-tile::before{
  content:""; position:absolute; inset:0; z-index:0; pointer-events:none; opacity:.5;
  -webkit-mask:linear-gradient(to left, #000 50%, transparent 94%); mask:linear-gradient(to left, #000 50%, transparent 94%);
}
body:not([data-theme="folio"]) .banner > .body,
body:not([data-theme="folio"]) .banner .cta{ position:relative; z-index:1; }
/* the `>` used to be on the tile itself; `.gt-face` came between them when the tile learned to turn
   over, and the unwrapped `blankTile` still puts them directly on the tile — so BOTH are matched, or a
   spare slot would silently lose its stacking order the day a seventh game leaves one empty again */
body:not([data-theme="folio"]) .game-tile > .gt-body,
body:not([data-theme="folio"]) .game-tile .gt-face > .gt-body{ position:relative; z-index:2; }
body:not([data-theme="folio"]) .game-tile > .gt-glyph,
body:not([data-theme="folio"]) .game-tile .gt-face > .gt-glyph{ z-index:1; }

/* ===== SYNTH — synthwave: a neon perspective grid racing to a glowing horizon ===== */
body[data-theme="synth"] .collection-deco{
  background:linear-gradient(180deg, color-mix(in srgb,var(--coll-bg) 8%, var(--card)), color-mix(in srgb,var(--coll-bg) 24%, var(--card)));
}
body[data-theme="synth"] .collection-deco::before{
  content:""; position:absolute; inset:auto; left:-25%; right:-25%; bottom:-45%; height:150%; background-image:none; opacity:.8;
  background:
    repeating-linear-gradient(0deg, color-mix(in srgb,var(--coll-bg) 75%, transparent) 0 1.5px, transparent 1.5px 28px),
    repeating-linear-gradient(90deg, color-mix(in srgb,var(--coll-bg) 75%, transparent) 0 1.5px, transparent 1.5px 28px);
  transform:perspective(150px) rotateX(64deg); transform-origin:bottom;
  -webkit-mask:linear-gradient(to top, #000 25%, transparent 92%); mask:linear-gradient(to top, #000 25%, transparent 92%);
}
body[data-theme="synth"] .collection-deco::after{
  content:""; position:absolute; left:0; right:0; bottom:40%; height:2px; opacity:.85;
  background:color-mix(in srgb,var(--coll-bg) 92%, white);
  box-shadow:0 0 18px 3px color-mix(in srgb,var(--coll-bg) 70%, transparent);
}
body[data-theme="synth"] .banner::before{
  opacity:.6; -webkit-mask:none; mask:none;
  background:
    repeating-linear-gradient(0deg, color-mix(in srgb,var(--indigo) 30%, transparent) 0 1.5px, transparent 1.5px 30px),
    repeating-linear-gradient(90deg, color-mix(in srgb,var(--indigo) 22%, transparent) 0 1.5px, transparent 1.5px 30px);
}
body[data-theme="synth"] .game-tile::before{
  opacity:.45;
  background:repeating-linear-gradient(0deg, color-mix(in srgb,var(--tile) 30%, transparent) 0 1.5px, transparent 1.5px 22px),
            repeating-linear-gradient(90deg, color-mix(in srgb,var(--tile) 22%, transparent) 0 1.5px, transparent 1.5px 22px);
}

/* (the banner-art pattern layers are STATIC by design — the old drift/sweep keyframes were removed on request) */


/* ============================================================
   SEVEN MORE THEMES — arcade, academy, scroll, marble, dynasty,
   grove, gazette. Authored as full overhauls: tokens (hex-only,
   light + night), ambient field, chrome, surfaces, ornament.
   Collection banners stay STATIC (no animated patterns).
   ============================================================ */

/* ================= ARCADE — a loving 16-bit game interface ================= */
/* tokens (light: bright sky-blue console, coin gold, hero red) */
body[data-theme="arcade"]{
  --paper:#EDF3F7; --paper-2:#DCE7EF; --card:#FFFFFF;
  --ink:#182430; --ink-soft:#43596D; --ink-faint:#7A8FA0; --rule:#C3D3DE;
  --indigo:#0968C4; --indigo-bright:#2B93EA; --indigo-wash:#D7EAFB; --ochre:#C98E06;
  --zh:#D8342C; --zh-wash:#F9DFDB; --geo:#1E9478; --good:#27A24A;
  --hard:#A9BCCA; /* hard-offset shadow tone */
  --serif:"Nunito Sans","Inter",sans-serif; --display:"Space Grotesk","Nunito Sans",sans-serif;
  --mono:"IBM Plex Mono",ui-monospace,monospace; --sans:"Nunito Sans","Inter",sans-serif;
  --px:"Press Start 2P","IBM Plex Mono",monospace;
}
/* night: deep CRT navy, brighter neons */
body[data-theme="arcade"].night{
  --paper:#0A1424; --paper-2:#060E1B; --card:#101E33;
  --ink:#E7F1FB; --ink-soft:#ADC3D8; --ink-faint:#6C849B; --rule:#263F5B;
  --indigo:#3FB0FF; --indigo-bright:#78CBFF; --indigo-wash:#0E3151; --ochre:#FFC53D;
  --zh:#FF6055; --zh-wash:#421719; --geo:#35C295; --good:#3CDC82;
  --hard:#02070D;
}
/* ambient: faint static CRT scanlines */
body[data-theme="arcade"]::before{ background:repeating-linear-gradient(0deg, color-mix(in srgb,var(--ink) 5%, transparent) 0 1px, transparent 1px 3px); opacity:.5; filter:none; }
/* pixel geometry: zero rounded corners (the gold seal alone stays a coin) */
body[data-theme="arcade"] *{ border-radius:0 !important; }
/* top bar = console chrome */
body[data-theme="arcade"] .topbar{ background:var(--paper); backdrop-filter:none; -webkit-backdrop-filter:none; border-bottom:3px solid var(--ink); box-shadow:0 3px 0 var(--hard); }
body[data-theme="arcade"] .brand .mark{ font-family:var(--px); font-weight:400; font-size:calc(14px * var(--fs)); letter-spacing:0; }
body[data-theme="arcade"] .brand .tag{ font-family:"VT323",var(--mono); font-size:calc(13px * var(--fs)); letter-spacing:.2em; }
body[data-theme="arcade"] .tab{ font-family:var(--px); font-size:calc(9px * var(--fs)); font-weight:400; letter-spacing:.04em; line-height:1.5; padding:12px 11px; }
body[data-theme="arcade"] .tab .ink{ height:3px; bottom:3px; }
/* page head = level title card */
body[data-theme="arcade"] .page-head h1{ font-family:var(--px); font-weight:400; font-size:calc(clamp(16px,2.8vw,21px) * var(--fs)); letter-spacing:.02em; line-height:1.55; text-transform:uppercase; }
body[data-theme="arcade"] .eyebrow{ font-family:"VT323",var(--mono); font-size:calc(15px * var(--fs)); font-weight:400; letter-spacing:.18em; }
body[data-theme="arcade"] .eyebrow::before{ content:"\25B6 "; color:var(--zh); }
body[data-theme="arcade"] .page-head::after{ height:5px; background:repeating-linear-gradient(90deg, var(--indigo) 0 12px, transparent 12px 18px); }
/* buttons depress like pad buttons */
body[data-theme="arcade"] .btn{ font-weight:800; text-transform:uppercase; letter-spacing:.08em; border:2px solid var(--ink); box-shadow:3px 3px 0 var(--hard); }
body[data-theme="arcade"] .btn:hover{ transform:translate(-1px,-1px); box-shadow:4px 4px 0 var(--hard); }
body[data-theme="arcade"] .btn:active{ transform:translate(3px,3px); box-shadow:0 0 0 var(--hard); }
body[data-theme="arcade"] .btn.ghost{ background:var(--card); }
body[data-theme="arcade"] .grades .grade{ border:2px solid var(--ink); box-shadow:0 4px 0 var(--hard); }
body[data-theme="arcade"] .grades .grade:hover{ transform:translateY(-1px); box-shadow:0 5px 0 var(--hard); }
body[data-theme="arcade"] .grades .grade:active{ transform:translateY(4px); box-shadow:0 0 0 var(--hard); }
/* surfaces: chunky ink borders + hard offset shadows */
body[data-theme="arcade"] .study-card{ border:2px solid var(--ink); box-shadow:8px 8px 0 var(--hard); }
body[data-theme="arcade"] .placard{ border:2px solid var(--ink); box-shadow:3px 3px 0 var(--hard); }
body[data-theme="arcade"] .gloss-win{ border:2px solid var(--ink); box-shadow:8px 8px 0 var(--hard); }
body[data-theme="arcade"] .gloss-bar{ background:var(--indigo-wash); border-bottom:2px solid var(--ink); }
body[data-theme="arcade"] .msn-card{ border:2px solid var(--ink); box-shadow:5px 5px 0 var(--hard); }
/* home epigraph = RPG dialog box (static continue-arrow) */
body[data-theme="arcade"] .daily-quote{ position:relative; border:2px solid var(--ink); background:var(--card); box-shadow:4px 4px 0 var(--hard); padding:16px 20px; }
body[data-theme="arcade"] .daily-quote::after{ content:"\25BC"; position:absolute; right:11px; bottom:7px; font-size:calc(9px * var(--fs)); color:var(--ink-faint); }
/* XP = chunky segmented HP bar; level numeral = score */
body[data-theme="arcade"] .xp-track{ height:14px; border:2px solid var(--ink); background:var(--paper-2); }
body[data-theme="arcade"] .xp-fill{ background:repeating-linear-gradient(90deg, var(--good) 0 8px, color-mix(in srgb,var(--good) 62%, #000) 8px 10px); }
body[data-theme="arcade"] .banner .stat b{ font-family:var(--px); font-weight:400; font-size:calc(14px * var(--fs)); line-height:1.3; }
/* daily banner = title screen */
body[data-theme="arcade"] .banner, body[data-theme="arcade"] .banner:hover{ border:2px solid var(--ink); border-left:8px solid var(--tile); }
body[data-theme="arcade"] .banner{ box-shadow:6px 6px 0 var(--hard); }
body[data-theme="arcade"] .banner:hover{ transform:translate(-2px,-2px); box-shadow:8px 8px 0 var(--hard); }
body[data-theme="arcade"] .banner::before{ background:repeating-conic-gradient(color-mix(in srgb,var(--indigo) 30%, transparent) 0 25%, transparent 0 50%) 0 0/12px 12px; opacity:.25; }
/* game tiles = menu cartridges (grip ridges along the top; unplayed stays quiet) */
body[data-theme="arcade"] .game-tile{ border:2px solid var(--ink); box-shadow:4px 4px 0 var(--hard); }
body[data-theme="arcade"] button.game-tile:hover{ transform:translate(-2px,-2px); box-shadow:6px 6px 0 var(--hard); border-color:var(--ink); }
body[data-theme="arcade"] button.game-tile:active{ transform:translate(2px,2px); box-shadow:0 0 0 var(--hard); }
body[data-theme="arcade"] .game-tile::before{ background:repeating-linear-gradient(90deg, color-mix(in srgb,var(--gt-accent) 55%, transparent) 0 5px, transparent 5px 10px) 0 0/100% 7px no-repeat; -webkit-mask:none; mask:none; opacity:.55; }
body[data-theme="arcade"] .gt-title{ font-family:var(--px); font-weight:400; font-size:calc(11px * var(--fs)); line-height:1.5; letter-spacing:.02em; }
/* library = level select; deco = static pixel-dither checker of the collection hue */
body[data-theme="arcade"] .collection{ border:2px solid var(--ink); box-shadow:6px 6px 0 var(--hard); }
body[data-theme="arcade"] .collection-deco{
  background:
    repeating-conic-gradient(color-mix(in srgb,var(--coll-bg,#888) 55%, transparent) 0 25%, transparent 0 50%) 0 0/10px 10px,
    linear-gradient(90deg, color-mix(in srgb,var(--coll-bg,#888) 26%, var(--card)), var(--card) 68%);
  -webkit-mask:linear-gradient(to left,#000 46%, transparent 94%); mask:linear-gradient(to left,#000 46%, transparent 94%);
}
body[data-theme="arcade"] .chev{ border:2px solid var(--ink); background:var(--card); box-shadow:0 3px 0 var(--hard); }
body[data-theme="arcade"] .node{ border-left-width:6px; }
/* small coins */
body[data-theme="arcade"] .hi-num{ font-family:var(--px); font-size:calc(10px * var(--fs)); border:2px solid var(--indigo); background:var(--indigo-wash); }
body[data-theme="arcade"] .theme-opt{ border:2px solid var(--rule); }

/* ============ ACADEMY — tokens ============ */
body[data-theme="academy"]{
  --paper:#F5F0E4; --paper-2:#EBE4D1; --card:#FCF9F0;
  --ink:#262218; --ink-soft:#564E3B; --ink-faint:#99906F; --rule:#DFD6BC;
  --indigo:#16305B; --indigo-bright:#24457E; --indigo-wash:#DEE4F0; --ochre:#A9822F;
  --zh:#8E2233; --zh-wash:#F2DEDA; --geo:#557C5B; --good:#3E7A46;
  --serif:"EB Garamond", Georgia, serif; --display:"EB Garamond", Georgia, serif;
  --mono:"IBM Plex Mono", ui-monospace, monospace; --sans:"Inter", ui-sans-serif, -apple-system, "Segoe UI", sans-serif;
}
/* night: past-midnight library — blue-black paper, lamp-lit warm text, brass */
body[data-theme="academy"].night{
  --paper:#0B101E; --paper-2:#070B15; --card:#121A2E;
  --ink:#EAE0C6; --ink-soft:#C0B394; --ink-faint:#847E66; --rule:#273250;
  --indigo:#8AA9D8; --indigo-bright:#A5BFE8; --indigo-wash:#1A2439; --ochre:#C9A25A;
  --zh:#CD5F6A; --zh-wash:#2E1B24; --geo:#7FA98B; --good:#74AE78;
}

/* ambient: faint laid-paper lines (horizontal wires + sparse chain lines) */
body[data-theme="academy"]::before{
  background:
    repeating-linear-gradient(180deg, color-mix(in srgb,var(--ink) 6%, transparent) 0 1px, transparent 1px 8px),
    repeating-linear-gradient(90deg, color-mix(in srgb,var(--ink) 4%, transparent) 0 1px, transparent 1px 120px);
  opacity:.55;
}
body[data-theme="academy"].night::before{ opacity:.3; }

/* topbar: flat masthead under a double rule */
body[data-theme="academy"] .topbar{ backdrop-filter:none; background:var(--paper); box-shadow:none; border-bottom:3px double color-mix(in srgb,var(--ink) 65%, transparent); }
body[data-theme="academy"] .brand .mark{ font-family:var(--display); font-variant:small-caps; letter-spacing:.06em; }
body[data-theme="academy"] .brand .tag{ font-family:var(--serif); font-style:italic; }
body[data-theme="academy"] .tab{ font-family:var(--display); font-variant:small-caps; letter-spacing:.12em; font-size:calc(14px * var(--fs)); font-weight:600; border-radius:0; }
body[data-theme="academy"] .tab .ink{ background:var(--zh); height:2px; border-radius:0; }
body[data-theme="academy"] .tab.active{ color:var(--indigo); }

/* page head: centred, double-rule, fleuron ornament */
body[data-theme="academy"] .page-head{ text-align:center; }
body[data-theme="academy"] .page-head h1{ font-family:var(--display); font-variant:small-caps; font-weight:600; letter-spacing:.03em; padding-bottom:10px; border-bottom:3px double color-mix(in srgb,var(--ink) 70%, transparent); }
body[data-theme="academy"] .page-head p{ font-family:var(--serif); font-style:italic; font-size:calc(15px * var(--fs)); }
body[data-theme="academy"] .page-head::after{ content:"\2766"; width:auto; max-width:none; height:auto; margin:9px auto 0; background:none; border-radius:0; color:var(--ochre); font-size:calc(15px * var(--fs)); line-height:1; }
body[data-theme="academy"] .eyebrow{ color:var(--zh); letter-spacing:.26em; }

/* buttons: rectangular, engraved (inset top highlight) */
body[data-theme="academy"] .btn{ border-radius:2px; font-family:var(--display); font-variant:small-caps; letter-spacing:.09em; font-weight:600; box-shadow:inset 0 1px 0 color-mix(in srgb,#FFFFFF 28%, transparent), 0 1px 2px color-mix(in srgb,var(--ink) 24%, transparent); }
body[data-theme="academy"] .btn.ghost{ box-shadow:none; }
body[data-theme="academy"] .grades .grade{ border-radius:2px; font-family:var(--display); box-shadow:inset 0 1px 0 color-mix(in srgb,#FFFFFF 22%, transparent), 0 1px 2px color-mix(in srgb,var(--ink) 20%, transparent); }

/* letterheads: thin ink frame + inner hairline ring */
body[data-theme="academy"] .card,
body[data-theme="academy"] .study-card,
body[data-theme="academy"] .banner,
body[data-theme="academy"] .msn-card{
  border-radius:3px; border:1px solid color-mix(in srgb,var(--ink) 48%, transparent);
  box-shadow:inset 0 0 0 4px var(--card), inset 0 0 0 5px color-mix(in srgb,var(--ink) 28%, transparent), 0 2px 6px color-mix(in srgb,var(--ink) 10%, transparent);
}

/* banner: embossed stationery head */
body[data-theme="academy"] .banner{ border-left:4px solid var(--tile); }
body[data-theme="academy"] .banner .review-title h2{ font-family:var(--display); font-variant:small-caps; letter-spacing:.03em; }
body[data-theme="academy"] .banner .meta{ letter-spacing:.04em; }
body[data-theme="academy"] .xp .xp-track{ border-radius:2px; border:1px solid var(--rule); }
body[data-theme="academy"] .xp .xp-fill{ border-radius:1px; }

/* game tiles: quiet until earned, square-cut, small-caps titles */
body[data-theme="academy"] .game-tile{ border-radius:3px; }
body[data-theme="academy"] .game-tile:not(.done):not(.won){ background:var(--card); }
body[data-theme="academy"] .gt-title{ font-family:var(--display); font-variant:small-caps; letter-spacing:.04em; }
body[data-theme="academy"] .gt-sub{ font-family:var(--serif); font-style:italic; }

/* collections: framed, heraldic side-band deco (STATIC) */
body[data-theme="academy"] .collection{ border-radius:3px; border:1px solid color-mix(in srgb,var(--ink) 48%, transparent); box-shadow:0 2px 6px color-mix(in srgb,var(--ink) 10%, transparent); }
body[data-theme="academy"] .collection-deco,
body[data-theme="academy"].night .collection-deco{
  background:linear-gradient(to left, color-mix(in srgb,var(--coll-bg,#888) 94%, #000000) 0 62px, transparent 62px);
}
body[data-theme="academy"] .collection-deco::after{
  content:""; position:absolute; top:7px; bottom:7px; right:7px; width:47px; pointer-events:none;
  border:1px solid color-mix(in srgb,#FFFFFF 55%, transparent);
}
body[data-theme="academy"].night .collection-deco::after{ border-color:color-mix(in srgb,#FFFFFF 32%, transparent); }
body[data-theme="academy"] .collection-title{ font-family:var(--display); font-variant:small-caps; letter-spacing:.02em; }
body[data-theme="academy"] .pill.soon{ border-radius:2px; letter-spacing:.08em; }
body[data-theme="academy"] .node{ border-radius:0; }

/* daily quote: formal epigraph between hairlines */
body[data-theme="academy"] .daily-quote{ border-top:1px solid var(--rule); border-bottom:1px solid var(--rule); padding:30px 12px; }
body[data-theme="academy"] .daily-quote blockquote{ font-family:var(--serif); font-size:calc(19px * var(--fs)); line-height:1.55; }
body[data-theme="academy"] .daily-quote figcaption{ letter-spacing:.14em; color:var(--ink-soft); }

/* glossary popups: navy title bar, framed body */
body[data-theme="academy"] .gloss-win{ border-radius:3px; border:1px solid color-mix(in srgb,var(--ink) 55%, transparent); box-shadow:0 10px 30px color-mix(in srgb,var(--ink) 25%, transparent); }
body[data-theme="academy"] .gloss-bar{ background:var(--indigo); color:#F2ECDA; font-family:var(--display); font-variant:small-caps; letter-spacing:.07em; }
body[data-theme="academy"].night .gloss-bar{ background:#16233C; color:#E8DFC6; }
body[data-theme="academy"] .gloss-body{ font-family:var(--serif); font-size:calc(15px * var(--fs)); }

/* study extras */
body[data-theme="academy"] .placard{ border-radius:2px; font-family:var(--display); font-variant:small-caps; letter-spacing:.08em; }
body[data-theme="academy"] .msn-chip{ border-radius:2px; }
body[data-theme="academy"] .faq-q{ font-family:var(--display); font-variant:small-caps; letter-spacing:.02em; }

/* ================= MARBLE — classical antiquity: white marble, bronze, inscriptions ================= */
/* tokens (light: gallery marble + bronze) */
body[data-theme="marble"]{
  --paper:#F2F0EA; --paper-2:#E5E1D6; --card:#FAF8F3;
  --ink:#262420; --ink-soft:#5B5449; --ink-faint:#97907E; --rule:#D8D3C5;
  --indigo:#8C6A3F; --indigo-bright:#A9834E; --indigo-wash:#EEE7D7; --ochre:#A98235;
  --zh:#7E2F27; --zh-wash:#F1DFDA; --geo:#6D7C4E; --good:#537A44;
  --serif:"Cormorant Garamond",Georgia,serif; --display:"Cinzel","Trajan Pro",Georgia,serif;
  --mono:"IBM Plex Mono",ui-monospace,monospace; --sans:"Inter",system-ui,sans-serif;
}
/* tokens (night: black basalt + gold leaf) */
body[data-theme="marble"].night{
  --paper:#141311; --paper-2:#0D0C0A; --card:#1D1B17;
  --ink:#EBE4D2; --ink-soft:#B4AB96; --ink-faint:#7E7663; --rule:#36322A;
  --indigo:#C9A45B; --indigo-bright:#E3BE72; --indigo-wash:#2B2417; --ochre:#B08C3F;
  --zh:#C25B4C; --zh-wash:#3B221C; --geo:#8B9A66; --good:#7EA35B;
}
/* ambient: faint fluted-column stripes (static) */
body[data-theme="marble"]::before{
  background:
    repeating-linear-gradient(90deg, transparent 0 62px, rgba(72,63,48,.05) 62px 64px, rgba(255,255,255,.65) 64px 66px, transparent 66px 128px),
    linear-gradient(180deg,#EDEAE1,#F2F0EA 42%);
  opacity:1; filter:none;
}
body[data-theme="marble"].night::before{
  background:
    repeating-linear-gradient(90deg, transparent 0 62px, rgba(0,0,0,.55) 62px 64px, rgba(255,255,255,.05) 64px 66px, transparent 66px 128px),
    linear-gradient(180deg,#191712,#141311 46%);
}
/* type: Cormorant body at 16px, incised Trajan-style capitals */
body[data-theme="marble"]{ font-size:calc(16px * var(--fs)); }
body[data-theme="marble"] .page-head h1,
body[data-theme="marble"] .review-title h2,
body[data-theme="marble"] .collection-title,
body[data-theme="marble"] .brand .mark,
body[data-theme="marble"] .gt-title{
  font-family:var(--display); text-transform:uppercase; letter-spacing:.1em;
  text-shadow:0 1px 0 rgba(255,255,255,.8), 0 -1px 0 rgba(0,0,0,.18);
}
body[data-theme="marble"].night .page-head h1,
body[data-theme="marble"].night .review-title h2,
body[data-theme="marble"].night .collection-title,
body[data-theme="marble"].night .brand .mark,
body[data-theme="marble"].night .gt-title{
  text-shadow:0 1px 0 rgba(0,0,0,.85), 0 -1px 0 rgba(255,255,255,.14);
}
/* topbar = architrave */
body[data-theme="marble"] .topbar{ backdrop-filter:none; background:var(--paper); border-bottom:4px double color-mix(in srgb,var(--ink) 45%, transparent); box-shadow:none; }
body[data-theme="marble"] .brand .mark{ letter-spacing:.2em; }
body[data-theme="marble"] .brand .tag{ font-family:var(--serif); font-style:italic; }
body[data-theme="marble"] .tab{ font-family:var(--display); text-transform:uppercase; letter-spacing:.12em; font-size:calc(11.5px * var(--fs)); font-weight:600; border-radius:0; }
body[data-theme="marble"] .tab .ink{ background:var(--indigo); }
/* page head: incised title, double-rule + fleuron ornament */
body[data-theme="marble"] .page-head h1{ font-weight:700; letter-spacing:.14em; }
body[data-theme="marble"] .eyebrow{ color:var(--indigo); letter-spacing:.24em; }
body[data-theme="marble"] .page-head p{ font-size:1.06em; }
body[data-theme="marble"] .page-head::after{
  content:"\2767"; position:static; display:block; width:100%; height:auto; margin-top:12px;
  text-align:center; font-size:calc(14px * var(--fs)); line-height:1; color:var(--indigo);
  background:
    linear-gradient(var(--rule),var(--rule)) 0 5px/42% 1px no-repeat,
    linear-gradient(var(--rule),var(--rule)) 100% 5px/42% 1px no-repeat,
    linear-gradient(var(--rule),var(--rule)) 0 8px/42% 1px no-repeat,
    linear-gradient(var(--rule),var(--rule)) 100% 8px/42% 1px no-repeat;
}
/* stone slabs: near-zero radius, edge bevel, faint diagonal marble veining */
body[data-theme="marble"] .study-card,
body[data-theme="marble"] .msn-card,
body[data-theme="marble"] .collection,
body[data-theme="marble"] .banner,
body[data-theme="marble"] .gloss-win{
  border:1px solid var(--rule); border-radius:2px;
  background:
    linear-gradient(104deg, transparent 42%, rgba(120,111,92,.05) 43%, transparent 46%),
    linear-gradient(76deg, transparent 63%, rgba(120,111,92,.04) 64%, transparent 68%),
    linear-gradient(118deg, transparent 21%, rgba(148,138,114,.05) 22%, transparent 25%),
    var(--card);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.75), inset 0 -1px 0 rgba(0,0,0,.1), 0 2px 6px rgba(40,34,24,.08);
}
body[data-theme="marble"].night .study-card,
body[data-theme="marble"].night .msn-card,
body[data-theme="marble"].night .collection,
body[data-theme="marble"].night .banner,
body[data-theme="marble"].night .gloss-win{
  background:
    linear-gradient(104deg, transparent 42%, rgba(235,228,210,.04) 43%, transparent 46%),
    linear-gradient(76deg, transparent 63%, rgba(235,228,210,.03) 64%, transparent 68%),
    var(--card);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.07), inset 0 -1px 0 rgba(0,0,0,.55), 0 3px 8px rgba(0,0,0,.45);
}
/* banner = museum pedestal plaque (bronze fillet + stepped plinth below) */
body[data-theme="marble"] .banner{ border-left:4px solid var(--tile); box-shadow: inset 0 1px 0 rgba(255,255,255,.75), inset 0 -1px 0 rgba(0,0,0,.1), 0 6px 0 -2px var(--paper-2), 0 7px 0 -1px var(--rule); }
body[data-theme="marble"].night .banner{ box-shadow: inset 0 1px 0 rgba(255,255,255,.07), inset 0 -1px 0 rgba(0,0,0,.55), 0 6px 0 -2px var(--paper-2), 0 7px 0 -1px var(--rule); }
/* buttons = chiseled bronze plaques */
body[data-theme="marble"] .btn{
  border-radius:0; font-family:var(--display); text-transform:uppercase; letter-spacing:.09em; font-size:calc(12.5px * var(--fs)); font-weight:600;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.3), inset 0 -1px 0 rgba(0,0,0,.22), 0 2px 0 rgba(40,34,24,.16);
  transition: box-shadow .15s ease, background .15s ease;
}
body[data-theme="marble"] .btn:active{ box-shadow: inset 0 2px 3px rgba(0,0,0,.25); }
body[data-theme="marble"] .grades .grade{
  border-radius:0; font-family:var(--display); text-transform:uppercase; letter-spacing:.07em; font-size:calc(13px * var(--fs));
  box-shadow: inset 0 1px 0 rgba(255,255,255,.35), inset 0 -1px 0 rgba(0,0,0,.18);
}
/* game tiles = inscription tablets (unplayed stays quiet marble) */
body[data-theme="marble"] .game-tile{ border-radius:0; }
body[data-theme="marble"] .game-tile:not(.done):not(.won){
  border:1px solid var(--rule);
  background:
    linear-gradient(104deg, transparent 40%, rgba(120,111,92,.05) 41%, transparent 44%),
    linear-gradient(70deg, transparent 66%, rgba(148,138,114,.045) 67%, transparent 70%),
    var(--card);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.7), inset 0 -1px 0 rgba(0,0,0,.1);
}
body[data-theme="marble"].night .game-tile:not(.done):not(.won){
  background: linear-gradient(104deg, transparent 40%, rgba(235,228,210,.04) 41%, transparent 44%), var(--card);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.06), inset 0 -1px 0 rgba(0,0,0,.5);
}
body[data-theme="marble"] .gt-sub{ font-family:var(--serif); font-style:italic; font-size:1.05em; }
/* collections: plain stone — a quiet wash of the collection hue (the meander frieze band was removed on request) */
body[data-theme="marble"] .collection-deco{
  background:linear-gradient(105deg, color-mix(in srgb,var(--coll-bg) 14%, var(--card)), var(--card) 66%);
  -webkit-mask:none; mask:none;
}
body[data-theme="marble"] .pill.soon{ border-radius:0; letter-spacing:.06em; }
body[data-theme="marble"] .node{ border-radius:0; }
/* glossary popups, xp bar, epigraph */
body[data-theme="marble"] .gloss-bar{ font-family:var(--display); text-transform:uppercase; letter-spacing:.09em; font-size:calc(12px * var(--fs)); background:var(--paper-2); border-bottom:1px solid var(--rule); }
body[data-theme="marble"] .xp .xp-track{ border-radius:0; box-shadow:inset 0 1px 2px rgba(0,0,0,.14); }
body[data-theme="marble"] .xp .xp-fill{ border-radius:0; background:linear-gradient(180deg,var(--indigo-bright),var(--indigo)); }
body[data-theme="marble"] .daily-quote{ font-family:var(--serif); font-style:italic; font-size:1.08em; }

/* ---- GAZETTE — 1940s newspaper press: black on newsprint grey, one rubber-stamp red ---- */
body[data-theme="gazette"]{
  --paper:#DAD8CF; --paper-2:#CBC9BF; --card:#E8E6DE;
  --ink:#191917; --ink-soft:#45443E; --ink-faint:#807E75; --rule:#B7B5AA;
  --indigo:#1D1C1A; --indigo-bright:#B5271D; --indigo-wash:#CFCDC2; --ochre:#8E2B1F;
  --zh:#B5271D; --zh-wash:#E6D0CA; --geo:#5E6B4F; --good:#3E6B41;
  --serif:"PT Serif", Georgia, serif; --display:"Oswald", "Arial Narrow", Arial, sans-serif;
  --mono:"Special Elite", "IBM Plex Mono", ui-monospace, monospace; --sans:"Inter", Arial, sans-serif;
}
body[data-theme="gazette"].night{ /* late edition */
  --paper:#151412; --paper-2:#0F0E0D; --card:#1D1C19;
  --ink:#E9E6DB; --ink-soft:#C3C0B4; --ink-faint:#8A877D; --rule:#3B3933;
  --indigo:#CE4231; --indigo-bright:#E4604B; --indigo-wash:#331E19; --ochre:#D97B45;
  --zh:#E05A47; --zh-wash:#39201B; --geo:#93A37F; --good:#7CB374;
}
/* flat press shadows — hard offset or none (kept out of the token blocks: not globe colours) */
body[data-theme="gazette"]{ --shadow-sm:none; --shadow-md:3px 3px 0 color-mix(in srgb,var(--ink) 22%, transparent); --shadow-lg:5px 5px 0 color-mix(in srgb,var(--ink) 22%, transparent); }
body[data-theme="gazette"].night{ --shadow-md:3px 3px 0 color-mix(in srgb,#000 60%, transparent); --shadow-lg:5px 5px 0 color-mix(in srgb,#000 60%, transparent); }

/* ambient — static halftone dot screen on newsprint */
body[data-theme="gazette"]::before{
  background:radial-gradient(color-mix(in srgb,var(--ink) 7%, transparent) 1px, transparent 1.4px) 0 0/7px 7px,
             radial-gradient(color-mix(in srgb,var(--ink) 5%, transparent) 1px, transparent 1.4px) 3.5px 3.5px/7px 7px;
  opacity:1; filter:none;
}
body[data-theme="gazette"].night::before{ opacity:.45; }

/* topbar — masthead bar with double rule */
body[data-theme="gazette"] .topbar{ backdrop-filter:none; background:var(--paper); border-bottom:2px solid var(--ink); box-shadow:0 4px 0 -3px var(--ink); }
body[data-theme="gazette"] .brand .mark{ font-family:var(--display); text-transform:uppercase; letter-spacing:.06em; font-weight:600; }
body[data-theme="gazette"] .brand .tag{ font-family:var(--mono); }
body[data-theme="gazette"] .tab{ font-family:var(--display); text-transform:uppercase; letter-spacing:.12em; font-size:calc(12px * var(--fs)); font-weight:500; border-radius:0; }
body[data-theme="gazette"].night .tab.active{ text-shadow:0 0 12px color-mix(in srgb,var(--indigo) 45%, transparent); }

/* page-head — the MASTHEAD: 3px rule above, 1px below, typewriter dateline */
body[data-theme="gazette"] .page-head{ border-top:3px solid var(--ink); border-bottom:1px solid var(--ink); padding:12px 0 14px; }
body[data-theme="gazette"] .page-head h1{ font-family:var(--display); text-transform:uppercase; font-weight:700; letter-spacing:.015em; font-size:calc(clamp(32px,5.4vw,46px) * var(--fs)); line-height:1.04; }
body[data-theme="gazette"] .page-head p{ font-family:var(--serif); color:var(--ink-soft); }
body[data-theme="gazette"] .eyebrow{ color:var(--ink); letter-spacing:.2em; }
body[data-theme="gazette"] .page-head::after{ display:none; }

/* square everything — column-rule geometry, no soft corners */
body[data-theme="gazette"] .btn, body[data-theme="gazette"] .grade, body[data-theme="gazette"] .card,
body[data-theme="gazette"] .placard, body[data-theme="gazette"] .banner, body[data-theme="gazette"] .collection,
body[data-theme="gazette"] .game-tile, body[data-theme="gazette"] .study-card, body[data-theme="gazette"] .gloss-win,
body[data-theme="gazette"] .msn-card, body[data-theme="gazette"] .msn-curve, body[data-theme="gazette"] .msn-chip,
body[data-theme="gazette"] .faq-q, body[data-theme="gazette"] .chev, body[data-theme="gazette"] .xp-track,
body[data-theme="gazette"] .xp-fill, body[data-theme="gazette"] .prog, body[data-theme="gazette"] .prog .fill,
body[data-theme="gazette"] .active-deck, body[data-theme="gazette"] .node, body[data-theme="gazette"] .pill,
body[data-theme="gazette"] .daily-quote, body[data-theme="gazette"] .theme-opt,
body[data-theme="gazette"] .lang-opt{ border-radius:0; }

/* ink frames + hard offsets */
body[data-theme="gazette"] .banner{ border-color:var(--ink); box-shadow:var(--shadow-md); }
body[data-theme="gazette"] .collection{ border:1px solid var(--ink); box-shadow:var(--shadow-md); }
body[data-theme="gazette"] .study-card, body[data-theme="gazette"] .placard{ border:1px solid var(--ink); box-shadow:var(--shadow-md); }
body[data-theme="gazette"] .gloss-win{ border:1px solid var(--ink); box-shadow:var(--shadow-lg); }
body[data-theme="gazette"] .gloss-bar{ background:var(--paper-2); border-bottom:1px solid var(--ink); font-family:var(--display); text-transform:uppercase; letter-spacing:.06em; }

/* buttons — condensed caps, stamped flat */
body[data-theme="gazette"] .btn{ font-family:var(--display); text-transform:uppercase; letter-spacing:.1em; font-weight:500; }
body[data-theme="gazette"] .btn.ghost{ border-color:var(--ink); color:var(--ink); }
body[data-theme="gazette"].night .btn:not(.ghost){ box-shadow:0 0 18px color-mix(in srgb,var(--indigo) 35%, transparent); }

/* game tiles — classified-ad boxes; tile colour only as a corner tag */
body[data-theme="gazette"] .game-tile{ background:var(--card); border:1px solid var(--ink); }
body[data-theme="gazette"] .game-tile:not(.done):not(.won){ box-shadow:inset 0 0 0 3px var(--card), inset 0 0 0 4px var(--rule); }
body[data-theme="gazette"] .game-tile::after{ content:""; position:absolute; top:0; left:0; width:0; height:0; border-top:16px solid var(--gt-accent, var(--rule)); border-right:16px solid transparent; pointer-events:none; }
body[data-theme="gazette"] .gt-title{ font-family:var(--display); text-transform:uppercase; letter-spacing:.05em; }
body[data-theme="gazette"] .gt-glyph{ opacity:.08; }
/* the played check as a red rubber stamp */

/* collections — engraved hatching in the collection hue (static) */
body[data-theme="gazette"] .collection-deco{
  background:repeating-linear-gradient(-45deg, color-mix(in srgb,var(--coll-bg) 50%, transparent) 0 1px, transparent 1px 6px);
  -webkit-mask:linear-gradient(to left,#000 55%, transparent 95%); mask:linear-gradient(to left,#000 55%, transparent 95%);
}
body[data-theme="gazette"] .collection-title{ font-family:var(--display); text-transform:uppercase; letter-spacing:.04em; }
body[data-theme="gazette"] .pill.soon{ font-family:var(--mono); border:1px solid var(--ink); }

/* About prose — two newspaper columns on desktop */
@media (min-width:900px){
  body[data-theme="gazette"] .msn-prose{ column-count:2; column-gap:28px; column-rule:1px solid var(--rule); }
}

/* daily quote — boxed pull-quote with a big quotation mark */
body[data-theme="gazette"] .daily-quote{ position:relative; background:var(--card); border:1px solid var(--ink); border-top-width:3px; border-bottom-width:3px; padding:30px 30px 24px; }
body[data-theme="gazette"] .daily-quote::before{ content:"\201C"; position:absolute; top:0; left:12px; font-family:var(--display); font-weight:700; font-size:calc(76px * var(--fs)); line-height:1; color:var(--zh); opacity:.5; pointer-events:none; }

/* xp — solid ink column-inch bars; level numeral in stark ink, not gold */
body[data-theme="gazette"] .xp-track{ border:1px solid var(--ink); background:var(--paper-2); }
body[data-theme="gazette"] .xp-fill, body[data-theme="gazette"] .prog .fill{ background:var(--ink); }
body[data-theme="gazette"] .coll-ic{ color:var(--ink); }

/* ---- theme picker (settings) ---- */
.set-row-block{flex-direction:column; align-items:stretch; gap:16px;}
.theme-grid{display:grid; grid-template-columns:repeat(4,1fr); gap:12px;}
.theme-opt{display:flex; flex-direction:column; align-items:flex-start; gap:9px; appearance:none; cursor:pointer; text-align:left; padding:14px; border:1.5px solid var(--rule); border-radius:12px; background:var(--card); transition:border-color .15s, box-shadow .15s, transform .12s;}
.theme-opt:hover{border-color:var(--ink-faint); transform:translateY(-1px);}
/* A LOCKED TILE, WHICH ONLY AN EDITOR EVER SEES (Sep 2026, on request: "on an Admin account, allow me to
   see and select locked themes"). It is dimmed and its name is set in the quiet ink, so the row still
   reads at a glance as "these are yours, those are not" — but it is NOT `pointer-events:none` and carries
   no padlock, because unlike the pre-Aug-2026 picker this tile is one the reader pressing it CAN have.
   A control that looks disabled and works is worse than either, so the dimming is light and the tag line
   under the name says which state it is in outright. */
.theme-opt.theme-locked{opacity:.62;}
.theme-opt.theme-locked .theme-tag{color:var(--ochre); font-style:italic;}
.theme-opt.theme-locked:hover, .theme-opt.theme-locked.active{opacity:1;}
.theme-opt.active{border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash);}
.theme-opt:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
/* A LOCKED THEME (Aug 2026) — shown rather than hidden, since a collectible nobody can see is one nobody
   knows to want, and still PRESSABLE, since a disabled button fires no mouse events in Chrome and the
   hover try-on is the best advertisement a theme has. What marks it is a dashed rim and a quieted
   specimen; the name and the padlock stay at full strength, or the row reads as failed rather than
   locked. `setTheme` is what actually refuses it. */
.theme-opt.theme-locked{border-style:dashed; cursor:pointer;}
.theme-opt.theme-locked .theme-mock{opacity:.5; filter:saturate(.55);}
.theme-opt.theme-locked:hover .theme-mock{opacity:1; filter:none;}
.theme-opt .theme-lock{font-size:calc(10px * var(--fs)); opacity:.75;}
/* the chest's theme reveal: the same specimen the picker draws, at the size an artefact's plate gets */
.chest-art.chest-theme{width:min(210px,58vw);}
.chest-art.chest-theme .theme-mock{width:100%; height:auto; aspect-ratio:16/10;}
.theme-swatches{display:flex; gap:5px;}
.theme-swatches i{width:26px; height:26px; border-radius:6px; border:1px solid color-mix(in srgb,var(--ink) 14%,transparent);}
.theme-name{font-family:var(--display); font-size:calc(16px * var(--fs)); font-weight:600; color:var(--ink); line-height:1;}
.theme-opt[data-theme="folio"] .theme-name{font-family:"Nunito Sans",system-ui,sans-serif; font-weight:700; letter-spacing:-.01em;}
.theme-opt[data-theme="synth"] .theme-name{font-family:"Sora",sans-serif;}
.theme-opt[data-theme="arcade"] .theme-name{font-family:"Press Start 2P",monospace; font-size:calc(10px * var(--fs)); font-weight:400; line-height:1.4;}
.theme-opt[data-theme="academy"] .theme-name{font-family:"EB Garamond",Georgia,serif; font-variant:small-caps;}
.theme-opt[data-theme="marble"] .theme-name{font-family:"Cinzel",Georgia,serif; font-size:calc(13.5px * var(--fs)); letter-spacing:.08em; text-transform:uppercase;}
.theme-opt[data-theme="gazette"] .theme-name{font-family:"Oswald","Arial Narrow",sans-serif; text-transform:uppercase; letter-spacing:.04em;}
.theme-tag{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink-faint);}
/* mini mockup: the theme's paper with a title bar, two text lines and an accent dot */
.theme-mock{position:relative; display:block; width:100%; height:46px; border-radius:7px; background:var(--tm-p); border:1px solid color-mix(in srgb,var(--ink) 14%, transparent); overflow:hidden;}
.theme-mock i{position:absolute; display:block; border-radius:2px;}
.theme-mock .tm-bar{left:8px; top:8px; width:46%; height:7px; background:var(--tm-a);}
.theme-mock .tm-line{left:8px; top:22px; width:72%; height:3.5px; background:color-mix(in srgb,var(--tm-a) 32%, var(--tm-p));}
.theme-mock .tm-line.short{top:30px; width:52%;}
.theme-mock .tm-dot{right:8px; top:8px; width:12px; height:12px; border-radius:50%; background:var(--tm-b);}
.theme-group-label{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.2em; text-transform:uppercase; color:var(--ink-faint); margin:14px 0 8px;}
#themeGrid > .theme-group-label:first-child{margin-top:0;}
/* settings section heads (reuse the About page's chip language) */
.set-head{display:flex; align-items:center; gap:12px; padding:16px 0 4px;}
.set-head h2{margin:0; font-family:var(--display); font-weight:600; font-size:calc(19px * var(--fs)); letter-spacing:-.01em; color:var(--ink);}
/* narrator cards */
.narr-grid{display:grid; grid-template-columns:repeat(2,1fr); gap:9px;}
.narr-card{display:flex; align-items:center; gap:10px; appearance:none; cursor:pointer; text-align:left; padding:11px 13px; border:1.5px solid var(--rule); border-radius:10px; background:var(--card); font:inherit; color:var(--ink); transition:border-color .15s, box-shadow .15s;}
.narr-card:hover{border-color:var(--ink-faint);}
.narr-card.active{border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash);}
.nc-flag{flex:none; font-family:var(--mono); font-size:calc(10px * var(--fs)); font-weight:600; letter-spacing:.06em; color:var(--indigo); background:var(--indigo-wash); border-radius:6px; padding:4px 7px;}
.nc-name{flex:1; font-family:var(--sans); font-size:calc(13.5px * var(--fs)); font-weight:600;}
.nc-play{flex:none; width:24px; height:24px; display:flex; align-items:center; justify-content:center; border-radius:50%; background:color-mix(in srgb,var(--indigo) 10%, transparent); color:var(--indigo);}
.nc-play svg{width:11px; height:11px; display:block;}
/* danger zone */
.set-card.danger{border-color:color-mix(in srgb,var(--zh) 45%, var(--rule));}
.set-card.danger .set-head h2{color:var(--zh);}
.danger-btn{border-color:color-mix(in srgb,var(--zh) 55%, var(--rule)); color:var(--zh);}
.danger-btn:hover{background:var(--zh-wash);}
@media (max-width: 640px){ .theme-grid{grid-template-columns:repeat(2,1fr);} .narr-grid{grid-template-columns:1fr;} }

/* ============================================================
   COMMUNITY DECKS — the Library's "Your decks" section + the Studio
   ============================================================ */
.btn.tiny{padding:8px 13px; font-size:calc(11px * var(--fs)); letter-spacing:.07em; box-shadow:none;}
.btn.tiny:hover{box-shadow:var(--shadow-sm);}
/* ---------- A DECK DOWNLOADING, IN THE ROW'S OWN BUTTON (Aug 2026, on request) ----------
   The Download button on a `.dk-pending` row fills up as the file arrives instead of saying
   "Downloading…" for half a minute. The bar is the BUTTON — a language deck is up to 9.6 MB and the row
   is already narrow, so a second element beside it would be two controls saying one thing.
   The fill is a `position:absolute` child UNDER the label rather than a background gradient, so the
   percentage stays legible over it at any width; the button keeps `overflow:hidden` so the fill takes
   its rounded corners; and the label sits in its own `position:relative` span so it paints above.
   `pointer-events:none` on both, because the button is disabled while this is up and neither child
   should ever be the click target. The width TRANSITIONS, so the reduced-motion killswitch lands it on
   its true value with no rule of its own. */
.btn.tiny.dk-dl-busy{position:relative; overflow:hidden; cursor:default; min-width:112px;}
.dkdl-t{position:relative; z-index:1; pointer-events:none;}
.dkdl-fill{
  position:absolute; inset-block:0; inset-inline-start:0; width:0; z-index:0; pointer-events:none;
  background:color-mix(in srgb, var(--indigo) 26%, transparent);
  transition:width .2s var(--ease);
}
/* AN UPDATE IS AVAILABLE FOR A DECK ALREADY HERE (Sep 2026, on the 蛋糕 report). It shares Download's
   markup and its bar because it is the same fetch, and differs in ink because it is a different offer:
   Download is the only thing a pending row can do, where this sits on a row that is already perfectly
   studiable and must not shout over the deck's own counts. Ochre rather than indigo — the site's
   "worth knowing" colour, and never the red that would read as a fault in the reader's deck. */
.btn.tiny.dk-up{
  border-color:color-mix(in srgb, var(--ochre) 55%, var(--rule));
  color:color-mix(in srgb, var(--ochre) 82%, var(--ink));
  background:color-mix(in srgb, var(--ochre) 10%, transparent);
}
.btn.tiny.dk-up:hover{background:color-mix(in srgb, var(--ochre) 20%, transparent);}
/* THE CHARACTER NETWORK (Sep 2026) — the other words in this deck built on the character just tapped.
   It borrows `.ctx-menu`'s box, shadow and positioning rather than inventing a second floating panel;
   what it adds is a scrolling list and a three-column row. Capped in height so a character used by
   forty words cannot fill the screen, and the list scrolls inside it. */
/* A CONJUGATION HEADING THAT EXPLAINS ITSELF (Sep 2026, on request). Only a heading app.js has a note
   for is given the class, so the affordance is never on one that answers a press with nothing; it is a
   dotted underline and `cursor:help` rather than a link, because the heading is still a heading and the
   table must not read as a row of buttons. */
.uc-cj-ex{
  cursor:help; text-decoration:underline dotted currentColor;
  text-underline-offset:3px; text-decoration-thickness:1px;
}
.uc-cj-ex:hover, .uc-cj-ex:focus-visible{color:var(--ink); text-decoration-style:solid;}
.tensewin{padding:0; min-width:250px; max-width:min(380px, 92vw);}
.tw-head{
  display:flex; align-items:center; gap:8px; padding:9px 10px 7px;
  border-bottom:1px solid var(--rule);
}
.tw-n{flex:1; font-family:var(--serif); font-size:calc(14px * var(--fs)); color:var(--ink);}
.tw-x{
  flex:none; width:24px; height:24px; border:0; background:transparent; cursor:pointer;
  font-size:calc(17px * var(--fs)); line-height:1; color:var(--ink-soft); border-radius:6px;
}
.tw-x:hover{background:var(--paper-2); color:var(--ink);}
.tw-en{
  padding:8px 10px 0; font-family:var(--sans); font-size:calc(10.5px * var(--fs));
  letter-spacing:.06em; text-transform:uppercase; color:var(--ink-faint);
}
.tw-b{padding:3px 10px 10px; font-family:var(--sans); font-size:calc(12.5px * var(--fs)); line-height:1.5; color:var(--ink-soft);}
.charwin{padding:0; min-width:230px; max-width:min(360px, 92vw);}
.cw-head{
  display:flex; align-items:center; gap:9px; padding:9px 10px 8px;
  border-bottom:1px solid var(--rule);
}
.cw-ch{font-family:var(--han); font-size:calc(21px * var(--fs)); line-height:1; color:var(--ink);}
/* the character's own reading, beside it (Sep 2026). Set in the site's mono face rather than the Han one:
   it is a romanisation, and setting it in the CJK face would file it with the character it describes. */
.cw-r{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.02em; color:var(--zh);}
.cw-n{flex:1; font-family:var(--sans); font-size:calc(11px * var(--fs)); color:var(--ink-faint);}
.cw-x{
  flex:none; width:24px; height:24px; border:0; background:transparent; cursor:pointer;
  font-size:calc(17px * var(--fs)); line-height:1; color:var(--ink-soft); border-radius:6px;
}
.cw-x:hover{background:var(--paper-2); color:var(--ink);}
.cw-list{max-height:min(46vh, 320px); overflow-y:auto; padding:4px 0;}
.cw-row{display:flex; align-items:baseline; gap:8px; padding:4px 10px;}
.cw-row + .cw-row{border-top:1px solid color-mix(in srgb, var(--rule) 45%, transparent);}
.cw-w{flex:none; font-family:var(--han); font-size:calc(15px * var(--fs)); color:var(--ink);}
.cw-p{flex:none; font-family:var(--sans); font-size:calc(11px * var(--fs)); color:var(--zh);}
.cw-g{
  flex:1; min-width:0; font-family:var(--sans); font-size:calc(11px * var(--fs));
  color:var(--ink-soft); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.cw-more{padding:8px 10px; font-family:var(--sans); font-size:calc(11px * var(--fs)); color:var(--ink-faint);}
/* the character block's own characters are the handle, so they say so */
.uc-chc{cursor:pointer;}
.btn.danger{background:transparent; color:var(--zh); border-color:color-mix(in srgb,var(--zh) 45%, var(--rule));}
.btn.danger:hover{background:var(--zh); color:#fff; border-color:var(--zh);}

/* --- Library section --- */
.community-group{margin-top:34px;}
.udeck-intro{margin:0 0 14px; font-family:var(--sans); font-size:calc(13.5px * var(--fs)); line-height:1.6; color:var(--ink-soft); max-width:62ch;}
.udeck-actions{display:flex; flex-wrap:wrap; gap:9px; margin-bottom:16px;}
/* a deck of your own reads as clearly not-curated: a dashed rule instead of a collection's hue wash */
.collection.udeck > .collection-row{background:var(--card); border:1.5px dashed var(--rule); border-left-width:3px; border-left-style:solid; border-left-color:var(--ink-faint);}
.collection.udeck > .collection-row:hover{border-left-color:var(--indigo);}
.collection.udeck .collection-deco{display:none;}
.udeck-sub{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-faint); margin:1px 0 5px;}
.udeck-edit{display:flex; align-items:center; justify-content:center; width:32px; height:32px; border-radius:8px; border:1px solid var(--rule); background:transparent; color:var(--ink-faint); cursor:pointer; transition:color .18s var(--ease), border-color .18s var(--ease), background .18s var(--ease);}
.udeck-edit svg{width:14px; height:14px; display:block;}
.udeck-edit:hover{color:var(--indigo); border-color:var(--indigo); background:var(--indigo-wash);}

/* --- Studio: deck list --- */
/* `.studio-actions` is a hook on `.udeck-actions` now (Oct 2026); it has no look of its own */
.studio-list{display:flex; flex-direction:column; gap:11px;}
/* the orphan rows only (Oct 2026): a reader's own deck rows are `.collection` rows on the Collections page now and
   carry `.studio-deck` as a hook, not as a look */
.orphan-deck{display:flex; align-items:stretch; gap:10px; background:var(--card); border:1px solid var(--rule); border-radius:13px; padding:13px 15px; transition:border-color .18s var(--ease);}
.orphan-deck:hover{border-color:var(--ink-faint);}
.studio-deck-open{flex:1; min-width:0; display:flex; flex-direction:column; gap:3px; text-align:left; background:none; border:0; padding:0; cursor:pointer; color:inherit;}
.sd-title{font-family:var(--display); font-size:calc(18px * var(--fs)); font-weight:600; letter-spacing:-.01em; color:var(--ink);}
.sd-meta{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink-faint);}
.sd-sub{font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink-soft);}
.studio-deck-actions{display:flex; align-items:center; gap:7px; flex-wrap:wrap;}
.studio-empty{margin-top:6px;}
.studio-empty code{font-family:var(--mono); font-size:calc(11px * var(--fs));}
/* Decks this account has published that this device holds no copy of (Aug 2026 — the other half of the
   delete fix). DASHED, like a community row, because these are not decks that can be opened or studied
   here: the only thing this device holds is the right to take them down. */
.studio-orphans{margin-top:30px;}
.studio-sec{margin:0 0 6px; font-family:var(--display); font-size:calc(19px * var(--fs)); font-weight:600; letter-spacing:-.01em; color:var(--ink);}
.studio-sec-note{margin:0 0 13px; max-width:62ch; font-family:var(--sans); font-size:calc(13px * var(--fs)); line-height:1.55; color:var(--ink-soft);}
.orphan-deck{border-style:dashed;}
.orphan-deck .sd-title{color:var(--ink-soft);}

/* --- Studio: one deck --- */
.studio-head{display:flex; align-items:center; gap:14px; flex-wrap:wrap; margin-bottom:16px;}
.studio-title{flex:1; min-width:200px; margin:0; font-family:var(--display); font-size:calc(30px * var(--fs)); font-weight:600; letter-spacing:-.02em; color:var(--ink);}
.studio-head-actions{display:flex; gap:7px;}
.studio-settings{margin-bottom:18px; border:1px solid var(--rule); border-radius:13px; background:var(--card); overflow:hidden;}
.studio-settings > summary{cursor:pointer; padding:13px 16px; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-soft);}
.studio-settings > summary:hover{color:var(--indigo);}
.studio-settings-body{padding:4px 16px 16px; display:flex; flex-direction:column; gap:11px; border-top:1px solid var(--rule);}
.af-note{margin:2px 0 0; font-family:var(--sans); font-size:calc(12.5px * var(--fs)); line-height:1.55; color:var(--ink-faint);}
.studio-cols{display:grid; grid-template-columns:262px minmax(0,1fr); gap:18px; align-items:start;}
.studio-cardlist{border:1px solid var(--rule); border-radius:13px; background:var(--card); overflow:hidden; position:sticky; top:14px;}
.studio-cardlist-head{display:flex; align-items:center; justify-content:space-between; gap:8px; padding:11px 12px; border-bottom:1px solid var(--rule); font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.07em; text-transform:uppercase; color:var(--ink-faint);}
.studio-cardrows{max-height:min(64vh,620px); overflow-y:auto;}
.studio-cardrow{display:flex; align-items:center; gap:4px; padding:2px 8px 2px 4px; border-bottom:1px solid var(--rule-faint,var(--rule));}
.studio-cardrow:last-child{border-bottom:0;}
.studio-cardrow.active{background:var(--indigo-wash);}
.scr-open{flex:1; min-width:0; display:flex; align-items:center; gap:8px; background:none; border:0; padding:9px 4px; cursor:pointer; text-align:left; color:inherit;}
.scr-n{flex:none; width:20px; font-family:var(--mono); font-size:calc(10px * var(--fs)); color:var(--ink-faint);}
.scr-title{flex:1; min-width:0; font-family:var(--sans); font-size:calc(13px * var(--fs)); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.studio-cardrow.active .scr-title{font-weight:600; color:var(--indigo);}
.scr-move{display:flex; flex-direction:column; gap:1px;}
.scr-btn{width:19px; height:14px; display:flex; align-items:center; justify-content:center; font-size:calc(8px * var(--fs)); line-height:1; border:0; border-radius:4px; background:none; color:var(--ink-faint); cursor:pointer;}
.scr-btn:hover:not([disabled]){color:var(--indigo); background:var(--indigo-wash);}
.scr-btn[disabled]{opacity:.25; cursor:default;}
.studio-editor{min-width:0;}
@media (max-width: 860px){
  .studio-cols{grid-template-columns:1fr;}
  .studio-cardlist{position:static;}
  .studio-cardrows{max-height:280px;}
}

/* --- Community: browse + one deck's page (phase 2) --- */
.cbrowse-bar{display:flex; flex-wrap:wrap; gap:9px; align-items:center; margin-bottom:18px;}
.cbrowse-q{flex:1; min-width:180px;}
/* ---------- the shared-deck TABLE, on the Collections page (Aug 2026, on request) ----------
   Columns that sort, which is what a reader comparing twenty strangers' decks is after — and a real
   <table>, so the header cells carry aria-sort and a screen reader is told what each figure is a figure of.
   `.sd-wrap` is what keeps a wide table honest: it scrolls INSIDE ITSELF rather than widening the page,
   which is the site's own rule for wide content. Below 640px three of the six go — author, installs and
   updated — leaving the deck and the two figures a reader chooses on (its size and its rating). The name is
   the only part of a row with no shorter form, so anything taking width from it is what gives, and
   everything dropped is on the deck's own page one press away. Each cell carries its COLUMN'S OWN KEY
   (`sd-c-<key>`, on the header and the cells alike), so dropping one is naming it once rather than counting
   to the same nth-child in two places and getting one of them wrong. */
.sd-wrap{border:1px solid var(--rule); border-radius:13px; background:var(--card); overflow-x:auto;}
.sd-table{width:100%; border-collapse:collapse; font-family:var(--sans);}
.sd-th{text-align:left; padding:0; border-bottom:1px solid var(--rule); background:var(--paper-2); white-space:nowrap;}
.sd-th.sd-num{text-align:right;}
.sd-sort{width:100%; display:inline-flex; align-items:center; gap:5px; background:none; border:0; cursor:pointer;
  padding:10px 14px; font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.09em;
  text-transform:uppercase; color:var(--ink-faint);}
.sd-th.sd-num .sd-sort{justify-content:flex-end;}
.sd-sort:hover{color:var(--ink);}
.sd-th.on .sd-sort{color:var(--indigo);}
.sd-arrow{font-size:calc(8px * var(--fs));}
.sd-row{border-top:1px solid var(--rule); cursor:pointer; transition:background .16s var(--ease);}
.sd-row:first-child{border-top:0;}
.sd-row:hover{background:var(--paper-2);}
.sd-row:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}
.sd-table td{padding:11px 14px; vertical-align:middle; font-size:calc(13.5px * var(--fs)); color:var(--ink);}
.sd-table td.sd-num{text-align:right; font-family:var(--mono); font-size:calc(12px * var(--fs)); color:var(--ink-faint); white-space:nowrap;}
/* NOWRAP, now that nothing in this cell takes a full basis: the author's description left the table on
   request (see sharedRowsHTML) and the wrap that let it sit under the title would otherwise only ever
   fire by accident, dropping a pill onto a second line and making one row of a scannable list taller
   than its neighbours for no reason a reader could see. */
.sd-deck{display:flex; align-items:center; gap:6px; min-width:190px;}
.sd-title{font-family:var(--display); font-size:calc(15px * var(--fs)); color:var(--ink); overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.sd-author{color:var(--ink-faint); font-size:calc(12.5px * var(--fs)); white-space:nowrap;}
.sd-rate .stars{justify-content:flex-end;}
@media (max-width:640px){
  .sd-c-author,.sd-c-installs,.sd-c-updated{display:none;}
  .sd-table td{padding:10px;}
  .sd-sort{padding:9px 10px;}
}
/* ---------- shared decks: a LIST, not a grid of tiles (Aug 2026, on request) ----------
   A tile gives every deck the same area whatever it has to say, so a title, a subtitle, a rating and three
   figures were stacked into a card the width of a phrase, and two decks whose titles differ in the fourth
   word looked identical. A row lets the title run at full width and puts the rating in a column that lines
   up down the list, which is what a reader scanning twenty decks is actually reading. The whole row is the
   button, so the row IS the way to the deck's page. */
.cdeck-list{display:flex; flex-direction:column; border:1px solid var(--rule); border-radius:13px; overflow:hidden; background:var(--card);}
.cdeck{display:flex; align-items:center; gap:14px; width:100%; text-align:left; padding:13px 15px; border:0; border-top:1px solid var(--rule); background:none; cursor:pointer; position:relative; transition:background .16s var(--ease);}
.cdeck:first-child{border-top:0;}
.cdeck:hover{background:var(--paper-2);}
.cdeck:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}
.cdeck-main{flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:3px;}
.cdeck-titleline{display:flex; align-items:baseline; gap:9px; flex-wrap:wrap;}
.cdeck-title{font-family:var(--display); font-size:calc(17px * var(--fs)); font-weight:600; letter-spacing:-.01em; color:var(--ink);}
.cdeck-sub{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-soft); line-height:1.45; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.cdeck-meta{display:flex; flex-wrap:wrap; gap:9px; margin-top:2px; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint);}
/* the chevron says the row goes somewhere, which a tile said by being a tile */
.cdeck-go{flex:none; width:16px; height:16px; color:var(--ink-faint); display:grid; place-items:center;}
.cdeck-go svg{width:16px; height:16px;}
.cdeck:hover .cdeck-go{color:var(--indigo);}
.cdeck-have{flex:none; font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.07em; text-transform:uppercase; color:var(--indigo); background:var(--indigo-wash); border-radius:5px; padding:3px 6px;}
/* …and below 560px the rating drops under the title rather than squeezing it: the title is the one part of
   the row with no shorter form, which is the rule the review list's own rows already follow */
@media (max-width:560px){
  .cdeck{flex-wrap:wrap;}
  .cdeck-main{flex:1 1 100%;}
  .cdeck-rate{flex:1 1 auto;}
}
.ddetail-head{display:flex; align-items:center; gap:10px; margin-bottom:8px;}
.ddetail-title{margin-bottom:8px;}
.ddetail-sub{font-family:var(--sans); font-size:calc(15px * var(--fs)); color:var(--ink-soft);}
.ddetail-meta{display:flex; flex-wrap:wrap; gap:14px; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink-faint); margin-bottom:12px;}
.ddetail-tags{display:flex; flex-wrap:wrap; gap:6px; margin-bottom:13px;}
/* the author's own paragraph about their deck, under a heading of its own since Aug 2026 — it is the one
   thing on this page written BY the person whose deck it is, and it used to be a loose paragraph among the
   metadata */
.ddetail-desc{max-width:66ch; margin:0 0 15px;}
.ddetail-desc h2{font-family:var(--display); font-size:calc(15px * var(--fs)); font-weight:600; letter-spacing:-.01em; color:var(--ink); margin:0 0 5px;}
.ddetail-desc p{font-family:var(--sans); font-size:calc(14.5px * var(--fs)); line-height:1.65; color:var(--ink-soft); margin:0 0 8px;}
.ddetail-desc p:last-child{margin-bottom:0;}
.ddetail-nodesc p{color:var(--ink-faint); font-style:italic;}
.ddetail-warn{border-left:3px solid var(--ochre,var(--ink-faint)); background:color-mix(in srgb,var(--ochre,#8A6D3F) 7%, transparent); padding:11px 14px; border-radius:0 9px 9px 0; font-family:var(--sans); font-size:calc(13px * var(--fs)); line-height:1.55; color:var(--ink-soft); max-width:66ch; margin-bottom:18px;}
.ddetail-actions{display:flex; flex-wrap:wrap; gap:9px; margin-bottom:28px;}
/* why "Add to my account" is offered over a deck the reader can already see — a note about the account,
   not a warning about the deck, so it takes the quiet ink rather than the ochre rule above it */
.ddetail-adopt{font-family:var(--sans); font-size:calc(13px * var(--fs)); line-height:1.55; color:var(--ink-soft); max-width:66ch; margin:-18px 0 26px;}
.ddetail-sample h2{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.09em; text-transform:uppercase; color:var(--ink-faint); font-weight:600; margin:0 0 12px;}
.creports{margin-top:34px; border:1.5px dashed color-mix(in srgb,var(--zh) 45%, var(--rule)); border-radius:13px; padding:15px 17px;}
.creports-head{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.09em; text-transform:uppercase; color:var(--zh); margin:0 0 11px; font-weight:600;}
.creports-head span{opacity:.65;}
.creport{display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:10px; padding:9px 0; border-top:1px solid var(--rule);}
.creport-main{font-family:var(--sans); font-size:calc(13.5px * var(--fs)); color:var(--ink-soft);}
.creport-note{display:block; font-size:calc(12.5px * var(--fs)); color:var(--ink-faint); margin-top:2px;}
.creport-acts{display:flex; gap:6px;}
/* publish strip in the Studio */
.studio-publish{display:flex; flex-wrap:wrap; align-items:center; justify-content:space-between; gap:11px; padding:12px 15px; margin-bottom:16px; border:1px solid var(--rule); border-radius:12px; background:var(--card);}
.studio-publish.on{border-color:color-mix(in srgb,var(--indigo) 40%, var(--rule)); background:var(--indigo-wash);}
.studio-publish.installed{border-style:dashed;}
.sp-state{font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink-soft); line-height:1.5;}
.sp-link{color:var(--indigo); text-decoration:underline; text-underline-offset:2px;}
.sp-acts{display:flex; gap:7px; flex-wrap:wrap;}
.studio-cardlist-ro .scr-open{cursor:default;}
.udeck-tag{font-size:calc(9px * var(--fs)); letter-spacing:.07em;}

/* ---- a community deck's subdecks (Aug 2026) ----
   THEY ARE CURATED DECK ROWS NOW, in the curated fold (on request: "collapsible with chevrons and
   visually look the same as the curated collections"). They were a flat always-open `.udeck-subs` list of
   quiet hairline rows — a shape nothing else on the Collections page wears, and one that put nine rows
   under a nine-level deck whether the reader wanted them or not. The markup is `buildNode`'s, so the box,
   the border, the hover, the collection hue on the left hairline, the grid fold and the entrance stagger
   are all the curated rules and cannot drift from them; what is left here is the DEPTH INDENT and
   nothing else, a community deck's paths nesting where the curated tree nests its containers. */
/* a subdeck added to the review on its own: named by itself, with its deck for context */
.dk-sup{margin-left:8px; font-family:var(--sans); font-size:calc(10.5px * var(--fs)); color:var(--ink-faint); white-space:nowrap; overflow:hidden; text-overflow:ellipsis;}
/* --sd is the row's DEPTH in the subdeck path, set inline by udeckSubRowsHTML. A margin rather than the
   old padding, because the row is a card of its own now: indenting inside the box would leave every
   level's card starting at the same x with only its words moved, which reads as one flat list. */
.node.udeck-subrow{margin-inline-start:calc(var(--sd, 0) * 20px);}
.node.udeck-subrow .node-title{font-size:calc(15px * var(--fs));}
.udeck-upd{font-size:calc(9px * var(--fs)); letter-spacing:.07em; color:#fff; background:var(--indigo); border-color:var(--indigo);}
.collection.udeck-installed > .collection-row{border-left-color:var(--indigo);}

/* --- Studio: tabs + a deck's own glossary (phase 4) --- */
.studio-tabs{display:flex; gap:4px; margin-bottom:15px; border-bottom:1px solid var(--rule);}
.studio-tab{position:relative; bottom:-1px; padding:9px 15px; border:1px solid transparent; border-bottom:0; border-radius:9px 9px 0 0; background:none; cursor:pointer; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.07em; text-transform:uppercase; color:var(--ink-faint);}
.studio-tab:hover{color:var(--indigo);}
.studio-tab.active{color:var(--ink); background:var(--card); border-color:var(--rule); border-bottom:1px solid var(--card);}
.studio-tab span{opacity:.55; margin-left:5px;}
.gm-picker{display:flex; flex-direction:column; gap:6px; margin-top:4px;}
.gm-opt{display:grid; grid-template-columns:auto 1fr; gap:2px 9px; align-items:baseline; padding:9px 11px; border:1px solid var(--rule); border-radius:9px; cursor:pointer; transition:border-color .18s var(--ease), background .18s var(--ease);}
.gm-opt:hover{border-color:var(--ink-faint);}
.gm-opt.on{border-color:var(--indigo); background:var(--indigo-wash);}
.gm-opt input{grid-row:1/3; align-self:center; accent-color:var(--indigo);}
.gm-label{font-family:var(--sans); font-size:calc(13.5px * var(--fs)); font-weight:600; color:var(--ink);}
.gm-hint{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-faint); line-height:1.45;}
.gm-warn{margin-bottom:14px; padding:10px 13px; border-left:3px solid var(--ochre,var(--ink-faint)); background:color-mix(in srgb,var(--ochre,#8A6D3F) 7%, transparent); border-radius:0 9px 9px 0; font-family:var(--sans); font-size:calc(13px * var(--fs)); line-height:1.55; color:var(--ink-soft);}
/* --- Card types: the Studio's template editor --- */
.ut-form{display:flex; flex-direction:column; gap:12px; padding:2px;}
.ut-about{font-family:var(--sans); font-size:calc(13.5px * var(--fs)); line-height:1.6; color:var(--ink-soft);}
.ut-about p{margin:0 0 10px;}
.ut-builtin{flex:0 0 auto; padding-right:6px; font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink-faint);}
.ut-help{padding:9px 11px; border-radius:9px; background:var(--paper-2); font-family:var(--sans); font-size:calc(12.5px * var(--fs)); line-height:1.6; color:var(--ink-soft);}
.ut-help code{font-family:var(--mono); font-size:calc(11.5px * var(--fs)); padding:1px 4px; border-radius:4px; background:var(--card); color:var(--ink);}
/* a template is code, so it is set as code — and never spell-checked or auto-capitalised */
.ut-code{font-family:var(--mono); font-size:calc(12px * var(--fs)); line-height:1.55; white-space:pre; overflow-wrap:normal; overflow-x:auto; tab-size:2;}
/* The ready-made shapes, shown in the pane an author is already looking at rather than only behind the
   "Add a type" button. Each names the fields it comes with, because that is what tells you whether it is
   the shape you want — a blurb says what it is for, a field list says what you will be filling in. */
.ut-presets{display:grid; grid-template-columns:repeat(auto-fit,minmax(200px,1fr)); gap:10px; margin-top:14px;}
.ut-preset{
  appearance:none; display:flex; flex-direction:column; gap:5px; text-align:left; cursor:pointer;
  padding:12px 13px; border:1px solid var(--rule); border-radius:12px; background:var(--card);
  font-family:var(--sans); color:inherit;
  transition:border-color .14s var(--ease), background .14s var(--ease), transform .14s var(--ease);
}
.ut-preset:hover{border-color:var(--indigo); background:var(--paper-2); transform:translateY(-1px);}
.ut-preset:focus-visible{outline:none; border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash);}
.ut-preset-name{font-family:var(--display); font-weight:600; font-size:calc(15px * var(--fs)); color:var(--ink);}
.ut-preset-blurb{font-size:calc(12.5px * var(--fs)); line-height:1.5; color:var(--ink-soft);}
.ut-preset-fields{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.05em; color:var(--ink-faint);}
.ut-preview{display:flex; flex-direction:column; gap:5px;}
.ut-preview-box{padding:10px; border:1px dashed var(--rule); border-radius:11px; background:var(--paper-2);}
/* The wrapper a type's own CSS is scoped to. It sets nothing but the containing block, so an author starts
   from the page's own typography rather than from a box that has already made decisions for them — and
   `position:relative` is what keeps an absolutely-positioned rule inside the card it belongs to. */
.uc-card{position:relative;}
/* A back template that renders {{FrontSide}} draws the question itself, so the shell must not draw it a
   second time above the answer — Anki's rule, where the back replaces the whole card, and the reason the
   vocabulary and picture shapes set a rule under it. Without this the word, its part of speech and its
   closed blanks all print twice, one under the other. A template that does NOT ask for the front is
   untouched: its question stays above the answer, as a Basic card's does, which is what keeps a reader's
   graded cloze guess on the page beside what it should have been. */
/* THE "QUESTION" HEADING GOES WITH THE QUESTION IT HEADS, and this half of the rule had stopped
   matching (Sep 2026, on a bug report that a Chinese card's question "turns invisible" once the marker
   is down). It was written `> .label`, which was right when the label was a direct child of the card;
   the q-head restructure moved it to `.q-head > .q-lead > .label`, so the selector silently addressed
   nothing. Nothing looked wrong while it was broken, because the heading landed directly above the
   front the back redraws — until the WRITING BAND opened between the two, at which point the card read
   as "Question" over nine centimetres of nothing, with the question itself pushed below the band. The
   band is right to stay where it is (a reader's working is on it, and the ink is in page coordinates,
   so moving the band would orphan it); the heading is what had to go. */
.study-card:has(.uc-back.uc-hasfront) > .q-head .label,
.study-card:has(.uc-back.uc-hasfront) > .question{display:none;}
/* …EXCEPT WHILE THE MARKER IS DOWN (Sep 2026, on a bug report: "for Mandarin cards, when the answer is
   revealed, the extra marker space currently appears above the question instead of staying below it").
   The writing band (`.scratch`) sits between the shell's question and the reveal, so hiding the shell's
   copy and letting the back redraw the front put the question BELOW the band. With the pen down the
   swap runs the other way: the shell's question and its heading stay where they were when the card
   opened, above the band, and the back's redrawn front (`.uc-frontside`, see cardTypeFieldGetter) is
   the copy that goes — together with the rule the template draws under it. Nothing moves at the reveal,
   which is what keeps the reader's ink over the working it was written on. */
body.wb-down .study-card:has(.uc-back.uc-hasfront) > .q-head .label{display:flex;}
body.wb-down .study-card:has(.uc-back.uc-hasfront) > .question{display:block;}
body.wb-down .study-card .uc-back .uc-frontside,
body.wb-down .study-card .uc-back .uc-frontside + hr{display:none;}
/* ---------- the two behaviours Folio lends a card type ----------
   A type's own CSS is scoped to its own cards, so anything a type needs to LOOK like it must style itself.
   These two are different: they are things Folio DOES — reading a run of text aloud, and closing a blank —
   so their base appearance ships here, unscoped, and works in any type without being written out again. A
   type's own stylesheet can still override either, since its selectors are more specific. */
/* A run of text a card type has marked to be heard: <span class="uc-tts">{{Word}}</span>. The speaker is a
   mask rather than an <img> or an inline <svg> — a template's HTML is sanitized and cannot carry an svg, and
   a mask takes `currentColor`, so the icon follows the theme, the night mode and the high-contrast mode with
   nothing further to keep in step. */
.uc-tts{
  --uc-spk:url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M11 5 6 9H2v6h4l5 4z'/%3E%3Cpath d='M15.5 8.5a5 5 0 0 1 0 7'/%3E%3Cpath d='M19 5a9 9 0 0 1 0 14'/%3E%3C/svg%3E");
  display:inline-flex; align-items:center; gap:.42em; cursor:pointer;
  padding:.1em .5em .1em .45em; border-radius:8px;
  border:1px solid var(--rule); background:var(--paper-2); color:inherit;
  transition:border-color .14s var(--ease), background .14s var(--ease), box-shadow .14s var(--ease);
}
.uc-tts::before{
  content:""; flex:none; width:.92em; height:.92em; background-color:currentColor;
  -webkit-mask:var(--uc-spk) center/contain no-repeat; mask:var(--uc-spk) center/contain no-repeat;
}
.uc-tts:hover{border-color:var(--indigo); background:var(--indigo-wash);}
.uc-tts:focus-visible{outline:none; border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash);}
.uc-tts.tts-playing{border-color:var(--indigo); background:var(--indigo-wash); box-shadow:0 0 0 3px var(--indigo-wash);}
/* No speech engine on this device → the words stay words. A control that answers a press with nothing is
   worse than no control, and the text it wraps is the answer the card is about, so it must still read. */
body.no-tts .uc-tts{display:inline; padding:0; border:0; background:none; cursor:auto; box-shadow:none;}
body.no-tts .uc-tts::before{display:none;}
/* A cloze blank. Closed on the front, and on the back the same words in the same place, marked — a reader
   comparing the two is looking for exactly where the sentence changed. */
.uc-cloze{color:var(--indigo); font-weight:600;}
.uc-cloze-on{
  color:inherit; background:var(--indigo-wash); border-radius:5px; padding:0 .18em;
  box-shadow:inset 0 -.42em 0 var(--indigo-wash);
}
/* A blank this card is NOT asking about, on a type that makes one card per blank. It reads as ORDINARY
   PROSE — no colour, no weight — because that is exactly what it is here: the rest of the sentence, which is
   the context the reader is recalling from. It keeps a class of its own only so a type can style it if it
   wants to; without this rule it would inherit .uc-cloze's indigo and every card would look as though it
   were asking about every blank at once, which is the behaviour this replaced. */
.uc-cloze-other{color:inherit; font-weight:inherit;}
.ces-typebar{display:flex; align-items:center; gap:12px; flex-wrap:wrap; margin-bottom:12px;}
.ces-typepick{display:flex; align-items:center; gap:8px;}
.ces-typepick span{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.07em; text-transform:uppercase; color:var(--ink-faint);}
.ces-typepick .af-input{width:auto; min-width:220px;}
.ces-typenote{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-faint);}

.gloss-edit-form{display:flex; flex-direction:column; gap:12px; padding:2px;}
.gloss-desc-edit{min-height:120px; line-height:1.6; font-family:var(--sans); font-size:calc(14px * var(--fs)); white-space:pre-wrap;}
.gloss-desc-edit:focus{outline:2px solid var(--indigo-bright); outline-offset:1px;}

/* --- Ratings, reviews and staff picks (phase 3) --- */
.stars{display:inline-flex; gap:1px; line-height:1; vertical-align:middle;}
.star{color:var(--rule); font-size:calc(13px * var(--fs));}
.star.on{color:#C39A2E;}
.star.half{background:linear-gradient(90deg,#C39A2E 50%,var(--rule) 50%); -webkit-background-clip:text; background-clip:text; color:transparent;}
body.night .star.on{color:#E6C765;}
body.night .star.half{background:linear-gradient(90deg,#E6C765 50%,var(--rule) 50%); -webkit-background-clip:text; background-clip:text;}
.stars-n{margin-left:5px; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.04em; color:var(--ink-faint); vertical-align:middle;}
.stars-none{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint);}
.cdeck-rate{flex:none; display:flex; align-items:center;}
.cdeck-pick{flex:none; font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:#8A6D3F; background:color-mix(in srgb,#C39A2E 16%, transparent); border-radius:5px; padding:3px 6px;}
body.night .cdeck-pick{color:#E6C765;}
.btn.tiny.on{background:var(--indigo); color:#fff; border-color:var(--indigo);}
.ddetail-pick{display:inline-block; margin-bottom:10px; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.07em; text-transform:uppercase; color:#8A6D3F; background:color-mix(in srgb,#C39A2E 15%, transparent); border-radius:7px; padding:6px 10px;}
body.night .ddetail-pick{color:#E6C765;}
.ddetail-fork{font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink-faint); margin:0 0 12px;}
.ddetail-stars{display:inline-flex; align-items:center;}
.ddetail-reviews,.ddetail-more{margin-top:34px;}
.ddetail-reviews h2,.ddetail-more h2{font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.09em; text-transform:uppercase; color:var(--ink-faint); font-weight:600; margin:0 0 13px;}
.rv-summary{display:flex; flex-wrap:wrap; gap:26px; align-items:center; margin-bottom:16px;}
.rv-big{display:flex; align-items:center; gap:9px;}
.rv-avg{font-family:var(--display); font-size:calc(34px * var(--fs)); font-weight:600; line-height:1; color:var(--ink);}
.rv-count{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint);}
.rv-dist{display:flex; flex-direction:column; gap:3px; min-width:190px;}
.rv-bar{display:flex; align-items:center; gap:7px;}
.rv-bar-k{font-family:var(--mono); font-size:calc(10px * var(--fs)); color:var(--ink-faint); width:22px;}
.rv-bar-track{flex:1; height:6px; border-radius:3px; background:var(--rule); overflow:hidden;}
.rv-bar-fill{display:block; height:100%; background:#C39A2E; border-radius:3px;}
body.night .rv-bar-fill{background:#E6C765;}
.rv-bar-n{font-family:var(--mono); font-size:calc(10px * var(--fs)); color:var(--ink-faint); width:20px; text-align:right;}
.rv-note{font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink-soft); background:var(--card); border:1px dashed var(--rule); border-radius:10px; padding:11px 14px; margin:0 0 16px;}
.rv-why{display:block; font-size:calc(12.5px * var(--fs)); color:var(--ink-faint); margin-top:3px;}
.rv-form{border:1px solid var(--rule); border-radius:12px; padding:14px 16px; margin-bottom:18px; background:var(--card);}
.rv-form-head{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.07em; text-transform:uppercase; color:var(--ink-faint); margin-bottom:8px;}
.rv-pick{display:flex; gap:3px; margin-bottom:10px;}
.rv-star{background:none; border:0; cursor:pointer; font-size:calc(26px * var(--fs)); line-height:1; color:var(--rule); padding:0 2px; transition:color .12s var(--ease), transform .12s var(--ease);}
.rv-star:hover{transform:scale(1.12);}
.rv-star.on{color:#C39A2E;}
body.night .rv-star.on{color:#E6C765;}
.rv-text{width:100%; resize:vertical; margin-bottom:10px;}
.rv-form-acts{display:flex; gap:7px; flex-wrap:wrap;}
/* the comments have a heading and a count of their own (Aug 2026, on request), so the list reads as a
   comments section rather than as an unlabelled run under "Ratings" */
.rv-listhead{font-family:var(--display); font-size:calc(15px * var(--fs)); font-weight:600; letter-spacing:-.01em; color:var(--ink); margin:18px 0 9px; display:flex; align-items:baseline; gap:8px;}
.rv-listhead span{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:var(--ink-faint);}
.rv-list{display:flex; flex-direction:column; gap:11px;}
.rv-item{border-top:1px solid var(--rule); padding-top:11px;}
.rv-item.own{border-left:3px solid var(--indigo); padding-left:11px; border-top-color:transparent;}
.rv-head{display:flex; flex-wrap:wrap; align-items:center; gap:9px; margin-bottom:4px;}
.rv-who{font-family:var(--sans); font-size:calc(13px * var(--fs)); font-weight:600; color:var(--ink);}
.rv-when{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint);}
.rv-body{font-family:var(--sans); font-size:calc(14px * var(--fs)); line-height:1.6; color:var(--ink-soft); margin:0; max-width:66ch;}

/* ============================================================
   THE LIBRARY — the shelf of books, and one book's reading room
   ============================================================
   The Collections page is a list of DECKS; this is a list of BOOKS, and it should not read like the
   same page twice. A book tile is a portrait card with a coloured spine down its left edge, laid out
   two to a row where the game grid runs three — a blurb and a title need the width, and a shelf of
   six narrow tiles reads as a menu rather than as books. */
/* ONE BANNER PER ROW, full width, at every size (Aug 2026, on request). It was 320px tiles two to a
   row with a paragraph of blurb in each, then briefly 210px tiles; both are gone. What stays from that
   pass is the BLURB being gone — a book is chosen from its author, title and date, and what it is
   about is a tap away in its own front matter — which is what lets a full-width banner still be short.
   Reading left to right rather than top to bottom is the other half of that. */
.book-grid{display:grid; grid-template-columns:1fr; gap:10px; margin-bottom:22px;}
/* The shelf's tools row: the search box on the left, taking whatever width is going, and the two sort
   controls held to the right. It was `flex-end` with only the sort in it; a search box has to be the wide
   one, since a field narrow enough to fit beside two controls holds about four characters. */
.lib-tools{display:flex; justify-content:flex-end; align-items:center; gap:10px; flex-wrap:wrap; margin-bottom:12px;}
.lib-search{flex:1 1 220px; min-width:180px;}
.lib-search input{
  width:100%; font-family:var(--sans); font-size:calc(13.5px * var(--fs)); padding:9px 12px;
  border:1px solid var(--rule); border-radius:9px; background:var(--card); color:var(--ink);
  transition:border-color .16s var(--ease), box-shadow .16s var(--ease);
}
.lib-search input:focus{outline:none; border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash);}
/* How many books the shelf is showing. A quiet mono figure beside the search box that changes it — it is a
   fact about the list, not a control, so it takes the caption ink and no border. `flex:0 0 auto` keeps it
   whole: it is the one item in this row with no shorter form, and the search box is the elastic one. */
.lib-count{
  flex:0 0 auto; font-family:var(--mono); font-size:calc(11.5px * var(--fs)); letter-spacing:.02em;
  color:var(--ink-faint); white-space:nowrap;
}
/* Nothing matched. A line rather than an empty shelf, which reads as a page that failed to draw. */
.lib-none{
  padding:26px 18px; text-align:center; border:1px dashed var(--rule); border-radius:14px;
  background:var(--paper-2); font-family:var(--sans); font-size:calc(14px * var(--fs)); color:var(--ink-soft);
}
.lib-none p{margin:0;}
/* ---------- the shelf's two sections: favourites, then the rest (Aug 2026, on request) ----------
   Headings appear only once something has been starred — an unstarred shelf is the page it always was,
   and a lone "Everything else" over the only section there is would be a heading naming nothing. A
   starred book is NOT repeated below, so the lower heading is honest as written. */
.lib-sec{display:block;}
.lib-sec-head{
  margin:0 0 9px; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.16em;
  text-transform:uppercase; font-weight:600; color:var(--ink-faint);
}
.lib-sec + .lib-sec .lib-sec-head{margin-top:4px;}
/* The star a favourite wears: a MARK on the banner, not a control — the way to set and clear one is the
   long-press sheet, and a second target 8px from the first is two answers to one question. It sits in
   the banner's top-right corner, out of the way of the reading-progress line below it. */
.bk-star{position:absolute; top:9px; right:11px; width:14px; height:14px; color:var(--ochre); pointer-events:none;}
.bk-star svg{display:block; width:100%; height:100%;}
.book-tile.bk-fav .bk-tile-foot{padding-right:20px;}   /* …so the star never lands on the length line */
/* THE BANNER CARRIES ITS BOOK'S COLOUR AS A WASH (Aug 2026, on request — "similar to the collection
   banners"). It was a flat `var(--card)` with the hue stated only on the 8px spine, which on a shelf of
   twenty books left every banner looking alike until the eye reached the spine. The gradient is the
   COLLECTION banner's own bookplate treatment, written the way `.active-deck` writes it — a wash of the
   accent laid OVER var(--card) rather than mixed into it, so the fade target is whatever paper the theme
   happens to be using and no theme needs a rule of its own. The accent is `--tile`, the book's AUTHOR'S
   colour (bookColor in app.js), so two books by one hand wash the same, exactly as their spines do.
   Night wants a weaker mix than daylight, for the reason the collection banner's pair does: the hue sits
   on a dark card and reads twice as strongly there. */
/* THE BANNER PAINTS FROM `--bk-accent`, NOT FROM `--tile` DIRECTLY, AND ON A DARK PAPER THAT IS A
   LIGHTER COLOUR (Aug 2026, on a bug report: "in night mode some of the books' authors and 'Start
   reading' are too dark to read on the dark background").
   The author colours in BOOK_AUTHOR_COLOR were chosen inside a deliberately dark lightness band and
   measured against the SIXTEEN LIGHT papers alone — every one of them reads 1.5–3.0 on the dark ones,
   which their own notes in app.js say outright. That is right for a SPINE seen against white and wrong
   the moment the same value is set as text on `--card` at #26262C: the Book of Rites' dark plum reads
   1.08:1 there, which is not low contrast but no contrast at all, and the spine that identifies the book
   disappears with the words. So night lightens the accent for the whole banner rather than re-toning six
   rules by hand, and it is done as a MIX rather than a second table of colours, so a book added later is
   covered by having a colour at all.
   45% is measured, not guessed: it is the strongest mix at which the darkest swatch on the shelf still
   clears 4.5:1 on the lightest night `--card` (#460030 → 5.0:1; the whole shelf lands 5.0–6.5; at 50% it
   falls to 4.3 and misses). It keeps each hue and only takes chroma, so two books by one hand still wash,
   spine and read alike — which is the whole point of the colour.
   `--tile` is set INLINE on the element (bookColor in app.js), so it cannot be overridden from here; a
   derived property can, which is why the indirection exists at all. */
.book-tile, .gloss-book{
  --bk-accent:var(--tile, var(--indigo));
  position:relative; display:flex; gap:0; text-align:left; padding:0; overflow:hidden;
  background:linear-gradient(105deg, color-mix(in srgb, var(--bk-accent) 34%, transparent), transparent 64%) var(--card);
  border:1px solid var(--rule); border-radius:14px;
  box-shadow:var(--shadow-sm); cursor:pointer; font:inherit; color:inherit;
  transition:transform .14s var(--ease), box-shadow .14s var(--ease), border-color .14s var(--ease), background-color .14s var(--ease);
}
body.night .book-tile, body.night .gloss-book{
  --bk-accent:color-mix(in srgb, var(--tile, var(--indigo)) 45%, #FFFFFF);
  background:linear-gradient(105deg, color-mix(in srgb, var(--bk-accent) 20%, transparent), transparent 64%) var(--card);
}
/* MORE COLOUR AT BOTH ENDS, AND IT IS CHROMA THAT WAS MISSING RATHER THAN LIGHTNESS (Aug 2026, on a
   report: the banners read "too dark in day mode and too whited out in dark mode").
   Both faults are the same one seen from either side. The swatches were chosen dark and muted, so at
   full strength on white the hue barely declares itself; and lifting them for night by MIXING WITH WHITE
   raises lightness by taking chroma away — measured over all 28, `color-mix(… 45%, #FFF)` cuts the Book
   of Rites' chroma from 11.1 to 4.9 and Augustine's from 7.1 to 2.7, which is not a lighter plum and a
   lighter brown but two greys.
   So the accent is now derived in OKLCh, where lightness and chroma are separate axes: day keeps the
   lightness it always had and takes 50% more chroma, night sets lightness to 0.74 and takes 30% MORE
   chroma rather than less. Measured against every theme's `--card`, this is better on contrast as well as
   on colour — day's worst case goes 3.88 → 3.96 (gazette's paper; 4.85 → 4.94 on white, which is what
   the a11y test measures) and night's 5.03 → 5.62. The gradient washes go up with them, 26% → 34% by day
   and 15% → 20% at night, since a wash of a greyish colour is what made the banner itself look flat.
   It is gated on `@supports` rather than layered as a fallback declaration, because a custom property
   accepts ANY token stream at parse time: an unsupported `oklch(from …)` would not be dropped in favour
   of the declaration above it, it would poison every property that reads it. Browsers without relative
   colour keep exactly the two rules above. */
@supports (color: oklch(from red l c h)){
  .book-tile, .gloss-book{--bk-accent:oklch(from var(--tile, var(--indigo)) calc(l - 0.01) calc(c * 1.5) h);}
  body.night .book-tile, body.night .gloss-book{--bk-accent:oklch(from var(--tile, var(--indigo)) 0.74 calc(c * 1.3) h);}
}
/* A banner is HELD for its options sheet, so holding it must not select the title under the finger, and on
   iOS must not raise the platform's own callout — the guard the daily review's rows already carry. */
.book-tile{-webkit-user-select:none; user-select:none; -webkit-touch-callout:none;}
.book-tile:hover{transform:translateY(-3px); box-shadow:var(--shadow-lg); border-color:color-mix(in srgb,var(--bk-accent) 45%, var(--rule));}
.book-tile:active{transform:translateY(0) scale(.995);}
.book-tile:focus-visible{outline:2px solid var(--bk-accent); outline-offset:3px;}
/* the spine: where the book's colour is stated boldest, so the shelf reads as a shelf. The colour is
   its AUTHOR'S (BOOK_AUTHOR_COLOR in app.js), so two books by one hand carry one spine — which is why
   the author line below takes it too, and the title does not. */
.bk-spine{flex:0 0 8px; align-self:stretch; background:var(--bk-accent); position:relative;}
.bk-spine::after{content:""; position:absolute; inset:0 2px; border-left:1px solid rgba(255,255,255,.25); border-right:1px solid rgba(0,0,0,.12);}
/* the banner's own row: who and what on the left, how long and where you got to on the right */
.bk-tile-body{display:flex; flex-flow:row wrap; align-items:center; gap:6px 18px; padding:13px 18px; min-width:0; flex:1;}
.bk-tile-main{display:flex; flex-direction:column; gap:2px; min-width:0; flex:1 1 220px;}
/* THE AUTHOR LINE IS THE ONE PLACE THE ACCENT IS ASKED TO BE READ AS TYPE, AND IT WAS THE SMALLEST TYPE
   ON THE BANNER (Aug 2026, on a report: "the authors' names are sometimes quite hard to read in their
   book banners"). Three things were against it at once and each is worth naming, because fixing only the
   obvious one would have left it hard to read.
   · SIZE AND WEIGHT. 9.5px of uppercase MONOSPACE at the face's default weight is the least legible
     combination on the site — a mono face gives a capital I and a capital M the same width, so the word
     shapes that carry a name are flattened, and uppercase removes the ascenders that carry them too.
     11px at 600 restores the stroke without touching the eyebrow grammar the shelf is built on; the
     tracking stays at .08em, which HELPS uppercase rather than hurting it, and is not the fault here.
   · CONTRAST — and this is why the report says "sometimes". The colour is the book's AUTHOR'S, so how
     legible the line is depends on which book it is: measured over all 28 author colours against all six
     light and six dark papers, the day accent falls below 4.5:1 on 6 of those 168 pairings (worst 3.88,
     Marcus Aurelius's teal on the gazette paper), while night — already lightened for its own bug report
     below — clears it everywhere at 4.84 and up. Small text needs the 4.5 bar, so those six are exactly
     the books a reader would call hard to read, and no amount of extra weight fixes a colour.
   · SO THE TEXT TAKES ITS OWN DERIVED COLOUR, `--bk-ink`, mixed 80% accent into `var(--ink)` — toward
     the paper's own ink, which DARKENS by day and LIGHTENS at night without needing to know which it is.
     80% is measured, not guessed: it is the lightest mix that clears 4.5:1 on every one of those 168
     pairings (worst 5.11 by day, 5.64 at night; at 85% the gazette pairing scrapes 4.76 and the headroom
     goes). It keeps the hue almost entirely, so two books by one hand still read alike — which is the
     whole point of the colour — and it is deliberately NOT applied to the spine, the wash or the reading
     bar, which are shapes rather than words and are legible at the accent's own strength.
   It is declared on `.book-tile` rather than beside the rule below so it derives from whichever
   `--bk-accent` won — the base one, the night one, or the OKLCh one in the @supports block. */
.book-tile{--bk-ink:color-mix(in srgb, var(--bk-accent) 80%, var(--ink));}
.bk-tile-author{font-family:var(--mono); font-size:calc(11px * var(--fs)); font-weight:600; letter-spacing:.08em; text-transform:uppercase; color:var(--bk-ink);}
.bk-tile-title{font-family:var(--display); font-size:calc(19px * var(--fs)); font-weight:600; line-height:1.2; color:var(--ink);}
.bk-tile-sub{font-family:var(--serif); font-style:italic; font-size:calc(12.5px * var(--fs)); color:var(--ink-soft); margin-top:-2px;}
/* When it was written — the one fact about a work of history a reader wants before opening it, and
   what took the blurb's place on the banner (Aug 2026, on request). */
.bk-tile-when{font-family:var(--serif); font-style:italic; font-size:calc(13px * var(--fs)); color:var(--ink-soft);}
.bk-tile-foot{display:flex; flex-direction:column; align-items:flex-end; gap:3px; flex:none; min-width:0;}
.bk-tile-meta{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.05em; text-transform:uppercase; color:var(--ink-faint);}
/* THE ORIGINAL-LANGUAGE CHIP (Aug 2026, on request): between how long the book is and the way into it,
   and drawn only on the twenty books that actually carry a facing original. It takes `--bk-accent` — the
   book's own colour, lightened for the paper in use — so it belongs to its banner rather than reading as
   a system label, and it is an OUTLINE rather than a filled pill: "Start reading" directly under it is
   the banner's one call to action and is set in the same accent, and two solid marks in one colour a
   line apart would make the reader look twice to see which of them is the control. */
.bk-tile-orig{
  font-family:var(--sans); font-size:calc(9.5px * var(--fs)); font-weight:600; letter-spacing:.03em;
  line-height:1; padding:3px 7px 4px; border-radius:999px; flex:none; white-space:nowrap;
  color:var(--bk-accent);
  border:1px solid color-mix(in srgb, var(--bk-accent) 42%, transparent);
  background:color-mix(in srgb, var(--bk-accent) 9%, transparent);
}

/* How many people have read the book (Sep 2026). Quiet and monospaced like every other figure on the
   site — it is a footnote on the banner, not a badge: the tile's own chips say what the book IS, and a
   readership count competing with them would read as a claim about the work. */
.bk-tile-reads{
  font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.04em;
  line-height:1; flex:none; white-space:nowrap; color:var(--ink-faint);
}
.bk-tile-new{font-family:var(--sans); font-size:calc(11.5px * var(--fs)); font-weight:600; color:var(--bk-accent);}
/* where the reader left off — the tile's one changing fact, so it takes the accent colour */
.bk-tile-resume{font-family:var(--sans); font-size:calc(11.5px * var(--fs)); font-weight:600; color:var(--bk-accent);}
/* The reading bar as the banner's own bottom EDGE (Aug 2026, on request — it was a short bar inside the
   right-hand block). Absolutely positioned, so it takes no height from the row and the banner stays as
   short as it is without one; .book-tile is already `position:relative` and `overflow:hidden`, so it
   follows the banner's rounded corners. */
.bk-tile-bar{position:absolute; left:0; right:0; bottom:0; height:3px; background:var(--rule); overflow:hidden;}
.bk-tile-bar span{display:block; height:100%; background:var(--bk-accent); border-radius:2px;}
.lib-note{font-family:var(--sans); font-size:calc(13px * var(--fs)); line-height:1.6; color:var(--ink-soft);
  background:var(--card); border:1px dashed var(--rule); border-radius:12px; padding:13px 16px; max-width:70ch;}

/* ---------- one book ---------- */
.bk-head{margin-bottom:14px;}
.bk-back{background:none; border:0; padding:0; cursor:pointer; font:inherit; color:var(--indigo); letter-spacing:inherit;}
.bk-back:hover{text-decoration:underline;}
.bk-byline{font-family:var(--serif); font-style:italic; font-size:calc(14.5px * var(--fs)); color:var(--ink-soft);}

/* The chapter menu bar. Seneca ships 65 chapters and has 124 to come, so the bar SCROLLS rather than
   wrapping: a wrapped grid of 124 buttons is not a menu bar, it is the page. The arrows step one at a
   time and the ☰ opens the whole contents for a jump. */
/* The bar and its contents panel are ONE sticky block, so the panel can never be left behind at the top
   of the document while the bar that opens it rides with the reader. Being sticky, the wrapper is a
   positioned element and therefore the containing block the panel hangs from. */
.bk-barwrap{position:sticky; top:calc(var(--bar-h) + 4px); z-index:20; margin-bottom:16px;}
/* A highlight made from the reader's own selection (see BOOK HIGHLIGHTS). The colour is set inline —
   it is one of the marker's five and is already an rgba by the time it arrives — so all this carries is
   the shape. `box-decoration-break:clone` keeps the rounding on every line a highlight wraps onto
   instead of only its first and last. */
.bk-hl{border-radius:2px; padding:.05em 0; -webkit-box-decoration-break:clone; box-decoration-break:clone;}
.bk-bar{
  display:flex; align-items:center; gap:6px; padding:7px 8px;
  background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm);
}
.bk-nav{
  flex:0 0 auto; display:grid; place-items:center; width:32px; height:32px; padding:0;
  background:none; border:1px solid var(--rule); border-radius:8px; cursor:pointer; color:var(--ink-soft);
  transition:background .12s var(--ease), color .12s var(--ease), border-color .12s var(--ease);
}
.bk-nav svg{width:16px; height:16px;}
.bk-nav:hover:not(:disabled){background:var(--paper); color:var(--ink); border-color:var(--indigo);}
.bk-nav:disabled{opacity:.35; cursor:default;}
/* the "?" that brings the first-open card back. It is the only .bk-nav whose face is a CHARACTER rather
   than an svg, so it needs the type stated — a button falls back to the UA font otherwise. It stands last
   in the bar, past the controls it explains, and quiet until pointed at. */
.bk-help-btn{font-family:var(--mono); font-size:calc(14px * var(--fs)); line-height:1; opacity:.7;}
.bk-help-btn:hover{opacity:1;}
.bk-tabs{
  flex:1 1 auto; display:flex; gap:5px; overflow-x:auto; scroll-behavior:smooth;
  scrollbar-width:thin; padding:1px 0;
}
.bk-tabs::-webkit-scrollbar{height:5px;}
.bk-tabs::-webkit-scrollbar-thumb{background:var(--rule); border-radius:3px;}
.bk-tab{
  flex:0 0 auto; display:flex; align-items:baseline; gap:6px; max-width:230px;
  padding:6px 11px; background:none; border:1px solid transparent; border-radius:8px;
  cursor:pointer; font:inherit; color:var(--ink-soft); white-space:nowrap;
  transition:background .12s var(--ease), color .12s var(--ease), border-color .12s var(--ease);
}
.bk-tab:hover{background:var(--paper); color:var(--ink);}
.bk-tab.on{background:var(--indigo); border-color:var(--indigo); color:#FFFFFF;}
.bk-tab:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.bk-tab-n{font-family:var(--mono); font-size:calc(11px * var(--fs)); opacity:.8;}
.bk-tab-t{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); overflow:hidden; text-overflow:ellipsis;}
/* The front matter's tab wears a book rather than a number: "0" beside 1, 2, 3 reads as a chapter the
   author did not write, and on a phone the titles are hidden and the number is all there is. It is
   also pinned to the left of the scroller, so the way back to the front matter is always in view —
   otherwise it is 60 tabs behind wherever the reader has got to. */
.bk-tab-intro{position:sticky; left:0; z-index:1; background:var(--card); padding-left:8px;}
.bk-tab-intro.on{background:var(--indigo);}
.bk-tab-intro .bk-tab-n{display:flex; align-items:center; opacity:1;}
.bk-glyph{width:14px; height:14px; display:block;}
/* in the contents row the number cell is inline text, so the glyph rides the baseline rather than
   turning that cell into a flex container (which would move .bk-toc-item's own baseline alignment) */
.bk-toc-n .bk-glyph{display:inline-block; vertical-align:-2px;}

/* Hung off the bar rather than laid out under it: it opens where the button is, at any scroll depth, and
   overlays the prose instead of shoving it down the page. The ceiling is whichever is smaller — half the
   screen, or what is actually left between the pinned bar and the bottom bar. */
.bk-toc{
  position:absolute; top:calc(100% + 6px); left:0; right:0; z-index:1;
  border:1px solid var(--rule); border-radius:12px; background:var(--card);
  padding:14px 16px; margin:0; overflow-y:auto;
  max-height:min(52vh, calc(100vh - var(--bar-h) - var(--tabbar-h) - 96px));
  box-shadow:var(--shadow-lg);
}
.bk-toc[hidden]{display:none;}   /* the author display beats the UA rule — cf. .ces-imgpanel */
.bk-toc-part + .bk-toc-part{margin-top:14px; padding-top:12px; border-top:1px solid var(--rule);}
/* the front matter sits above the volumes and belongs to none of them, so it has no heading of its own */
.bk-toc-front{margin-bottom:10px;}
.bk-toc-part h4{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint); margin:0 0 8px;}
.bk-toc-part h4 span{text-transform:none; letter-spacing:.02em; opacity:.8;}
.bk-toc-list{display:grid; grid-template-columns:repeat(auto-fill,minmax(230px,1fr)); gap:2px;}
.bk-toc-item{
  display:flex; gap:9px; align-items:baseline; text-align:left; padding:6px 9px;
  background:none; border:0; border-radius:7px; cursor:pointer; font:inherit;
  font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink-soft);
}
.bk-toc-item:hover{background:var(--paper); color:var(--ink);}
.bk-toc-n{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:var(--ink-faint); min-width:22px; text-align:right;}

/* The reading surface. This is the one page on the site that is nothing but prose, so it is set for
   READING: a serif, a long measure capped in ch, and generous leading. */
.bk-page{background:var(--card); border:1px solid var(--rule); border-radius:14px; padding:26px 30px 22px; box-shadow:var(--shadow-sm);}
.bk-ch-head{margin-bottom:18px; padding-bottom:14px; border-bottom:1px solid var(--rule);}
.bk-ch-part{display:block; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint);}
.bk-ch-n{display:block; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--indigo); margin-top:3px;}
.bk-ch-t{font-family:var(--display); font-size:calc(27px * var(--fs)); font-weight:600; line-height:1.22; color:var(--ink); margin:4px 0 0;}
.bk-prose{font-family:var(--serif); font-size:calc(17px * var(--fs)); line-height:1.78; color:var(--ink); max-width:68ch;}
.bk-prose p{margin:0 0 1.05em;}
.bk-prose blockquote{margin:1.2em 0; padding-left:18px; border-left:2px solid var(--rule); font-style:italic; color:var(--ink-soft);}
/* The leading unnumbered line of a chapter, in both columns: Seneca's salutation, and the chapter
   head of a book that prints one (the Art of War's "I. Laying Plans." beside its "I. 計篇."). Two
   names for one role — a line that belongs to the chapter rather than to any section of it, and that
   therefore pairs across the two languages — kept apart so that a chapter title is not marked up as
   somebody's greeting. Both are written on by the importer AFTER the tag stripper, which drops
   attributes from every tag it keeps. */
.bk-salut,.bk-head{font-style:italic; color:var(--ink-soft);}
/* Gummere's section numbers — how this text is cited, so they are kept and set apart rather than
   folded into the prose */
.bk-prose .bk-n{
  font-family:var(--mono); font-size:calc(11px * var(--fs)); color:var(--ink-faint);
  vertical-align:.28em; margin-right:5px; user-select:none;
}
/* ---------- A PLAY ----------
   Oedipus Rex is the first work here that is performed rather than read, and the two things that
   makes necessary are both about telling KINDS of text apart. In a play the speaker's name is not
   furniture around the text, it is part of it — a page that renders it as an ordinary word turns a
   drama into an unattributed argument — and a stage direction is not spoken at all.

   A speech is one paragraph per section, so a long speech is several and only the FIRST of them
   carries the name (bookSections cuts a block at each marker, and repeating the speaker at every
   line number would read as the character starting again). `.bk-cont` is the rest of that speech,
   indented to hang under the name rather than under the margin, so the eye can see the speech is
   still running. Below the two-column breakpoint that indent is dropped — at a phone's measure it
   costs more than it says.

   The name is INLINE, run into the head of the line the way a printed play sets it, rather than
   standing on a line of its own. Two reasons, and the first is structural: it has to follow the
   section marker in the markup (see dramaHtml in .claude/fetch-book.js — a name before the marker is
   given to the previous section), and a block element after an inline number would strand that
   number alone on the line above. The second is that this text is cut into a section every couple of
   lines, so a name on its own line would roughly double the length of the page. */
.bk-prose .bk-sp{margin:0 0 .62em;}
.bk-prose .bk-who{
  font-family:var(--mono); font-size:calc(10.5px * var(--fs)); font-weight:600;
  letter-spacing:.09em; text-transform:uppercase; color:var(--tile,var(--indigo));
  margin-right:.5em; white-space:nowrap;
}
.bk-prose .bk-cont{padding-left:1.6em;}
/* Jebb's own, and the Greek carries none — the ancient text records no directions, so every one in a
   modern edition is its editor's inference. Set as a different kind of thing from the spoken line,
   and centred away from the margin so it cannot be mistaken for one. */
.bk-prose .bk-stage{
  font-style:italic; color:var(--ink-soft); font-size:calc(15.5px * var(--fs));
  margin:1.1em 0; padding-left:1.6em; max-width:56ch;
}
/* In two columns the stage direction sits beside an empty Greek cell, so it keeps the row's own
   spacing rather than opening a gap the other side cannot match. */
.bk-bi .bk-prose .bk-stage,.bk-bi .bk-stage{margin:.5em 0;}
/* THE ORIGINAL BESIDE THE TRANSLATION.
   The page is always built as paired rows once the original is loaded, and these rules decide which
   columns are shown — so turning the original on is a class change rather than a re-render, which is
   what lets the reader's place survive the switch exactly. `data-lang` is set by applyLangMode(). */
.bk-bi{max-width:none;}                       /* two columns need the width the single column caps */
.bk-bi .bk-row{display:grid; gap:0 34px; align-items:start;}
.bk-bi .bk-col{min-width:0;}
.bk-bi .bk-col > :last-child{margin-bottom:0;}
.bk-bi .bk-row + .bk-row{margin-top:1.05em;}  /* the paragraph gap the rows take over from <p> */
/* one language: the row is a single column and the other side is not rendered at all, so an empty
   cell can never leave a gap where its opposite number is long */
.bk-bi[data-lang="en"] .bk-row{grid-template-columns:1fr;}
.bk-bi[data-lang="en"] .bk-col-or{display:none;}
.bk-bi[data-lang="or"] .bk-row{grid-template-columns:1fr;}
.bk-bi[data-lang="or"] .bk-col-en{display:none;}
.bk-bi[data-lang="en"], .bk-bi[data-lang="or"]{max-width:68ch;}   /* back to a readable measure */
.bk-bi[data-lang="both"] .bk-row{grid-template-columns:1fr 1fr;}
/* The original set apart from the translation without being demoted: the same face, size and leading,
   a little quieter in colour, and ruled off so the eye knows which column it is in. Deliberately NOT
   italicised — italic is for a foreign phrase inside a sentence, and a whole column of it is simply
   harder to read for as long as this text lasts. It would also have set the same passage in two
   different faces depending on the window width, since a phone shows this column on its own. */
.bk-bi[data-lang="both"] .bk-col-or{
  padding-left:34px; margin-left:-34px; border-left:1px solid var(--rule); color:var(--ink-soft);
}
/* A section one edition numbers and the other does not — 15 of Seneca's 2,141. The row is still
   drawn, because closing the gap would sit each column beside a passage it is not; the empty side is
   simply left empty, which is what "this edition has nothing here" looks like. */
.bk-bi[data-lang="both"] .bk-col:empty{min-height:1.78em;}
/* TURNING THE PAGE OVER IS A CROSSFADE, NOT A CUT (Aug 2026, on request). The languages dissolve into
   each other the way the home page's daily quote does: the words on screen fade out and lift, the
   `data-lang` change is made while nothing is visible, and the incoming ones rise into place.
   `bk-fade-in` holds that incoming state with NO transition — removing the class is what animates it.
   The global prefers-reduced-motion killswitch flattens all of this, and flipProse() swaps outright in
   that case rather than waiting out the timings. */
.bk-prose{transition:opacity .16s ease, transform .16s ease;}
.bk-prose.bk-fade-out{opacity:0; transform:translateY(-4px);}
.bk-prose.bk-fade-in{opacity:0; transform:translateY(6px); transition:none;}
/* STEPPING A CHAPTER IS A SLIDE, NOT A CUT (Aug 2026, on request) — and there is deliberately NO CSS for
   it, which is worth a note here because this is where anyone would look for it. The panel leaves the way
   the finger went and the next one arrives from the opposite side, all of it driven by slideChapter() in
   app.js through Element.animate. It cannot be done from this file: `.bk-page` is a direct child of
   `.page`, so the entrance stagger above matches it, and a `both`-filled animation goes on applying its
   last keyframe — `transform:none` — for the life of the page, outranking any declaration written here.
   A `.bk-page{transform:translateX(-10%)}` rule is simply ignored, silently and for ever. */
/* The second column ARRIVES rather than appearing: it rises a little further and settles a moment after
   the translation beside it, so a wide screen reads as gaining a column rather than as the whole page
   repainting. Scoped to the two-column state on purpose — on a phone this column is the only one
   showing, and the delay would do nothing there but make every tap feel slower. */
.bk-bi[data-lang="both"] .bk-col-or{transition:opacity .26s ease .06s, transform .26s ease .06s;}
.bk-bi[data-lang="both"].bk-fade-in .bk-col-or{opacity:0; transform:translateY(9px); transition:none;}
.bk-lang-btn{width:auto; padding:0 11px; gap:5px; font-family:var(--mono);
  font-size:calc(10.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase;}
.bk-lang-btn.on{background:var(--indigo); border-color:var(--indigo); color:#FFFFFF;}
.bk-lang-loading{opacity:.6; pointer-events:none;}
/* Dead on the front matter (Aug 2026, on request): chapter 0 is in English only and has no facing
   original to turn to. Greyed rather than removed — a control that disappears on one chapter and comes
   back on the next reads as a page that failed to draw; its title says why. */
.bk-lang-btn.bk-lang-na{opacity:.42; cursor:default; background:var(--card); border-color:var(--rule); color:var(--ink-faint);}

.bk-notes{margin-top:20px;}
.bk-foot{display:flex; justify-content:space-between; gap:10px; margin:16px 0 22px;}
.bk-foot .btn:disabled{opacity:.4; cursor:default;}

/* The front matter's essay, and the region an admin edits in place (see wireIntroEdit). It carries the
   About page's .msn-editable / .msn-editing outlines, which stand 6px OFF the block they mark — hence the
   gap while one is open, or the outline would cross the licence box directly under it. A reader who is not
   an admin never sees any of this: the class is added by the wiring, not by the markup, and the wrapper is
   deliberately styled with NOTHING in its resting state — it exists to be targeted, and the front matter
   has to render exactly as it did before it was wrapped. */
.bk-intro-essay.msn-editing{margin-bottom:16px;}
.bk-rights{border:1px solid var(--rule); border-radius:12px; background:var(--card); padding:16px 18px; max-width:74ch;}
.bk-rights h3{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint); margin:0 0 9px;}
.bk-rights p{font-family:var(--sans); font-size:calc(13.5px * var(--fs)); line-height:1.62; color:var(--ink-soft); margin:0 0 8px;}
.bk-rights-note{border-left:2px solid var(--good); padding-left:12px;}
.bk-rights-src{margin-bottom:0 !important;}

@media (max-width:640px){
  /* The banner keeps the full width on a phone; what changes is that the right-hand block comes DOWN
     onto a line of its own rather than trying to sit beside a title in 330px. Left-aligned there, since
     a right-aligned column under a left-aligned title reads as two unrelated things. */
  .bk-tile-body{padding:10px 13px; gap:4px 12px;}
  .bk-tile-main{flex:1 1 100%;}
  .bk-tile-foot{flex-direction:row; align-items:center; align-self:stretch; gap:10px; margin-top:3px;}
  .bk-tile-title{font-size:calc(16.5px * var(--fs));}
  .bk-tile-when,.bk-tile-sub{font-size:calc(12px * var(--fs));}
  .bk-tile-meta{font-size:calc(9px * var(--fs));}
  .bk-tile-new,.bk-tile-resume{font-size:calc(11px * var(--fs));}
  /* three things share one line here, so the chip gives back what it can: it is `flex:none` and cannot
     shrink, and the length line beside it is the only part of the foot with a shorter form */
  .bk-tile-orig{font-size:calc(9px * var(--fs)); padding:2px 6px 3px;}

  /* the top bar is gone on a phone (--bar-h is 0), so the chapter bar pins to the very top */
  .bk-barwrap{top:4px;}
  .bk-bar{border-radius:10px; padding:6px;}
  .bk-tab-t{display:none;}          /* at 390px a title per tab leaves room for two tabs; the numbers scale */
  .bk-tab{max-width:none; padding:6px 10px;}
  .bk-tab-n{font-size:calc(12.5px * var(--fs)); opacity:1;}
  .bk-page{padding:20px 18px 18px; border-radius:12px;}
  .bk-prose{font-size:calc(16.5px * var(--fs)); line-height:1.74;}
  /* at a phone's measure the hanging indent of a continued speech costs more width than it says */
  .bk-prose .bk-cont{padding-left:0;}
  .bk-prose .bk-stage{padding-left:0;}
  .bk-ch-t{font-size:calc(23px * var(--fs));}
  .bk-toc-list{grid-template-columns:1fr;}
}

/* ================= THE CARD BROWSER (Aug 2026) =====================================================
   A table, and the two things that make a table on a phone bearable: it SCROLLS SIDEWAYS INSIDE ITS OWN
   BOX (never widening the page — the house rule for every wide block on the site), and below the phone
   breakpoint four of its nine columns go, leaving the ones a reader is scanning for. The grid is declared
   once and the widths are `minmax(0, …)` throughout, because a cell's automatic minimum is its content's
   and a long card title would otherwise push the table to the width of the longest one. */
.bw-tools{display:flex; flex-wrap:wrap; align-items:center; gap:10px; margin-bottom:10px;}
.bw-search{flex:1 1 260px; min-width:0;}
.bw-search input{width:100%; appearance:none; background:var(--card); border:1px solid var(--rule); border-radius:99px; padding:9px 15px; font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink);}
.bw-search input:focus-visible{outline:2px solid var(--focus); outline-offset:2px;}
.bw-help{flex:0 0 auto; font-size:calc(12px * var(--fs)); padding:7px 13px;}
.bw-helpbox{background:var(--card); border:1px solid var(--rule); border-radius:12px; padding:14px 18px; margin-bottom:12px; font-family:var(--sans); font-size:calc(12.5px * var(--fs)); line-height:1.5; color:var(--ink-soft);}
.bw-helpbox[hidden]{display:none;}
.bw-helpbox dl{display:grid; grid-template-columns:auto 1fr; gap:6px 14px; margin:0 0 10px;}
.bw-helpbox dt{font-family:var(--mono); font-size:calc(11.5px * var(--fs)); color:var(--ink); white-space:nowrap;}
.bw-helpbox dd{margin:0;}
.bw-helpbox code{font-family:var(--mono); font-size:calc(11.5px * var(--fs)); background:var(--paper-2); border-radius:4px; padding:1px 5px;}
.bw-helpbox p{margin:0 0 8px;}
.bw-helpbox .bw-helpnote{color:var(--ink-faint); font-size:calc(11.5px * var(--fs)); margin-bottom:0;}
.bw-countline{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint); margin-bottom:8px;}
.bw-countline b{color:var(--ink);}
.bw-cap{text-transform:none; letter-spacing:0; font-family:var(--sans); font-size:calc(11.5px * var(--fs));}

/* The bulk bar. It exists only while something is selected, so it needs no disabled state and no empty
   height — the slot it lives in collapses to nothing. */
.bw-bulkslot:empty{display:none;}
.bw-bulk{display:flex; flex-wrap:wrap; align-items:center; gap:8px; background:color-mix(in srgb,var(--indigo) 8%,var(--card)); border:1px solid color-mix(in srgb,var(--indigo) 30%,var(--rule)); border-radius:12px; padding:9px 14px; margin-bottom:10px;}
.bw-bulkn{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-soft); margin-right:4px;}
.bw-bulkn b{color:var(--ink);}
.bw-bulk .btn{font-size:calc(11.5px * var(--fs)); padding:5px 11px;}

.bw-wrap{overflow-x:auto; background:var(--card); border:1px solid var(--rule); border-radius:12px; box-shadow:var(--shadow-sm);}
.bw-table{min-width:760px;}
.bw-head, .bw-row{display:grid; grid-template-columns:minmax(0,34px) minmax(0,26px) minmax(0,4fr) minmax(0,1.5fr) minmax(0,88px) minmax(0,92px) minmax(0,78px) minmax(0,68px) minmax(0,68px); align-items:center; gap:0 8px;}
.bw-head{border-bottom:1px solid var(--rule); background:var(--paper-2); position:sticky; top:0; z-index:2;}
.bw-th{appearance:none; background:none; border:0; font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.11em; text-transform:uppercase; color:var(--ink-faint); text-align:left; padding:10px 4px; cursor:pointer; display:flex; align-items:center; gap:4px; min-width:0;}
.bw-th:not([data-sort]){cursor:default;}
.bw-th.sorted{color:var(--ink);}
.bw-arrow{font-size:calc(9px * var(--fs));}
.bw-th-check, .bw-th-flag{justify-content:center; padding-inline:0;}
.bw-row{border-bottom:1px solid color-mix(in srgb,var(--rule) 55%,transparent); transition:background .13s var(--ease);}
.bw-row:last-child{border-bottom:0;}
.bw-row:hover{background:color-mix(in srgb,var(--indigo) 5%,transparent);}
.bw-row.sel{background:color-mix(in srgb,var(--indigo) 11%,transparent);}
/* a suspended card is set aside, and the row says so by being quieter as well as by its chip */
.bw-row.bw-susp .bw-name, .bw-row.bw-susp .bw-deck{opacity:.6;}
.bw-cell{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); color:var(--ink-soft); padding:9px 4px; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;}
.bw-cell-check, .bw-cell-flag{display:flex; align-items:center; justify-content:center; padding-inline:0;}
.bw-name{appearance:none; background:none; border:0; text-align:left; font:inherit; font-size:calc(13px * var(--fs)); color:var(--ink); cursor:pointer; text-decoration:underline; text-decoration-color:transparent; text-underline-offset:3px; transition:text-decoration-color .14s var(--ease);}
.bw-name:hover{text-decoration-color:var(--indigo);}
.bw-deck{font-size:calc(11.5px * var(--fs)); color:var(--ink-faint);}
.bw-due, .bw-ivl, .bw-reps, .bw-lapses{font-family:var(--mono); font-size:calc(11.5px * var(--fs));}
.bw-overdue{color:var(--zh); font-weight:600;}
.bw-tag{display:inline-block; font-family:var(--mono); font-size:calc(9px * var(--fs)); letter-spacing:.08em; text-transform:uppercase; color:var(--ink-faint); border:1px solid var(--rule); border-radius:99px; padding:0 6px; margin-left:5px; vertical-align:middle;}
.bw-tag-leech{color:var(--zh); border-color:color-mix(in srgb,var(--zh) 40%,transparent);}
.bw-none{margin:0; padding:20px 18px; font-family:var(--sans); font-size:calc(13px * var(--fs)); color:var(--ink-faint);}
/* the foot of the table, which is both the "there is more" line and the mark the observer watches for */
.bw-more{padding:14px 18px; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; color:var(--ink-faint); border-top:1px solid color-mix(in srgb,var(--rule) 55%,transparent);}
.bw-entry .rs-sub{max-width:60ch;}

@media (max-width:640px){
  /* Four columns go rather than being squeezed: at 390px the card's own title is the only part of a row
     with no shorter form, so anything that takes width from it is what has to give. The data is still a
     tap away in Card info, which is what the row opens. */
  .bw-table{min-width:0;}
  .bw-head, .bw-row{grid-template-columns:minmax(0,32px) minmax(0,24px) minmax(0,1fr) minmax(0,84px);}
  .bw-th-deck, .bw-th-ivl, .bw-th-reps, .bw-th-lapses, .bw-deck, .bw-ivl, .bw-reps, .bw-lapses{display:none;}
  .bw-th-state, .bw-state{display:none;}
  .bw-wrap{overflow-x:hidden;}
}

/* ---- EASY DAYS: the seven weekday toggles in Settings → Study (Aug 2026) --------------------------
   `role="switch"` rather than checkboxes, because each one is independently on or off and there is no
   "choose one" among them. Marked-off is the LOUD state — it is the thing the reader has said, and the
   unmarked six are just the ordinary week — so it takes the colour and the strike, and the label stays a
   word rather than becoming an icon: three letters are already the short form. */
/* THE ROW STACKS. `.set-row` is a flex line with the prose at `flex:1` and the control at `flex:none`, which
   is right for a switch and hopeless for seven buttons: at the settings column's ~370px they claim the whole
   line and the description beside them wraps to one word per line. So this row alone lays its control out
   UNDER the prose, full width — found by looking at the page, which is the only way a squeeze like that
   shows up. */
.set-row-stack{flex-direction:column; align-items:stretch; gap:12px;}
.set-row-stack .ctl{flex:none; width:100%;}
.ed-days{display:flex; flex-wrap:wrap; gap:6px;}
.ed-day{appearance:none; background:var(--card); border:1px solid var(--rule); border-radius:9px; padding:7px 9px; cursor:pointer; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.06em; text-transform:uppercase; color:var(--ink-soft); transition:background .14s var(--ease), border-color .14s var(--ease), color .14s var(--ease);}
.ed-day:hover{border-color:color-mix(in srgb,var(--indigo) 45%,var(--rule));}
.ed-day:focus-visible{outline:2px solid var(--focus); outline-offset:2px;}
.ed-day.off{background:color-mix(in srgb,var(--zh) 12%,var(--card)); border-color:color-mix(in srgb,var(--zh) 45%,transparent); color:var(--zh);}
.ed-day.off span{text-decoration:line-through; text-decoration-thickness:1px;}

/* ============================================================
   GEMSTONE THEMES (Sep 2026, on request)
   ============================================================
   "Create a number of new themes: Diamond, Ruby, Opalite, Jade, Emerald, Amber, Amethyst, Aquamarine,
   Bloodstone, Carnelian, all inspired by the look and feel of their gemstones. Make each theme feel
   unique in colors, shapes, etc."

   WHAT A THEME IS HERE is settled by the six above and is not re-argued: a full set of custom properties
   for light and for night, ALL OF THEM HEX (the canvas globe parses and blends them — see the golden rule
   in CLAUDE.md), plus whatever type, corner and ambient rules give it a character. So each stone below is
   a palette, a font pairing drawn from the faces the stylesheet ALREADY imports, a corner radius, and one
   signature treatment. Nothing here adds a webfont: there is one `@import` for the whole site and every
   visitor pays for it whatever theme they wear.

   HOW EACH STONE WAS READ, since "inspired by the gemstone" is a judgement and the next session should
   not have to re-make it:
     Diamond     colourless and faceted — near-white papers, cool steel ink, a CLIPPED corner rather than
                 a rounded one (a brilliant has facets, not fillets), hairline rules, the highest contrast
                 of the ten.
     Ruby        pigeon's blood on warm ivory, garnet-dark night; a soft corner and a heavy serif, the
                 stone being the one people call rich.
     Opalite     milky and iridescent — the accent SHIFTS across a gradient rather than sitting still, and
                 the corners are the roundest here. PROMOTED later the same month, on request, to be the
                 site's DEFAULT and renamed Folio, so it is no longer one of the ten: read it at the top
                 of this file, in `:root` / `body.night` and the `body[data-theme="folio"]` rules beside
                 them. The one thing that changed in the move is the red — the stone's own milky pink was
                 2.98:1 on its card, which is fine for a collectible and not for the answer term on every
                 card the site deals.
     Jade        carved nephrite: cool green over cream, a stepped inner rule on cards standing for the
                 relief a carver leaves, and the site's own Chinese ink beside it.
     Emerald     the STEP CUT — an emerald is cut in rectangular steps, so its cards are square-cornered
                 with a doubled border, and its green is deeper and bluer than jade's.
     Amber       fossil resin: everything warm and slightly translucent, a golden wash on the papers, and
                 the softest, roundest cards of the warm three.
     Amethyst    violet quartz, banded light to dark; a crystalline corner (one facet clipped) and a
                 lighter, airier type than Ruby's.
     Aquamarine  sea-pale beryl — the lightest and coolest of the ten, thin type, wide tracking, and a
                 horizon gradient on the page ground.
     Bloodstone  dark green heliotrope FLECKED WITH RED, which is the whole of the stone: a green so dark
                 it reads near-black, with the red used only in small marks, and hard corners.
     Carnelian   banded orange-red chalcedony: warm, opaque, a banded ambient, and a condensed display
                 face where Amber takes a round one.

   CONTRAST. `--ink` clears 12:1 on every paper below and `--ink-soft` 6:1, measured the same way the
   CONTRAST block near the top of this file measures the originals. The quiet tokens (`--ink-faint`,
   `--ochre`, `--geo`) are deliberately quiet here as they are there, and `body.hc` re-tones them to its
   own solved values for every theme at once, so the mode covers these without a rule apiece.
   ============================================================ */

/* ---------- DIAMOND ---------- */
body[data-theme="diamond"]{
  --paper:#FBFCFD; --paper-2:#EEF1F5; --card:#FFFFFF;
  --ink:#14181D; --ink-soft:#4A545F; --ink-faint:#8B959F; --rule:#DCE2E9;
  --indigo:#2C6E9B; --indigo-bright:#3E8ABF; --indigo-wash:#E7F0F7; --ochre:#8A7B4E;
  --zh:#A83A34; --zh-wash:#F6E7E6; --geo:#4E7C8A; --good:#2F7A5E;
  --serif:"Spectral",Georgia,serif; --display:"Sora",system-ui,sans-serif;
  --mono:"JetBrains Mono",ui-monospace,monospace; --sans:"Inter",system-ui,sans-serif;
}
body[data-theme="diamond"].night{
  --paper:#0E1216; --paper-2:#080B0E; --card:#161C22;
  --ink:#EDF2F7; --ink-soft:#AAB6C2; --ink-faint:#75818D; --rule:#2A333C;
  --indigo:#7FC0E8; --indigo-bright:#A2D5F5; --indigo-wash:#152430; --ochre:#C0AE78;
  --zh:#E0736A; --zh-wash:#31201F; --geo:#7FB3C2; --good:#66B58F;
}
/* a brilliant has FACETS, not fillets: every panel loses its top-left and bottom-right corner to a
   45-degree cut, which is one clip-path rather than a radius and reads as a cut stone at any size */
body[data-theme="diamond"] .card, body[data-theme="diamond"] .collection,
body[data-theme="diamond"] .study-card, body[data-theme="diamond"] .banner,
body[data-theme="diamond"] .game-tile, body[data-theme="diamond"] .placard{
  border-radius:0; clip-path:polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
}
body[data-theme="diamond"] .btn, body[data-theme="diamond"] .grade{border-radius:2px;}
body[data-theme="diamond"] .page-head h1, body[data-theme="diamond"] .brand .mark{
  font-family:var(--display); font-weight:600; letter-spacing:.01em;
}
/* the ground: a faint radial glint, as light caught in a table facet */
body[data-theme="diamond"]::before{
  background:radial-gradient(120% 90% at 50% -10%, #FFFFFF 0%, #F3F6FA 45%, #EAEFF5 100%);
  opacity:1; filter:none;
}
body[data-theme="diamond"].night::before{
  background:radial-gradient(120% 90% at 50% -10%, #18202A 0%, #10161C 48%, #0B0F13 100%);
}

/* ---------- RUBY ---------- */
body[data-theme="ruby"]{
  --paper:#FCF6F4; --paper-2:#F3E4E0; --card:#FFFBFA;
  --ink:#241417; --ink-soft:#5E4046; --ink-faint:#9C7A7E; --rule:#E6D0CC;
  --indigo:#9B1B30; --indigo-bright:#BC2841; --indigo-wash:#F7E3E5; --ochre:#A8722C;
  --zh:#8E2033; --zh-wash:#F5E0E2; --geo:#7A5C6A; --good:#4F7A4A;
  --serif:"EB Garamond",Georgia,serif; --display:"Cormorant Garamond",Georgia,serif;
  --mono:"IBM Plex Mono",ui-monospace,monospace; --sans:"Inter",system-ui,sans-serif;
}
body[data-theme="ruby"].night{
  --paper:#180B0E; --paper-2:#100609; --card:#221114;
  --ink:#F6E7E9; --ink-soft:#C3A3A8; --ink-faint:#8C6C71; --rule:#3B2126;
  --indigo:#E2566E; --indigo-bright:#F4788C; --indigo-wash:#33161C; --ochre:#C9974A;
  --zh:#E86A7C; --zh-wash:#38181E; --geo:#B08FA0; --good:#7FB073;
}
body[data-theme="ruby"]{ font-size:calc(16.5px * var(--fs)); }
body[data-theme="ruby"] .page-head h1, body[data-theme="ruby"] .brand .mark,
body[data-theme="ruby"] .collection-title, body[data-theme="ruby"] .banner h2{
  font-family:var(--display); font-weight:600; letter-spacing:.005em;
}
body[data-theme="ruby"] .card, body[data-theme="ruby"] .collection,
body[data-theme="ruby"] .study-card, body[data-theme="ruby"] .banner{
  border-radius:14px; box-shadow:0 10px 30px color-mix(in srgb,#9B1B30 12%, transparent);
}
/* the ground: the deep glow at the heart of a cut ruby */
body[data-theme="ruby"]::before{
  background:radial-gradient(130% 100% at 50% 0%, #FFFAF9 0%, #F8EAE7 55%, #F0DAD6 100%);
  opacity:1; filter:none;
}
body[data-theme="ruby"].night::before{
  background:radial-gradient(130% 100% at 50% 0%, #2A1116 0%, #180A0D 55%, #0E0507 100%);
}

/* ---------- OPALITE — RETIRED Sep 2026: IT IS THE DEFAULT NOW ----------
   On request, the Opalite theme replaced the default and took its name, so its tokens live in `:root` /
   `body.night` at the top of this file and its signature treatments sit beside them under
   `body[data-theme="folio"]`. There is no `opalite` id left in `THEMES`, so a reader who was wearing it
   falls through the whitelist in `applyTheme` to `folio` and sees the same thing they always did.
   Do NOT re-add a gemstone block here under that name: two blocks would then define one palette. */

/* ---------- JADE ---------- */
body[data-theme="jade"]{
  --paper:#F3F7F2; --paper-2:#E2ECE1; --card:#FAFCF9;
  --ink:#17231C; --ink-soft:#42574A; --ink-faint:#7E9184; --rule:#D0DED0;
  --indigo:#2E7A63; --indigo-bright:#3D977B; --indigo-wash:#E1F0EA; --ochre:#A8823C;
  --zh:#9C4A3C; --zh-wash:#F2E2DE; --geo:#4F7F6B; --good:#3E8055;
  --serif:"Spectral",Georgia,serif; --display:"Spectral",Georgia,serif;
  --mono:"IBM Plex Mono",ui-monospace,monospace; --sans:"Inter",system-ui,sans-serif;
}
body[data-theme="jade"].night{
  --paper:#0E1512; --paper-2:#080D0B; --card:#15211B;
  --ink:#E6F1E9; --ink-soft:#A6BCAF; --ink-faint:#748A7D; --rule:#26372E;
  --indigo:#6BC3A2; --indigo-bright:#8BDCBB; --indigo-wash:#15291F; --ochre:#C7A455;
  --zh:#D4796A; --zh-wash:#2C1B18; --geo:#7FB29A; --good:#77B98B;
}
/* THE STEPPED INNER RULE stands for the relief a jade carver leaves — a second hairline set in from the
   border, which is a box-shadow inset rather than a second element */
body[data-theme="jade"] .card, body[data-theme="jade"] .collection,
body[data-theme="jade"] .study-card, body[data-theme="jade"] .banner{
  border-radius:6px;
  box-shadow:inset 0 0 0 1px color-mix(in srgb,var(--indigo) 10%, transparent),
             inset 0 0 0 5px color-mix(in srgb,var(--card) 100%, transparent),
             inset 0 0 0 6px color-mix(in srgb,var(--indigo) 16%, transparent),
             var(--shadow-sm);
}
body[data-theme="jade"] .card, body[data-theme="jade"] .collection,
body[data-theme="jade"] .study-card, body[data-theme="jade"] .banner{padding-block:calc(10px + .4em);}
body[data-theme="jade"] .page-head h1, body[data-theme="jade"] .brand .mark{
  font-family:var(--display); font-weight:600; letter-spacing:.02em;
}
body[data-theme="jade"]::before{
  background:linear-gradient(180deg,#EEF5EE 0%, #F3F7F2 40%, #E9F1E8 100%);
  opacity:1; filter:none;
}
body[data-theme="jade"].night::before{
  background:linear-gradient(180deg,#111A15 0%, #0E1512 45%, #0A100D 100%);
}

/* ---------- EMERALD ---------- */
body[data-theme="emerald"]{
  --paper:#F2F7F3; --paper-2:#E0EBE3; --card:#F9FCFA;
  --ink:#0F1A14; --ink-soft:#3A5245; --ink-faint:#788D80; --rule:#CBDCD1;
  --indigo:#0F6B4B; --indigo-bright:#158A61; --indigo-wash:#DDEFE5; --ochre:#B08A2E;
  --zh:#9C3B33; --zh-wash:#F1DFDC; --geo:#3F7D69; --good:#2F7B52;
  --serif:"EB Garamond",Georgia,serif; --display:"Cinzel",Georgia,serif;
  --mono:"IBM Plex Mono",ui-monospace,monospace; --sans:"Inter",system-ui,sans-serif;
}
body[data-theme="emerald"].night{
  --paper:#0A1410; --paper-2:#050C09; --card:#101F18;
  --ink:#E3F2E9; --ink-soft:#A0BCAD; --ink-faint:#6E8A7B; --rule:#20362B;
  --indigo:#4FC792; --indigo-bright:#72E0AF; --indigo-wash:#0F2A1E; --ochre:#D0A93F;
  --zh:#D9736A; --zh-wash:#2B1917; --geo:#6FB295; --good:#66BC8B;
}
/* THE STEP CUT. An emerald is cut in rectangular steps, so the corners are square and the border is
   DOUBLED — an outer hairline and an inner one with the card's own paper between them. */
body[data-theme="emerald"] .card, body[data-theme="emerald"] .collection,
body[data-theme="emerald"] .study-card, body[data-theme="emerald"] .banner,
body[data-theme="emerald"] .game-tile, body[data-theme="emerald"] .placard{
  border-radius:0;
  box-shadow:inset 0 0 0 3px var(--card), inset 0 0 0 4px color-mix(in srgb,var(--indigo) 30%, transparent);
}
body[data-theme="emerald"] .btn, body[data-theme="emerald"] .grade{border-radius:0;}
body[data-theme="emerald"] .page-head h1, body[data-theme="emerald"] .brand .mark,
body[data-theme="emerald"] .collection-title{
  font-family:var(--display); font-weight:600; text-transform:uppercase; letter-spacing:.08em;
}
body[data-theme="emerald"]::before{
  background:linear-gradient(160deg,#F5F9F5 0%, #EAF3EC 55%, #DFEBE3 100%);
  opacity:1; filter:none;
}
body[data-theme="emerald"].night::before{
  background:linear-gradient(160deg,#0D1A14 0%, #08120D 60%, #050A07 100%);
}

/* ---------- AMBER ---------- */
body[data-theme="amber"]{
  --paper:#FBF4E7; --paper-2:#F2E5CE; --card:#FFFBF2;
  --ink:#241A0C; --ink-soft:#5A4525; --ink-faint:#9C8560; --rule:#E6D5B6;
  --indigo:#A9691A; --indigo-bright:#C8822B; --indigo-wash:#F7E9D3; --ochre:#9A7220;
  --zh:#9E3F22; --zh-wash:#F5E0D6; --geo:#7C6A2E; --good:#6B7A2C;
  --serif:"Lora",Georgia,serif; --display:"Fraunces",Georgia,serif;
  --mono:"IBM Plex Mono",ui-monospace,monospace; --sans:"Inter",system-ui,sans-serif;
}
body[data-theme="amber"].night{
  --paper:#171003; --paper-2:#0F0A02; --card:#221808;
  --ink:#F8EDD8; --ink-soft:#C9AF83; --ink-faint:#94805B; --rule:#3B2D14;
  --indigo:#E0A94A; --indigo-bright:#F5C46A; --indigo-wash:#33240C; --ochre:#D6A63C;
  --zh:#E08050; --zh-wash:#331C10; --geo:#B5A253; --good:#A8B45A;
}
/* resin: everything soft-cornered and warm, with a honeyed glow rather than a hard shadow */
body[data-theme="amber"] .card, body[data-theme="amber"] .collection,
body[data-theme="amber"] .study-card, body[data-theme="amber"] .banner,
body[data-theme="amber"] .game-tile, body[data-theme="amber"] .placard{
  border-radius:16px; box-shadow:0 8px 26px color-mix(in srgb,#A9691A 14%, transparent);
}
body[data-theme="amber"] .page-head h1, body[data-theme="amber"] .brand .mark{
  font-family:var(--display); font-weight:600; letter-spacing:-.005em;
}
body[data-theme="amber"]::before{
  background:radial-gradient(120% 100% at 50% 0%, #FFF8EA 0%, #F8EDD8 50%, #EFE0C4 100%);
  opacity:1; filter:none;
}
body[data-theme="amber"].night::before{
  background:radial-gradient(120% 100% at 50% 0%, #2A1D07 0%, #170F03 55%, #0D0902 100%);
}

/* ---------- AMETHYST ---------- */
body[data-theme="amethyst"]{
  --paper:#F8F5FC; --paper-2:#EDE5F6; --card:#FDFBFF;
  --ink:#1C1526; --ink-soft:#4C3F63; --ink-faint:#8E82A4; --rule:#E0D6EE;
  --indigo:#6B44A0; --indigo-bright:#8459C0; --indigo-wash:#EEE6F9; --ochre:#93743E;
  --zh:#A63C6B; --zh-wash:#F5E1EA; --geo:#5B6FA8; --good:#4B8368;
  --serif:"Cormorant Garamond",Georgia,serif; --display:"Sora",system-ui,sans-serif;
  --mono:"JetBrains Mono",ui-monospace,monospace; --sans:"Inter",system-ui,sans-serif;
}
body[data-theme="amethyst"].night{
  --paper:#140F1E; --paper-2:#0D0914; --card:#1E1729;
  --ink:#F0EAFA; --ink-soft:#B7ACCD; --ink-faint:#877CA0; --rule:#332A46;
  --indigo:#B18BE8; --indigo-bright:#C9A9F7; --indigo-wash:#241A38; --ochre:#C0A05E;
  --zh:#DC7BA6; --zh-wash:#2E1826; --geo:#8C9DDA; --good:#74B694;
}
/* ONE CLIPPED FACET rather than Diamond's two — a crystal point, not a cut stone */
body[data-theme="amethyst"] .card, body[data-theme="amethyst"] .collection,
body[data-theme="amethyst"] .study-card, body[data-theme="amethyst"] .banner,
body[data-theme="amethyst"] .game-tile{
  border-radius:0; clip-path:polygon(0 0, calc(100% - 14px) 0, 100% 14px, 100% 100%, 0 100%);
}
body[data-theme="amethyst"] .btn, body[data-theme="amethyst"] .grade{border-radius:4px;}
body[data-theme="amethyst"] .page-head h1, body[data-theme="amethyst"] .brand .mark{
  font-family:var(--display); font-weight:600; letter-spacing:0;
}
/* the banding a cut amethyst shows: pale at the base, deep at the tip */
body[data-theme="amethyst"]::before{
  background:linear-gradient(180deg,#EFE7FA 0%, #F8F5FC 45%, #FBFAFE 100%);
  opacity:1; filter:none;
}
body[data-theme="amethyst"].night::before{
  background:linear-gradient(180deg,#241A38 0%, #150F20 48%, #0C0812 100%);
}

/* ---------- AQUAMARINE ---------- */
body[data-theme="aquamarine"]{
  --paper:#F2FAFB; --paper-2:#DFF0F3; --card:#FBFEFE;
  --ink:#0E1D21; --ink-soft:#3B565C; --ink-faint:#7A9299; --rule:#CBE2E7;
  --indigo:#0E7490; --indigo-bright:#1793B3; --indigo-wash:#DEF0F5; --ochre:#8A7A46;
  --zh:#A2444A; --zh-wash:#F3E1E2; --geo:#3E9E9E; --good:#328672;
  --serif:"Newsreader",Georgia,serif; --display:"Space Grotesk",system-ui,sans-serif;
  --mono:"IBM Plex Mono",ui-monospace,monospace; --sans:"Nunito Sans",system-ui,sans-serif;
}
body[data-theme="aquamarine"].night{
  --paper:#08171B; --paper-2:#040F12; --card:#0F2329;
  --ink:#E3F4F7; --ink-soft:#9FBEC5; --ink-faint:#6D8C94; --rule:#1D3A42;
  --indigo:#4FC2E0; --indigo-bright:#78DBF4; --indigo-wash:#0E2C36; --ochre:#C0AC65;
  --zh:#E0787E; --zh-wash:#2B1618; --geo:#6FC9C9; --good:#65BFA4;
}
/* the lightest of the ten: thin rules, wide tracking, and a horizon on the ground */
body[data-theme="aquamarine"] .card, body[data-theme="aquamarine"] .collection,
body[data-theme="aquamarine"] .study-card, body[data-theme="aquamarine"] .banner,
body[data-theme="aquamarine"] .game-tile, body[data-theme="aquamarine"] .placard{
  border-radius:12px; box-shadow:0 6px 20px color-mix(in srgb,#0E7490 10%, transparent);
}
body[data-theme="aquamarine"] .page-head h1, body[data-theme="aquamarine"] .brand .mark{
  font-family:var(--display); font-weight:500; letter-spacing:.03em;
}
body[data-theme="aquamarine"] .eyebrow, body[data-theme="aquamarine"] .section-label{letter-spacing:.24em;}
body[data-theme="aquamarine"]::before{
  background:linear-gradient(180deg,#EAF7FA 0%, #F5FCFD 46%, #E3F2F5 100%);
  opacity:1; filter:none;
}
body[data-theme="aquamarine"].night::before{
  background:linear-gradient(180deg,#0C2229 0%, #08171B 50%, #040E11 100%);
}

/* ---------- BLOODSTONE ---------- */
body[data-theme="bloodstone"]{
  --paper:#EFF3EE; --paper-2:#DDE5DC; --card:#F7FAF6;
  --ink:#121A14; --ink-soft:#3D4C41; --ink-faint:#7A8A7D; --rule:#C9D5C7;
  --indigo:#2A5741; --indigo-bright:#376F53; --indigo-wash:#DEE9E1; --ochre:#8E6B2A;
  --zh:#A62B22; --zh-wash:#F3DEDB; --geo:#4A6B55; --good:#3D7A4E;
  --serif:"PT Serif",Georgia,serif; --display:"Oswald",system-ui,sans-serif;
  --mono:"IBM Plex Mono",ui-monospace,monospace; --sans:"Inter",system-ui,sans-serif;
}
body[data-theme="bloodstone"].night{
  --paper:#0B110D; --paper-2:#060A07; --card:#121C15;
  --ink:#E4EDE5; --ink-soft:#A3B5A7; --ink-faint:#728478; --rule:#223028;
  --indigo:#5FA582; --indigo-bright:#7CC4A0; --indigo-wash:#132419; --ochre:#C09A48;
  --zh:#E05B4E; --zh-wash:#2C1512; --geo:#7BA88C; --good:#6FB07E;
}
/* HARD CORNERS AND A FLECK. The stone is a dark green so deep it reads near-black, and the red appears
   only in small marks — so the red is spent on the ONE thing that has to be found at a glance rather than
   on any surface: the marks that say something is due, wrong, or waiting. */
body[data-theme="bloodstone"] .card, body[data-theme="bloodstone"] .collection,
body[data-theme="bloodstone"] .study-card, body[data-theme="bloodstone"] .banner,
body[data-theme="bloodstone"] .game-tile, body[data-theme="bloodstone"] .placard{border-radius:3px;}
body[data-theme="bloodstone"] .btn, body[data-theme="bloodstone"] .grade{border-radius:2px;}
body[data-theme="bloodstone"] .page-head h1, body[data-theme="bloodstone"] .brand .mark,
body[data-theme="bloodstone"] .collection-title{
  font-family:var(--display); font-weight:600; text-transform:uppercase; letter-spacing:.06em;
}
body[data-theme="bloodstone"]::before{
  background:linear-gradient(150deg,#EDF2EC 0%, #E6EEE5 60%, #DCE6DA 100%);
  opacity:1; filter:none;
}
body[data-theme="bloodstone"].night::before{
  background:linear-gradient(150deg,#0E150F 0%, #0A0F0B 60%, #060907 100%);
}

/* ---------- CARNELIAN ---------- */
body[data-theme="carnelian"]{
  --paper:#FDF3EC; --paper-2:#F5E2D5; --card:#FFF9F4;
  --ink:#26160E; --ink-soft:#5C3F2C; --ink-faint:#A08165; --rule:#EAD4C2;
  --indigo:#B4471F; --indigo-bright:#D25A2B; --indigo-wash:#FAE5D9; --ochre:#8A5A2B;
  --zh:#9B3520; --zh-wash:#F6DED4; --geo:#7A6440; --good:#6F7A34;
  --serif:"Newsreader",Georgia,serif; --display:"Oswald",system-ui,sans-serif;
  --mono:"IBM Plex Mono",ui-monospace,monospace; --sans:"Inter",system-ui,sans-serif;
}
body[data-theme="carnelian"].night{
  --paper:#190E07; --paper-2:#100804; --card:#241408;
  --ink:#F9E9DC; --ink-soft:#CCA98D; --ink-faint:#997B60; --rule:#3D2617;
  --indigo:#EE8452; --indigo-bright:#FFA274; --indigo-wash:#361B0D; --ochre:#CE9450;
  --zh:#E97455; --zh-wash:#331A11; --geo:#B59A5E; --good:#A5B25C;
}
/* BANDED, which is what tells a carnelian from a plain orange stone: the ground is laid in broad soft
   bands rather than one wash. Corners are a middling radius — warmer than Bloodstone, harder than Amber. */
body[data-theme="carnelian"] .card, body[data-theme="carnelian"] .collection,
body[data-theme="carnelian"] .study-card, body[data-theme="carnelian"] .banner,
body[data-theme="carnelian"] .game-tile, body[data-theme="carnelian"] .placard{border-radius:8px;}
body[data-theme="carnelian"] .page-head h1, body[data-theme="carnelian"] .brand .mark{
  font-family:var(--display); font-weight:500; text-transform:uppercase; letter-spacing:.05em;
}
body[data-theme="carnelian"]::before{
  background:repeating-linear-gradient(172deg,#FDF3EC 0 46px, #F9EADF 46px 76px, #FBEFE6 76px 118px);
  opacity:1; filter:none;
}
body[data-theme="carnelian"].night::before{
  background:repeating-linear-gradient(172deg,#190E07 0 46px, #21130A 46px 76px, #1C1008 76px 118px);
}

/* ---------- EACH STONE'S COLLECTION BANNER ----------
   THE BASE `.collection-deco` IS A STRONG WASH — the collection's hue at 46% to 76% — and every theme on
   the site overrides it, which is not decoration but a legibility requirement: the row's own quiet text
   (`STUDIED`, the card count, the progress bar) is `--ink-faint` over whatever this paints, and at 46% of
   a saturated hue it is unreadable. The ten below fell through to that base for an hour and Emerald was
   the one that showed it — deep salmon banners with the count barely visible on them.
   So each stone gets a treatment of its own, in the register the stone suggests, and none of them mixes
   the hue past about a fifth: the hue is there to SAY WHICH COLLECTION, and the words on top of it are
   what the reader is actually reading. `.night` flips each so the light text keeps the darker end,
   exactly as the base rule does. */
body[data-theme="diamond"] .collection-deco{
  background:linear-gradient(112deg, color-mix(in srgb,var(--coll-bg) 15%, var(--card)) 0%, var(--card) 46%),
             linear-gradient(112deg, transparent 54%, color-mix(in srgb,#FFFFFF 70%, transparent) 56%, transparent 60%);
}
body[data-theme="diamond"].night .collection-deco{
  background:linear-gradient(112deg, color-mix(in srgb,var(--coll-bg) 34%, var(--card)) 0%, var(--card) 50%),
             linear-gradient(112deg, transparent 54%, color-mix(in srgb,#FFFFFF 14%, transparent) 56%, transparent 60%);
}
/* Ruby: the glow sits in the corner the title starts from and falls away, as light does in a cut stone */
body[data-theme="ruby"] .collection-deco{
  background:radial-gradient(120% 160% at 0% 0%, color-mix(in srgb,var(--coll-bg) 20%, var(--card)) 0%, var(--card) 62%);
}
body[data-theme="ruby"].night .collection-deco{
  background:radial-gradient(120% 160% at 0% 0%, color-mix(in srgb,var(--coll-bg) 40%, var(--card)) 0%, var(--card) 66%);
}
/* Jade: a carved band down the leading edge, the rest left as stone */
body[data-theme="jade"] .collection-deco{
  background:linear-gradient(90deg, color-mix(in srgb,var(--coll-bg) 30%, var(--card)) 0 6px, color-mix(in srgb,var(--coll-bg) 9%, var(--card)) 6px 100%);
}
body[data-theme="jade"].night .collection-deco{
  background:linear-gradient(90deg, color-mix(in srgb,var(--coll-bg) 55%, var(--card)) 0 6px, color-mix(in srgb,var(--coll-bg) 22%, var(--card)) 6px 100%);
}
/* Emerald: the STEP CUT again — the hue in flat rectangular steps down the leading edge rather than a
   gradient, since a step cut has no curved facets anywhere on it */
body[data-theme="emerald"] .collection-deco{
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--coll-bg) 32%, var(--card)) 0 10px,
    color-mix(in srgb,var(--coll-bg) 18%, var(--card)) 10px 22px,
    color-mix(in srgb,var(--coll-bg) 9%, var(--card)) 22px 38px,
    var(--card) 38px 100%);
}
body[data-theme="emerald"].night .collection-deco{
  background:linear-gradient(90deg,
    color-mix(in srgb,var(--coll-bg) 62%, var(--card)) 0 10px,
    color-mix(in srgb,var(--coll-bg) 38%, var(--card)) 10px 22px,
    color-mix(in srgb,var(--coll-bg) 20%, var(--card)) 22px 38px,
    var(--card) 38px 100%);
}
/* Amber: the hue suspended in resin — a soft warm glow with no edge to it at all */
body[data-theme="amber"] .collection-deco{
  background:radial-gradient(140% 200% at 12% 0%, color-mix(in srgb,var(--coll-bg) 19%, var(--card)) 0%, var(--card) 70%);
}
body[data-theme="amber"].night .collection-deco{
  background:radial-gradient(140% 200% at 12% 0%, color-mix(in srgb,var(--coll-bg) 42%, var(--card)) 0%, var(--card) 74%);
}
/* Amethyst: banded from the base up, pale to deep, the way a crystal is */
body[data-theme="amethyst"] .collection-deco{
  background:linear-gradient(180deg, var(--card) 0%, color-mix(in srgb,var(--coll-bg) 18%, var(--card)) 100%);
}
body[data-theme="amethyst"].night .collection-deco{
  background:linear-gradient(180deg, var(--card) 0%, color-mix(in srgb,var(--coll-bg) 42%, var(--card)) 100%);
}
/* Aquamarine: a horizon — the hue lying along the bottom edge like water under a pale sky */
body[data-theme="aquamarine"] .collection-deco{
  background:linear-gradient(180deg, var(--card) 52%, color-mix(in srgb,var(--coll-bg) 16%, var(--card)) 100%);
}
body[data-theme="aquamarine"].night .collection-deco{
  background:linear-gradient(180deg, var(--card) 48%, color-mix(in srgb,var(--coll-bg) 38%, var(--card)) 100%);
}
/* Bloodstone: FLECKED, not washed. The stone is dark green with the colour in specks, so the hue appears
   as one small mark at the trailing edge and nowhere else — the most restrained of the ten by design. */
body[data-theme="bloodstone"] .collection-deco{
  background:linear-gradient(90deg, var(--card) 0 calc(100% - 54px), color-mix(in srgb,var(--coll-bg) 26%, var(--card)) calc(100% - 54px) 100%);
}
body[data-theme="bloodstone"].night .collection-deco{
  background:linear-gradient(90deg, var(--card) 0 calc(100% - 54px), color-mix(in srgb,var(--coll-bg) 52%, var(--card)) calc(100% - 54px) 100%);
}
/* Carnelian: the banding the stone is named for, laid across the row at a shallow angle */
body[data-theme="carnelian"] .collection-deco{
  background:repeating-linear-gradient(168deg,
    color-mix(in srgb,var(--coll-bg) 15%, var(--card)) 0 14px,
    color-mix(in srgb,var(--coll-bg) 7%, var(--card)) 14px 30px);
}
body[data-theme="carnelian"].night .collection-deco{
  background:repeating-linear-gradient(168deg,
    color-mix(in srgb,var(--coll-bg) 36%, var(--card)) 0 14px,
    color-mix(in srgb,var(--coll-bg) 18%, var(--card)) 14px 30px);
}

/* ==========================================================================================
   THE FIRST-SESSION ORDER PICKER, AND THE DECK PRETEST (Sep 2026, on request)
   ==========================================================================================
   Two pages that stand between pressing Study and studying. Both are read ONCE per deck and then
   never again, which decides how they are set: long lines, generous leading, nothing clever. A page
   somebody reads once has to be legible on the first pass or it is not read at all.

   The option cards are BUTTONS, so everything in them is a <span> — a <p> inside a <button> is invalid
   and gets hoisted out by the parser, exactly as a <button> inside the review banner is. */
.op-grid{display:grid; gap:14px; grid-template-columns:1fr;}
@media (min-width:900px){ .op-grid{grid-template-columns:1fr 1fr;} }
.op-card{display:flex; flex-direction:column; gap:9px; text-align:left; width:100%;
  background:var(--paper-2); border:1px solid var(--rule); border-radius:14px;
  padding:18px 18px 16px; cursor:pointer; color:inherit; font:inherit;
  transition:border-color .22s var(--ease), transform .22s var(--ease);}
.op-card:hover{border-color:var(--indigo); transform:translateY(-2px);}
.op-card:focus-visible{outline:2px solid var(--indigo); outline-offset:2px;}
.op-name{font-family:var(--display); font-size:calc(19px * var(--fs)); font-weight:600; color:var(--ink);
  display:flex; align-items:baseline; gap:9px; flex-wrap:wrap;}
.op-tag{font-family:var(--sans); font-size:calc(10px * var(--fs)); letter-spacing:.05em; text-transform:uppercase;
  color:var(--ink-faint); border:1px solid var(--rule); border-radius:999px; padding:1px 7px;}
.op-lead{font-size:calc(14.5px * var(--fs)); color:var(--ink); line-height:1.5;}
.op-p{font-size:calc(13.5px * var(--fs)); color:var(--ink-soft); line-height:1.62;}
.op-who{font-size:calc(13px * var(--fs)); color:var(--ink); line-height:1.5; font-style:italic;
  border-top:1px solid var(--rule); padding-top:9px; margin-top:2px;}
.op-go{align-self:flex-start; font-size:calc(12.5px * var(--fs)); font-weight:600; letter-spacing:.02em;
  color:var(--indigo);}
.op-card:hover .op-go{text-decoration:underline;}
.op-foot{margin-top:20px; border-top:1px solid var(--rule); padding-top:16px;
  display:flex; flex-direction:column; gap:12px; align-items:flex-start;}
.op-foot p{font-size:calc(13.5px * var(--fs)); color:var(--ink-soft); line-height:1.62; margin:0; max-width:70ch;}
.op-footacts{display:flex; gap:10px; flex-wrap:wrap;}

/* the pretest — one question at a time, and no feedback until the end */
.pt-intro,.pt-q,.pt-res{background:var(--paper-2); border:1px solid var(--rule); border-radius:14px; padding:20px;}
.pt-intro p,.pt-res p{font-size:calc(14.5px * var(--fs)); line-height:1.66; color:var(--ink-soft); margin:0 0 12px; max-width:68ch;}
.pt-acts{display:flex; gap:10px; flex-wrap:wrap; margin-top:14px;}
.pt-bar{height:4px; border-radius:999px; background:var(--rule); overflow:hidden; margin-bottom:18px;}
.pt-bar i{display:block; height:100%; background:var(--indigo); transition:width .22s var(--ease);}
.pt-qtext{font-family:var(--display); font-size:calc(19px * var(--fs)); line-height:1.62; color:var(--ink);}
.pt-note{font-size:calc(12px * var(--fs)); color:var(--ink-faint); line-height:1.5; margin:14px 0 0;}
.pt-list{display:flex; flex-direction:column; margin:16px 0 0; border-top:1px solid var(--rule);}
.pt-row{display:grid; grid-template-columns:20px 1fr auto; gap:10px; align-items:baseline;
  padding:8px 0; border-bottom:1px solid var(--rule); font-size:calc(13.5px * var(--fs));}
.pt-mark{color:var(--ink-faint); text-align:center;}
.pt-row.ok .pt-mark{color:var(--good);}
.pt-ans{color:var(--ink); font-weight:600;}
.pt-you{color:var(--ink-faint); font-style:italic;}

/* ANSWER BEFORE REVEALING (see deckAttempt in app.js) — the held-back Reveal, its escape hatch and the
   hint that says why. The hint is `hidden` rather than removed, so nothing reflows when it goes. */
.reveal-cta{display:flex; gap:10px; align-items:center; flex-wrap:wrap; justify-content:center;}
.reveal-cta .btn:disabled{opacity:.45; cursor:not-allowed;}
.reveal-hint{font-size:calc(12.5px * var(--fs)); color:var(--ink-faint); flex-basis:100%; text-align:center;}
.reveal-hint[hidden]{display:none;}
/* the grade the "I don't know" button suggests — a ring, never a fill: the reader still chooses */
.grade.grade-suggest{box-shadow:inset 0 0 0 2px currentColor;}

/* ELABORATION PROMPTS (see elabPromptHTML in app.js) — one per session, between the answer and the
   background. THE INDEX TAB (Sep 2026, on request, choosing design 5 of six rendered alternatives): a
   panel with a squared 3px corner and a hairline edge, with the section's name riding its TOP EDGE as a
   tab in the card's own answer ink, the way an index tab rides the edge of a page. The label was a line
   of small caps sitting inside the box, which is the same thing said quietly; on the edge it names the
   panel from outside it, which is what lets the panel itself be plain.

   IT IS PAPER ON CARD, WHERE THE MOCK WAS CARD ON PAPER, AND THAT IS THE SAME STEP MIRRORED. The design
   was drawn as a white card standing on the page's own ground; this block lives INSIDE the study card,
   which is already `--card`, so a white panel there would have no edge at all. `--paper-2` is one step
   the other way and reads as the same distinct object.

   THE TAB'S GROUND IS MIXED 72% TOWARDS THE INK, AND THAT RATIO IS SOLVED RATHER THAN CHOSEN. A tab is
   reversed type — the label takes `--card` — so its legibility is the contrast between `--zh` and
   `--card`, and measured across all fifteen themes in both modes that pair fails the 4.5:1 bar three
   times (synth 3.55, marble night 4.01, academy night 4.49). Mixing towards `--ink` moves
   the ground AWAY from the card in BOTH directions with no per-theme table, `--ink` being by definition
   the far end from `--card`: at 72% the worst case on the shelf is marble night at 5.71. (Re-measured
   Sep 2026 when Opalite became the default: it used to BE the worst case at both figures — 2.98 plain
   and 4.72 mixed — and the deeper red it was promoted with takes it to 6.10 and 8.46.) The plain `--zh` is
   declared first so a browser without `color-mix` still gets a readable tab rather than none. */
.elab{position:relative; border:1px solid var(--rule); border-radius:3px; background:var(--paper-2);
  padding:24px 16px 15px; margin:20px 0 4px; display:flex; flex-direction:column; gap:0;}
.elab-kind{position:absolute; top:calc(-10px * var(--fs)); left:15px;
  background:var(--zh); background:color-mix(in srgb, var(--zh) 72%, var(--ink));
  color:var(--card); font-size:calc(9.5px * var(--fs)); font-weight:600; letter-spacing:.13em;
  text-transform:uppercase; padding:calc(4px * var(--fs)) calc(10px * var(--fs));
  border-radius:2px; line-height:1.25; white-space:nowrap;}
.elab-q{margin:0; font-family:var(--serif); font-size:calc(16px * var(--fs)); line-height:1.6; color:var(--ink);}
/* THREE QUESTIONS, EACH WITH ITS ANSWER BEHIND A BUTTON (Sep 2026, on request) — see elabPromptHTML.
   A row is question + button on one line, with the answer uncovered underneath it. The button is
   `flex:none` and the question `flex:1 1 auto` with `min-width:0`, so a long question wraps and the
   button keeps its size rather than being squeezed to a word a line; below 520px the button drops to its
   own line under the question, because at that width a right-hand column of "Show answer" leaves the
   question about half the card. The rule between items is what stops three questions reading as one
   paragraph with buttons in it.

   IT IS SET IN THE CARD'S OWN TYPE (Sep 2026, on request: "change the fonts so it fits better with the
   rest of the card"). The block was in the display face at the size of a heading, which read as a second
   card sitting inside the first. Both halves take `--serif`, the face the question and the background are
   already set in: the QUESTION at the abstract's own size in the full ink, the ANSWER at the abstract's
   own size, leading and quieter ink exactly — so uncovering one reads as a paragraph of the card rather
   than as a panel that has opened. NEITHER IS BOLD (Sep 2026, on request, having been for a few hours):
   what tells a question from its answer is the ink and the rule the answer is indented under, and three
   bold lines in a row read as three headings over a card whose own prose is not set that way. */
/* AND EACH ITEM IS A NATIVE `<details>` WITH A CHEVRON (Sep 2026, on request) — see elabPromptHTML for
   why the button went. The summary is the whole row, so the target is the width of the card rather than
   of two words; `list-style:none` plus the WebKit pseudo-element is what takes the browser's own marker
   off, and both are needed. The chevron rotates rather than swapping glyph, so the reduced-motion
   killswitch lands it on its true angle with no rule of its own. */
.elab-item{padding-top:10px; border-top:1px solid var(--rule);}
.elab-item:first-of-type{padding-top:2px; border-top:0;}
.elab-qrow{display:flex; align-items:flex-start; gap:12px; cursor:pointer; list-style:none;
  padding:2px 0; border-radius:7px;}
.elab-qrow::-webkit-details-marker{display:none;}
.elab-qrow:focus-visible{outline:2px solid var(--indigo); outline-offset:3px;}
.elab-qrow .elab-q{flex:1 1 auto; min-width:0;}
.elab-chev{flex:none; color:var(--zh); display:flex; align-items:center; height:calc(16px * var(--fs) * 1.6);}
.elab-chev svg{width:15px; height:15px; display:block; transition:transform .18s var(--ease);}
.elab-item[open] > .elab-qrow .elab-chev svg{transform:rotate(180deg);}
.elab-qrow:hover .elab-chev{color:var(--ink);}
/* the legacy item (a `card.why` written before the answers were authored — see cardWhy) is a row and a
   button rather than a fold, since its answer is a block of the card's own background */
.elab-legacy .elab-qrow{cursor:default;}
.elab-legacy .elab-show{flex:none; white-space:nowrap; padding:6px 11px; font-size:calc(11.5px * var(--fs));}
.elab-legacy .elab-show:disabled{opacity:.4; cursor:default;}
/* the answer itself: the card's own prose, set off by a rule down its left edge so it reads as a reply to
   the line above rather than as another paragraph of the background */
.elab-wrap{padding-top:7px;}
.elab-a{margin:0; padding-left:13px; border-left:3px solid var(--zh);
  font-family:var(--serif); font-size:calc(16px * var(--fs)); line-height:1.66; color:var(--ink-soft);}
.elab-a[hidden]{display:none;}
@media (max-width:520px){
  .elab-legacy .elab-qrow{flex-direction:column; align-items:flex-start; gap:8px;}
}
/* `.elab-box` / `.elab-acts` / `.elab-note` were the self-explanation fallback's textarea and its
   footnote, and went with it in Sep 2026 (see elabPromptHTML) — the Think-it-through block is now the
   authored why-questions and nothing else, so there is no writing surface in it to style. */
/* the paragraph "Show me" opens onto, marked for a moment. A background wash rather than a border, so
   nothing moves — a border would relayout the prose the reader has just been sent to read. */
.abstract.elab-mark{background:var(--indigo-wash); border-radius:8px;
  box-shadow:0 0 0 6px var(--indigo-wash); transition:background .3s var(--ease), box-shadow .3s var(--ease);}

/* CAUSAL CHAINS — "what came of this" (see cardLeadsToHTML in app.js). A list of links at the foot of
   the answer, outside the Background fold. Set as rows rather than as a sentence: each edge is a
   separate claim about the past and reads as one. */
.leadsto{margin-top:18px;}
.lt-list{list-style:none; margin:8px 0 0; padding:0; display:flex; flex-direction:column; gap:2px;}
.lt-row{margin:0;}
.lt-go{display:flex; flex-direction:column; gap:2px; width:100%; text-align:left;
  background:none; border:0; border-left:2px solid var(--rule); padding:7px 0 7px 12px;
  cursor:pointer; color:inherit; font:inherit; transition:border-color .22s var(--ease);}
.lt-go:hover{border-left-color:var(--zh);}
.lt-go:focus-visible{outline:2px solid var(--indigo); outline-offset:2px;}
.lt-term{font-family:var(--display); font-weight:600; font-size:calc(15px * var(--fs)); color:var(--zh);}
.lt-how{font-size:calc(13px * var(--fs)); line-height:1.5; color:var(--ink-soft);}
/* the peek sheet a causal link opens */
.cp-dates{margin:2px 0 10px;}
.cp-lead{margin:0 0 4px; font-size:calc(14.5px * var(--fs)); line-height:1.62; color:var(--ink-soft);}

/* ELABORATED FEEDBACK ON A MISSED CARD (see the gradeCloze block in app.js). The defining sentence,
   directly under the answer and outside the Background fold, so a reader whose fold is collapsed still
   gets an explanation rather than a bare term. Set in the answer's own red so it reads as part of the
   correction rather than as the start of the prose below it. */
.miss-lead{margin:12px 0 0; padding:11px 13px; border-radius:10px;
  background:var(--zh-wash); border:1px solid var(--rule);}
.miss-kind{display:block; font-size:calc(10px * var(--fs)); letter-spacing:.06em;
  text-transform:uppercase; color:var(--zh); margin-bottom:5px;}
.miss-lead p{margin:0; font-size:calc(14px * var(--fs)); line-height:1.6; color:var(--ink);}
/* the wrong option a reader actually chose, explained (see PAGES.challenge). Set apart from the answer's
   own note above it by a rule rather than a colour, so it reads as a second paragraph and not a warning. */
.mc-chose{border-top:1px solid var(--rule); padding-top:10px; margin-top:10px;}

/* THE CONFUSION ROW (see noteConfusion in app.js) — the one thing on the home page that is personal to
   this reader. Deliberately quieter than the review banner above it: it is a suggestion, not the day's
   work, and it must never look like a warning about something being wrong with their study. */
.confuse-row{display:flex; flex-direction:column; gap:4px; width:100%; text-align:left;
  background:var(--paper-2); border:1px solid var(--rule); border-left:3px solid var(--newterm);
  border-radius:12px; padding:13px 15px; cursor:pointer; color:inherit; font:inherit;
  transition:border-color .22s var(--ease), transform .22s var(--ease);}
.confuse-row:hover{transform:translateY(-1px); border-color:var(--newterm);}
.confuse-row:focus-visible{outline:2px solid var(--indigo); outline-offset:2px;}
.cf-main{font-family:var(--display); font-size:calc(15.5px * var(--fs)); color:var(--ink);}
.cf-note{font-size:calc(12.5px * var(--fs)); color:var(--ink-faint); line-height:1.45;}
.cf-go{font-size:calc(12.5px * var(--fs)); font-weight:600; color:var(--newterm); margin-top:2px;}

/* THE READER'S OWN FORGETTING CURVE, AND THE RECALLED-ONCE LIST (see forgettingCurveHTML in app.js).
   Both are `.rs-card`s, so they take the statistics grid's own frame and need no layout of their own. */
.fg-bars{display:flex; align-items:flex-end; gap:8px; margin-top:6px;}
.fg-col{flex:1 1 0; min-width:0; display:flex; flex-direction:column; align-items:center; gap:5px;}
.fg-track{width:100%; height:74px; background:var(--paper-2); border-radius:5px;
  display:flex; align-items:flex-end; overflow:hidden;}
.fg-track i{display:block; width:100%; background:var(--good); border-radius:5px 5px 0 0;}
.fg-pct{font-size:calc(11.5px * var(--fs)); font-weight:600; color:var(--ink);}
.fg-lbl{font-size:calc(10px * var(--fs)); color:var(--ink-faint); text-align:center; line-height:1.25;}
.so-lead{margin:4px 0 8px; font-size:calc(13px * var(--fs)); line-height:1.55; color:var(--ink-soft);}
.so-names{margin:0 0 12px; font-size:calc(13px * var(--fs)); line-height:1.6; color:var(--ink);}

/* "HOW FOLIO STUDIES YOU" (see PAGES.how in app.js) — a page read once, so it is set as prose at a
   comfortable measure rather than as a grid of cards. The claim is the heading, because the claim is
   the point: a reader skimming should come away with the four sentences even if they read nothing else. */
.how-list{display:flex; flex-direction:column; gap:22px;}
.how-claim{border-left:3px solid var(--indigo); padding:2px 0 2px 16px;}
.how-claim h3{margin:0 0 8px; font-family:var(--display); font-size:calc(19px * var(--fs));
  line-height:1.35; font-weight:600; color:var(--ink);}
.how-claim p{margin:0 0 8px; font-size:calc(14.5px * var(--fs)); line-height:1.68;
  color:var(--ink-soft); max-width:72ch;}
.how-so{color:var(--ink) !important; font-style:italic;}
.how-foot{margin-top:26px; border-top:1px solid var(--rule); padding-top:16px;
  display:flex; flex-direction:column; gap:12px; align-items:flex-start;}
.how-foot p{margin:0; max-width:72ch; font-size:calc(13.5px * var(--fs)); line-height:1.62; color:var(--ink-soft);}

/* ============================================================
   SEARCH (Sep 2026) — see PAGES.search in app.js
   One field over the cards, the glossary and the shelf. The page is deliberately plain: it is read
   fast, scanned rather than admired, and every row is a target. Groups are separated by their heading
   alone — a card per group would spend three borders saying what three words already say.
   ============================================================ */
.srch-bar{margin:0 0 22px;}
.srch-input{
  width:100%; appearance:none; box-sizing:border-box;
  padding:13px 15px; border-radius:10px; border:1.5px solid var(--rule); background:var(--card);
  font-family:var(--serif); font-size:calc(17px * var(--fs)); color:var(--ink);
}
.srch-input::placeholder{color:var(--ink-quiet);}
.srch-input:focus{outline:none; border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash);}
.srch-input::-webkit-search-cancel-button{cursor:pointer;}

.srch-group{margin:0 0 26px;}
.srch-gh{
  display:flex; align-items:baseline; gap:9px; margin:0 0 8px;
  font-family:var(--mono); font-size:calc(10.5px * var(--fs)); font-weight:600;
  letter-spacing:.14em; text-transform:uppercase; color:var(--indigo);
}
.srch-n{font-weight:500; color:var(--ink-quiet); letter-spacing:.06em;}
.srch-gh::after{content:""; flex:1 1 auto; height:1px; background:var(--rule);}
.srch-list{list-style:none; margin:0; padding:0;}
.srch-row{
  appearance:none; border:0; background:none; cursor:pointer; width:100%; text-align:left;
  display:flex; align-items:baseline; gap:12px; flex-wrap:wrap;
  padding:10px 10px; margin:0 -10px; border-radius:8px;
}
.srch-row:hover{background:var(--paper-2);}
.srch-row:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:-2px;}
.srch-label{font-family:var(--serif); font-size:calc(16.5px * var(--fs)); color:var(--ink); flex:0 1 auto;}
/* the line that tells two cards with the same answer term apart — Korea and Japan share eighteen */
.srch-sub{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:var(--ink-quiet); letter-spacing:.04em; margin-left:auto; flex:none;}
@media (max-width:520px){ .srch-sub{margin-left:0; flex-basis:100%;} }
.srch-more,.srch-hint{font-size:calc(14.5px * var(--fs)); color:var(--ink-quiet); margin:6px 0 0; line-height:1.55;}
.srch-hint{margin-top:4px;}
.srch-hint .lnk{
  appearance:none; border:0; background:none; padding:0; cursor:pointer; font:inherit;
  color:var(--indigo); text-decoration:underline; text-underline-offset:3px;
}

/* the way in from the Collections page, which is how every phone reaches it */
.srch-open{
  appearance:none; cursor:pointer; width:100%; box-sizing:border-box;
  display:flex; align-items:center; gap:10px; margin:0 0 16px; padding:11px 14px;
  border:1px solid var(--rule); border-radius:10px; background:var(--card); color:var(--ink-quiet);
  font-family:var(--serif); font-size:calc(15px * var(--fs)); text-align:left;
}
.srch-open svg{width:16px; height:16px; flex:none;}
.srch-open:hover{border-color:var(--indigo); color:var(--ink);}
.srch-open:focus-visible{outline:none; border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash);}

/* ============================================================
   NEARBY IN THIS COLLECTION (Sep 2026) — see relatedCardsHTML in app.js
   The other half of the causal strip's slot. `leadsTo` is an AUTHORED claim that one thing led to
   another and is set on one card in the corpus; this is ranked from the tags every card already
   carries, so it reaches all of them. Quieter than the strip above it accordingly: no rule, no
   heading weight, just a label and a row of names.
   ============================================================ */
.kin{margin:18px 0 0; padding-top:14px; border-top:1px solid var(--rule);}
.kin .label{
  display:block; margin-bottom:7px;
  font-family:var(--mono); font-size:calc(10px * var(--fs)); font-weight:600;
  letter-spacing:.14em; text-transform:uppercase; color:var(--ink-quiet);
}
.kin-list{list-style:none; margin:0; padding:0; display:flex; flex-wrap:wrap; gap:7px;}
.kin-row{
  appearance:none; cursor:pointer; padding:6px 11px; border-radius:999px;
  border:1px solid var(--rule); background:var(--paper-2); color:var(--ink-soft);
  font-family:var(--serif); font-size:calc(14px * var(--fs)); text-align:left;
}
.kin-row:hover{border-color:var(--indigo); color:var(--ink);}
.kin-row:focus-visible{outline:none; border-color:var(--indigo); box-shadow:0 0 0 3px var(--indigo-wash);}

/* ---------- one card at its own address (#card/<id>) ---------- */
.cardpg-card{margin:0 auto;}
.cardpg-acts{display:flex; flex-wrap:wrap; gap:10px; margin:20px 0 0; justify-content:center;}

/* ============================================================
   COLLECTIONS: HOW FAR THROUGH, AND HOW DENSE (Sep 2026)
   ============================================================ */
/* The reach label — see COLLECTION_TARGET in app.js. It sits on the title row beside the name, in the
   quiet mono the counts already use, because it is the same kind of fact as a count and putting it in
   a second register would make it compete with the title. */
.coll-reach{
  font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.05em;
  color:var(--ink-quiet); white-space:nowrap; margin-left:auto; flex:none;
  display:inline-flex; align-items:baseline; gap:6px;
}
.coll-reach-pct{color:var(--indigo); font-weight:600;}
.coll-reach.is-done{
  color:var(--good); font-weight:600; letter-spacing:.11em; text-transform:uppercase;
}
/* the long form is the sentence; the short one is the same fact in four characters. Below 720px the
   sentence does not fit beside a collection's name — it ran under the + button and pushed the title
   to three lines — so the percentage stands alone there and the pct chip beside it goes with it. */
.coll-reach .cr-short{display:none;}
@media (max-width:720px){
  .coll-reach{margin-left:auto;}
  .coll-reach .cr-long,
  .coll-reach .coll-reach-pct{display:none;}
  .coll-reach .cr-short{display:inline; color:var(--indigo); font-weight:600;}
  .coll-reach.is-done .cr-short{color:var(--good);}
}
/* the title takes the room that is left, and ellipsises rather than wrapping to three lines */
.collection-title-row{min-width:0;}
.coll-compact .collection-title{
  flex:1 1 auto; min-width:0; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}

/* the tab row now carries the density toggle at its far end */
.coll-tabrow{display:flex; align-items:center; gap:10px; margin:0 0 18px;}
.coll-tabrow .coll-tabs{margin:0; flex:1 1 auto; min-width:0;}
.coll-dense{
  appearance:none; cursor:pointer; flex:none; display:inline-flex; align-items:center; gap:6px;
  padding:7px 11px; border-radius:999px; border:1px solid var(--rule); background:var(--card);
  font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.1em; text-transform:uppercase;
  font-weight:600; color:var(--ink-quiet);
}
.coll-dense svg{width:13px; height:13px;}
.coll-dense:hover{color:var(--ink); border-color:var(--indigo);}
.coll-dense.on{background:var(--indigo); border-color:var(--indigo); color:#fff;}
.coll-dense:focus-visible{outline:none; box-shadow:0 0 0 3px var(--indigo-wash);}
@media (max-width:520px){ .coll-dense span{display:none;} .coll-dense{padding:7px 9px;} }

/* COMPACT. A collection row is ~230px tall on a phone, so twenty of them is over 4,000px of scrolling
   to see what is on offer — and every row below the first screen carries an identical empty progress
   bar. Compact keeps the name, the count and the reach, and drops the wash, the icon, the bar and the
   deco to a single line per collection. The rows stay the same ELEMENTS, so every handler, drag target
   and test selector still resolves; only their height changes. */
.coll-compact .collection-row{padding-block:9px; min-height:0;}
.coll-compact .collection-deco{display:none;}
.coll-compact .coll-ic{width:22px; height:22px;}
.coll-compact .coll-ic svg{width:20px; height:20px;}
.coll-compact .deck-prog{display:none;}
.coll-compact .collection-title{font-size:calc(17px * var(--fs));}
.coll-compact .udeck-sub,
.coll-compact .node-sub{display:none;}
.coll-compact .node-row{padding-block:7px;}

/* ============================================================
   HOME PAGE — the Oct 2026 redesign (see docs/mockups/home-redesigns/final/)
   ============================================================
   The page kept every hook the handlers and the suites read (#b-review, .review-title, .stat.st-*, the
   .active-deck rows with their .dkc counts and .dk-prog bar, .game-tile[data-game], #b-addDecks,
   #b-about, .site-ver) and changed what they look like. Everything below is written AFTER the rules it
   overrides and at equal or higher specificity, so the earlier blocks keep working for the states this
   page does not draw. The parts, in page order:
     · the study banner (`.rv-banner`): deep indigo in both themes, "N cards" in rose, the three piles in
       their own colours plus the estimate, a Start button as wide as the pile row, and the top three cards
       of the pile fanned beside it (`.rv-preview`, desktop and tablet only);
     · the streak ribbon (`.streak-ribbon`): the run in weeks, each ending in its chest;
     · the collections header (`.rv-sec-h`) with the legend (the "+ Add decks" link stood at its right
       until Oct 2026, and the deck list's Edit control beside the heading for a day after that; the
       Collections tab is the route now), then the deck rows
       as FILL ROWS — the row's own tint is the share of the deck learned (the existing `.dk-prog .fill-l`,
       stretched to the row's height), with the three piles as colour boxes at the right;
     · the games as LIST-TILES (icon disc, full name, score, a tick), three across;
     · Continue reading beside This week (`.home-two`), and a foot carrying About, Changelog and the
       version line — which left the top of the page on request ("never mention the current date" there). */
.page .rv-banner, .page .rv-banner:hover{
  --sky:#2A3478; --sky-2:#1B2150; --sky-ink:#F2F3FF; --sky-rose:#F4B9C8; --sky-blue:#9DB4FF; --sky-green:#8FD9B6;
  border:0; border-left:0; border-radius:20px; padding:26px 30px; gap:24px; align-items:center;
  /* the solid colour under the gradient is what a contrast sweep reads (test-a11y's bgOf), and what paints while the gradient is unsupported */
  background:var(--sky-2) linear-gradient(135deg, var(--sky) 0%, var(--sky-2) 100%); color:var(--sky-ink);
}
body.night .page .rv-banner, body.night .page .rv-banner:hover{--sky:#1A1F4D; --sky-2:#0F1230; background:var(--sky-2) linear-gradient(135deg, var(--sky) 0%, var(--sky-2) 100%);}
.page .rv-banner.done, .page .rv-banner.won{background:var(--sky-2) linear-gradient(135deg, var(--sky) 0%, var(--sky-2) 100%);}
.rv-banner .body{flex:1; min-width:0;}
/* the eyebrow is GREY (Oct 2026, on request) — at .62 it still clears 4.5:1 on the gradient's lighter end */
.rv-banner .hero-eyebrow{display:block; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.16em; text-transform:uppercase; font-weight:600; color:rgba(242,243,255,.62);}
.page .rv-banner h2.review-title{font-family:var(--serif); font-weight:500; font-size:calc(38px * var(--fs)); line-height:1.08; letter-spacing:-.015em; margin:8px 0 16px; color:#fff;}
.page .rv-banner h2.review-title .rv-n{font-weight:600; color:var(--sky-rose);}
.page .rv-banner.hero h2.review-title{font-size:calc(34px * var(--fs));}
.rv-banner .desc{color:rgba(242,243,255,.82); margin-bottom:14px;}
/* the three piles, the estimate, and the Start button under them spanning exactly their width */
.page .rv-banner .meta{display:grid; grid-template-columns:auto auto auto auto; gap:0 24px; width:max-content; max-width:100%; margin:0; align-items:end; justify-items:center;}
.page .rv-banner .stat b{font-family:var(--sans); font-weight:800; font-size:calc(26px * var(--fs)); color:#fff; font-variant-numeric:tabular-nums;}
.page .rv-banner .stat span{color:rgba(242,243,255,.7);}
.page .rv-banner .stat.st-new b{color:var(--sky-blue);}
.page .rv-banner .stat.st-learn b{color:var(--sky-rose);}
.page .rv-banner .stat.st-rev b{color:var(--sky-green);}
.page .rv-banner .stat.stat-zero b{color:rgba(242,243,255,.4);}
.page .rv-banner .stat.st-est{padding-left:22px; border-left:1px solid rgba(255,255,255,.18);}
.page .rv-banner .stat.st-est b{color:#fff; white-space:nowrap;}
.page .rv-banner .cta{grid-column:1 / -1; margin:18px 0 0; display:block; width:100%;}
.page .rv-banner .cta .btn{display:flex; justify-content:center; background:#fff; color:var(--sky); border:0; font-family:var(--mono); font-size:calc(11.5px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; font-weight:700; padding:13px 20px; border-radius:11px;}
.page .rv-banner.hero .meta{display:flex;} .page .rv-banner.hero .cta{width:auto; margin:0;}
/* the level bar: in the stack with the piles, as wide as they are, with air above and below it (Oct 2026, on request) */
.page .rv-banner .rv-stack{display:grid; grid-template-columns:minmax(0, max-content); width:max-content; max-width:100%;}
.page .rv-banner .rv-stack > .meta{width:100%;}
.page .rv-banner .xp{margin:20px 0; max-width:none; width:100%; gap:7px;}
/* …and the title and the caught-up line above it carry no bottom margin of their own, so the air above the
   bar is the bar's own 20px, the same as below it (Oct 2026, on request) */
.page .rv-banner .rv-stack{margin-top:0;}
.page .rv-banner h2.review-title, .rv-banner .desc{margin-bottom:0;}
/* THE LEVEL BAR IS GOLD IN BOTH THEMES (Oct 2026, on request: "make the level bar gold in day mode like it
   already is in dark mode") — it was rose by day because this rule outranked `.banner .xp-fill` while the
   night rule outranked this one. The night gold, since the banner is deep indigo in both themes. */
.rv-banner .xp-lvl{color:#E6C765;} .rv-banner .xp-count{color:rgba(242,243,255,.6);}
.rv-banner .xp-track{background:rgba(255,255,255,.14); border-color:transparent; height:6px;}
.rv-banner .xp-fill{background:linear-gradient(90deg, #D4AA3C, #F2DC96);}
.rv-banner .glyph{display:none;}
.review-group .rv-banner .gt-check{top:14px; right:14px;}
/* the banner keeps all four corners (Oct 2026, on request: "the corners are too sharp") — the rule that
   squared its foot dates from when the deck list was joined to it, and the rows are separate cards now */
.review-group.has-active > #b-review.rv-banner{border-bottom-left-radius:20px; border-bottom-right-radius:20px;}
/* the fanned preview of the pile's top three */
.rv-preview{position:relative; width:300px; height:200px; flex:none;}
.pv-card{position:absolute; inset:0; background:var(--card); color:var(--ink); border:1px solid var(--rule); border-radius:12px; padding:16px 18px; box-shadow:var(--shadow-lg); transform-origin:50% 100%; display:flex; flex-direction:column; text-align:left;}
.pv-card.pv3{transform:rotate(-6deg) translate(-12px, 4px); opacity:.85;}
.pv-card.pv2{transform:rotate(3deg) translate(10px, 2px); opacity:.93;}
.pv-eb{display:flex; justify-content:space-between; gap:8px; font-family:var(--mono); font-size:9.5px; letter-spacing:.16em; text-transform:uppercase; color:var(--indigo); font-weight:600;}
.pv-eb em{font-style:normal; color:var(--zh);}
.pv-card p{font-family:var(--serif); font-size:14.5px; line-height:1.4; margin:10px 0 0; overflow:hidden; display:-webkit-box; -webkit-line-clamp:4; -webkit-box-orient:vertical;}
.pv-blank{display:inline-block; width:38px; border-bottom:2px solid var(--ochre);}
.pv-ft{margin-top:auto; padding-top:8px; font-family:var(--mono); font-size:9.5px; letter-spacing:.12em; text-transform:uppercase; color:var(--ink-faint);}
/* the streak ribbon */
.streak-ribbon{display:flex; align-items:center; gap:14px; flex-wrap:wrap; margin-top:12px; background:var(--card); border:1px solid var(--rule); border-radius:14px; padding:12px 18px; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-quiet);}
.sr-n{font-family:var(--sans); font-size:calc(24px * var(--fs)); font-weight:800; color:var(--good); letter-spacing:-.03em; line-height:1;}
.sr-days{display:flex; gap:10px; align-items:center;}
.sr-wk{display:inline-flex; gap:4px; align-items:center;}
.sr-wk i{display:block; width:14px; height:14px; border-radius:4px; background:var(--good); box-sizing:border-box;}
.sr-wk i.o{background:var(--paper-2); border:1px solid var(--rule);}
.sr-wk i.t{background:transparent; border:2px solid var(--good);}
.sr-chest{display:inline-block; width:30px; margin-left:6px; color:var(--ink-faint); opacity:.55;}
.sr-chest .chest-svg{width:100%; height:auto; display:block;}
.sr-chest.won{color:#C39A2E; opacity:1;} body.night .sr-chest.won{color:#E6C765;}
.sr-note{font-family:var(--sans); font-size:calc(12.5px * var(--fs)); letter-spacing:0; text-transform:none; color:var(--ink-quiet);}
.sr-note b{color:#C39A2E; font-weight:800;} body.night .sr-note b{color:#E6C765;}
.sr-best{margin-left:auto; white-space:nowrap;}
/* the collections header and the deck rows */
.rv-sec-h{display:flex; justify-content:space-between; align-items:center; flex-wrap:wrap; gap:8px 12px; margin:24px 0 10px; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.16em; text-transform:uppercase; color:var(--ink-quiet); font-weight:600;}
/* the heading (the deck list's Edit control stood just behind it for a day in Oct 2026 and is gone with
   its whole editor mode, on request) */
.rv-sec-l{display:inline-flex; align-items:center; flex-wrap:wrap; gap:6px 10px; min-width:0;}
.rv-legend{display:inline-flex; gap:10px; font-size:calc(10px * var(--fs)); letter-spacing:.12em;}
.rv-legend i{display:inline-block; width:7px; height:7px; border-radius:50%; margin-right:4px; vertical-align:1px;}
.rv-legend .n i{background:var(--indigo-bright);} .rv-legend .l i{background:var(--zh);} .rv-legend .r i{background:var(--good);}
.review-group .active-decks{display:grid; gap:8px;}
.review-group .active-deck, body.night .review-group .active-deck, .review-group .active-deck:last-child, .review-group .active-deck.dk-last{
  background:var(--card); border:1px solid var(--rule); border-left:4px solid var(--dk-accent); border-radius:12px; padding-top:12px; padding-bottom:12px; padding-right:16px; gap:14px; animation:none;
}
/* a GROUP's header row is shorter than the rows under it and painted in a deeper wash of its colour */
.review-group .active-deck.deck-group, .review-group .active-deck.dk-langhead, body.night .review-group .active-deck.deck-group, body.night .review-group .active-deck.dk-langhead{padding-top:4px; padding-bottom:4px; min-height:0; background:linear-gradient(90deg, color-mix(in srgb, var(--dk-accent) 16%, var(--card)), color-mix(in srgb, var(--dk-accent) 6%, var(--card)));}
.review-group .active-deck > :not(.dk-body):not(.dk-grip):not(.gt-check):not(.gt-seal){position:relative; z-index:1;}
.review-group .active-deck > .dk-grip{z-index:2;}
/* THE ICON AND THE GRIP STAND ABOVE THE TITLE'S BLOCK (Oct 2026, on a bug report: the deck's icon vanished once any
   card was learned). `.dk-body` is a stacking context and the fill under the row lives inside it, so the opaque
   learned fill painted over the icon to its left; the icon at z-index 1 came earlier in the order and lost. */
.review-group .active-deck > .dk-grip{z-index:3;}
/* the :not() chain only matches the specificity of the z-index:1 rule above, which it has to beat */
.review-group .active-deck > .dk-ic:not(.dk-body):not(.dk-grip):not(.gt-check):not(.gt-seal){z-index:3;}
.review-group .active-deck > .dk-body{z-index:1; order:2; flex:1;}
.review-group .active-deck > .dk-grip{order:0;} .review-group .active-deck > .dk-ic{order:1;}
.review-group .active-deck > .dk-counts{order:3;} .review-group .active-deck > .dk-dl{order:4;}
.review-group .active-deck > .dk-chev, .review-group .active-deck > .dk-chev-gap{order:5;}
.review-group .active-deck .dk-title{font-weight:700; font-size:calc(15px * var(--fs));}
.review-group .active-deck .dk-counts{display:inline-grid; grid-template-columns:repeat(3, 32px); gap:4px; min-width:0;}
/* a deck finished for the day shows its tick and nothing to count (Oct 2026, on request) */
.review-group .active-deck.dk-done .dk-counts{display:none;}
/* the three piles are COLOURED FIGURES, not colour boxes (Oct 2026, on request: "remove the boxes and make
   the numbers themselves coloured") — the same blue / red / green, now on the figure; a zero stays grey */
.review-group .active-deck .dkc{display:grid; place-content:center; height:28px; font-family:var(--mono); font-size:calc(14px * var(--fs)); font-weight:700; background:none;}
.review-group .active-deck .dkc-new{color:var(--indigo-bright);}
.review-group .active-deck .dkc-learn{color:var(--zh);}
.review-group .active-deck .dkc-rev{color:var(--good, #4F9D67);}
.review-group .active-deck .dkc.dkc-zero, body.night .review-group .active-deck .dkc.dkc-zero{color:var(--ink-faint); font-weight:500;}
/* the fill: the learned share tints the row from the left (the studied share's line is not drawn here) */
.review-group .active-deck .dk-prog .track{position:absolute; left:0; top:0; bottom:0; right:0; height:auto; background:transparent; border-radius:0; z-index:-1; pointer-events:none;}
.review-group .active-deck .dk-prog .fill{position:absolute; left:0; top:0; bottom:0; height:auto; border-radius:0; background:color-mix(in srgb, var(--dk-accent) 10%, transparent);}
/* the learned share is a clearly deeper tint than the studied one's 10% (Oct 2026, on request: at 20% fading to 9% it
   read as uncoloured) */
/* …AND IT IS ONE FLAT TINT WITH NO DARK EDGE (Oct 2026, on request: the deeper share "still does not look
   properly", and "the line to mark it should not be darker than the progress left to it"). It faded from 34% to
   24% towards its right end and then stopped at a 3px line of the full accent, so the band grew paler exactly
   where it met the darkest stroke on the row. It is one even tint now and its own edge is the marker.
   …AND DEEPER AGAIN, 28% → 40% (Oct 2026, on request: against the studied share's 10% it "sometimes has so
   little contrast … that it is almost impossible to see" — Russia's, on a tablet). Still well short of the full
   accent on the row's left edge, which it must not match. */
.review-group .active-deck .dk-prog .fill-l{background:color-mix(in srgb, var(--dk-accent) 40%, var(--card)); border-right:0;}
.review-group .active-deck .dk-prog.prog-done .fill-l{background:color-mix(in srgb, #C39A2E 22%, var(--card));}
/* the games */
.games-top{display:flex; justify-content:space-between; align-items:center; gap:12px; flex-wrap:wrap;}
.games-sec .games-head{text-align:left; margin:0; color:var(--ink-quiet);}
.games-sec .sweep-row{margin:0; justify-content:flex-end;}
.games-sec .game-grid{gap:10px;}
.games-sec button.game-tile, .games-sec .game-tile, body.night .games-sec button.game-tile, body.night .games-sec .game-tile{
  min-height:0; padding:10px 12px 10px 10px; border-radius:12px; background:var(--card);
  border:1px solid var(--rule); border-left:4px solid var(--gt-accent); box-shadow:var(--shadow-sm);
}
.games-sec .game-tile .gt-face{flex-direction:row; align-items:center; justify-content:flex-start; gap:12px;}
.games-sec .game-tile .gt-glyph, .games-sec .game-tile.done .gt-glyph, .games-sec .game-tile.won .gt-glyph, .games-sec button.game-tile .gt-glyph{
  display:grid; place-content:center; position:static; order:1; flex:none; width:40px; height:40px; border-radius:50%;
  background:color-mix(in srgb, var(--gt-accent) 18%, var(--card)); color:var(--gt-accent); opacity:1; font-size:calc(20px * var(--fs)); pointer-events:none;
}
.games-sec .game-tile .gt-glyph-svg svg{width:22px; height:22px;}
.games-sec .game-tile .gt-body{order:2; flex:1; min-width:0; display:flex; align-items:baseline; gap:8px; flex-wrap:wrap;}
.games-sec .game-tile .gt-title, .games-sec button.game-tile .gt-title{font-size:calc(15px * var(--fs)); color:var(--ink);}
.games-sec .game-tile .gt-sub{margin:0; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); color:var(--ink-quiet);}
.games-sec .game-tile .gt-eyebrow{display:none;}
.games-sec .game-tile.done .gt-face::after, .games-sec .game-tile.won .gt-face::after{display:none;}
.games-sec .game-tile .gt-check, .games-sec .game-tile .gt-seal{position:static; order:3; margin-left:auto; flex:none; width:21px; height:21px; clip-path:none; overflow:visible; background:var(--good, #4F9D67); box-shadow:none;}
.games-sec .game-tile .gt-seal{width:24px; height:24px; background:none;}
/* A FINISHED GAME IS NAMED BY A CHIP, NOT PAINTED (Oct 2026, on request: the filled tiles were "way too dark").
   The tile FILLED with its state colour, and the fill was the accent mixed three-quarters into a near-black so that
   white text could pass 4.5:1 — which is what turned the gold olive and the green teal; a solid fill cannot hold
   white words without going dark. Chosen from six rendered alternatives: the tile takes only a faint tint of the
   accent, the icon disc fills solid, and the mark at the right becomes a small pill that SAYS the state — Played
   in green, Perfect in gold — with the score staying beside the title as on an unplayed tile. The accent itself
   is untouched, so the chips and marks elsewhere that read `--gt-accent` keep their colour.
   THE WORD IS CSS `content`: the mark already carries the state as its aria-label (doneMarkHTML), so the visible
   word is a duplicate for sighted readers and the label stays the one spoken; a second text node would be read
   twice. The seal's wax, sweep and star stand down here — the pill is the mark. By night the pale accents are
   too light for white, so the pill's word and the disc's glyph take the paper colour instead. */
.games-sec .game-tile.done, .games-sec .game-tile.won, body.night .games-sec .game-tile.done, body.night .games-sec .game-tile.won{
  background:color-mix(in srgb, var(--gt-accent) 9%, var(--card));
}
.games-sec .game-tile.done .gt-glyph, .games-sec .game-tile.won .gt-glyph{background:var(--gt-accent); color:#fff;}
.games-sec .game-tile.done .gt-check, .games-sec .game-tile.won .gt-seal{
  width:auto; height:auto; display:inline-flex; align-items:center; gap:5px; padding:4px 9px 4px 7px; border-radius:999px;
  background:var(--gt-accent); background-image:none; color:#fff; clip-path:none; filter:none; animation:none;
  font-family:var(--mono); font-size:calc(10px * var(--fs)); font-weight:600; letter-spacing:.1em; text-transform:uppercase; line-height:1;
}
.games-sec .game-tile.done .gt-check::after{content:"Played";}
.games-sec .game-tile.won .gt-seal::after{content:"Perfect";}
body.night .games-sec .game-tile.done .gt-glyph, body.night .games-sec .game-tile.won .gt-glyph,
body.night .games-sec .game-tile.done .gt-check, body.night .games-sec .game-tile.won .gt-seal{color:var(--paper);}
.games-sec .game-tile .gt-check svg, .games-sec .game-tile .gt-seal-face svg{width:12px; height:12px; display:block;}
.games-sec .game-tile .gt-seal-face{color:inherit; filter:none;}
/* reading beside the week */
.home-two{display:grid; grid-template-columns:1fr 1fr; gap:16px;}
.home-box{position:relative; overflow:hidden; display:block; width:100%; background:var(--card); border:1px solid var(--rule); border-radius:16px; padding:18px 20px; box-shadow:var(--shadow-sm); min-width:0; text-align:left; color:var(--ink); font:inherit;}
button.home-box{appearance:none; cursor:pointer;}
button.home-box:focus-visible{outline:2px solid var(--indigo-bright); outline-offset:2px;}
.hb-k{display:block; font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.16em; text-transform:uppercase; color:var(--ink-quiet); font-weight:600; margin-bottom:10px;}
.hb-book{display:flex; gap:14px; align-items:center;}
.hb-spine{width:44px; height:62px; flex:none; border-radius:4px 7px 7px 4px; background:linear-gradient(90deg, var(--tile, var(--indigo)) 0 6px, color-mix(in srgb, var(--tile, var(--indigo)) 78%, var(--card)) 6px); box-shadow:var(--shadow-md);}
.hb-body{min-width:0;}
/* the title is ALWAYS whole (Oct 2026, on request): it wraps rather than ending in an ellipsis, which on a phone
   cut "The History of the Peloponnesian War" to its first five words */
.hb-body b{display:block; font-family:var(--serif); font-weight:600; font-size:calc(16px * var(--fs)); line-height:1.3; overflow-wrap:anywhere;}
.hb-body > span{display:block; font-size:calc(12.5px * var(--fs)); color:var(--ink-quiet);}
.hb-bar{display:block; height:4px; border-radius:2px; background:var(--paper-2); margin-top:8px; width:150px; max-width:100%; overflow:hidden;}
.hb-bar span{display:block; height:100%; background:var(--ochre);}
.hb-body em{display:block; font-style:normal; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ochre); margin-top:6px;}
.hb-empty{display:block; font-size:calc(13.5px * var(--fs)); color:var(--ink-quiet); line-height:1.5;}
.home-week{display:flex; flex-direction:column;}
.home-week .hb-k{align-self:flex-start;}
.home-week .hw-stats + .hb-k{margin-top:16px;}   /* the week's heading under the day's figures (Oct 2026) */
.hw-stats{flex:1; display:grid; grid-template-columns:1fr 1fr 1fr; gap:12px; align-content:center; justify-items:center; text-align:center;}
.hw-stats b{display:block; font-size:calc(26px * var(--fs)); font-weight:800; letter-spacing:-.03em; line-height:1; font-variant-numeric:tabular-nums;}
.hw-stats span{display:block; font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.12em; text-transform:uppercase; color:var(--ink-quiet); margin-top:4px;}
.hw-stats .g b{color:var(--good);}
.gdeco{position:absolute; right:-40px; bottom:-60px; width:150px; height:150px; border-radius:50%; border:1.5px solid var(--geo); opacity:.35; pointer-events:none;}
.gdeco i{position:absolute; inset:0; border-radius:50%; border:1px solid var(--geo);}
.gdeco i:nth-child(1){transform:scaleX(.25);} .gdeco i:nth-child(2){transform:scaleX(.62);} .gdeco i:nth-child(3){transform:scaleY(.25);} .gdeco i:nth-child(4){transform:scaleY(.62);}
/* the foot */
.home-foot{display:flex; align-items:center; gap:18px; flex-wrap:wrap; margin-top:10px; padding:16px 0 0;}
.home-foot .home-about, .home-foot .home-about:hover{width:auto; padding:0; text-align:left; font-size:calc(10.5px * var(--fs)); letter-spacing:.14em; color:var(--ink-faint);}
.home-foot .home-about:hover{color:var(--ink-soft);}
.home-foot .site-ver{margin:0 0 0 auto; font-size:calc(10.5px * var(--fs)); letter-spacing:.14em; text-transform:uppercase;}
@media (min-width:641px){ .home-foot .home-about{padding-top:0;} }
/* the middle column is the flexible one (Oct 2026, on request: the gap after the number was too wide) — with
   `auto auto 1fr` the week of dots spanning the first two columns stretched the number's column to fit */
/* ONE ROW (Oct 2026, on request, with the ribbon cut to the current week): the count and its label, then the
   week and its chest, each centred on the box's height, and Longest over the note filling the right */
.streak-ribbon{display:grid; grid-template-columns:auto auto auto 1fr; grid-template-rows:auto auto; align-items:center; gap:4px 10px;}
.streak-ribbon > .sr-n{grid-column:1; grid-row:1 / 3;} .streak-ribbon > .sr-t{grid-column:2; grid-row:1 / 3;}
.streak-ribbon > .sr-days{grid-column:3; grid-row:1 / 3; margin-left:12px;}
.streak-ribbon > .sr-best{grid-column:4; grid-row:1; justify-self:end; align-self:end; margin:0;}
.streak-ribbon > .sr-note{grid-column:4; grid-row:2; justify-self:end; align-self:start; text-align:right;}
.streak-ribbon:not(:has(> .sr-best)) > .sr-note{grid-row:1 / 3; align-self:center;}
/* THE PREVIEW SITS IN THE MIDDLE OF ITS ROOM (Oct 2026, on request: "as much empty space to the left as to the
   right of it"). The text column took every spare pixel and pushed the fan to the banner's right edge; now the
   column keeps its natural width and the fan splits what is left with auto margins. The banner's 24px gap is
   given up for this pair (it would count on the left only) and the fan carries 12px of its own each side
   instead, so the two can never touch; the cards are drawn inside that padding. */
@media (min-width:641px){
  .page .rv-banner:has(> .rv-preview){column-gap:0;}
  .page .rv-banner:has(> .rv-preview) > .body{flex:0 1 auto;}
  /* the fan's cards swing out past the box: the back-left one ~33px (rotate -6deg, 12px shift), the back-right
     one ~20px (rotate 3deg, 10px shift). The padding covers that overhang on each side, so the auto margins
     split the room between the cards' VISIBLE edges (Oct 2026, on request: the fan sat too close to the text). */
  .rv-preview{margin:0 auto; padding:0 24px 0 36px; box-sizing:content-box;}
  .pv-card{left:36px; right:24px;}
}
/* THE WEEK FILLS THE ROW on a tablet and desktop (Oct 2026, on request: "there is currently still a lot of
   empty space … grow the coloured boxes and the chest to fill as much of this empty space as possible"). The
   week's column is the flexible one and Longest-over-the-note takes only what it needs at the right; each day
   is a square that grows with the room it is given, up to 48px, and the chest grows with it, so the ribbon
   stays about as tall as its two-line note. A phone keeps its 12px days (its own block, below). */
@media (min-width:641px){
  .streak-ribbon{grid-template-columns:auto auto minmax(0, 1fr) auto;}
  .streak-ribbon > .sr-days{min-width:0;}
  .sr-wk{flex:1 1 auto; gap:8px; max-width:100%;}
  /* smaller than the slot it sits in (Oct 2026, on request, twice: "make the green squares a little smaller
     but keep them in their current places") — the 7px margins give back what the square gave up, so each day
     keeps its centre and the week keeps its spread */
  .sr-wk i{flex:1 1 14px; min-width:14px; max-width:34px; width:auto; height:auto; aspect-ratio:1 / 1; border-radius:7px; margin:0 7px;}
  .sr-wk i.t{border-width:3px;}
  /* …and air to the chest's right (Oct 2026, on request), between it and Longest */
  .sr-chest{flex:1.6 1 22px; min-width:30px; max-width:76px; width:auto; margin:0 20px 0 8px;}
  /* the chest is drawn in the lower part of its own viewBox (its lid's arc starts a sixth of the way down and
     its box ends near the foot), so a centred box is a chest sitting low — nudged up by that difference */
  .sr-chest .chest-svg{transform:translateY(-5%);}
}
@media (max-width:1024px){
  .rv-preview{width:260px; height:180px;}
  .games-sec .game-grid{grid-template-columns:repeat(2, 1fr);}
}
/* A NARROW TABLET DROPS THE FANNED PREVIEW (Oct 2026, on request): beside it the piles and the estimate ran
   under the cards, "≈ 1 h 9 min" cut off at the edge. The piles keep their own width.
   …AND A PORTRAIT TABLET HAS IT BACK (Oct 2026, on request: "a large empty space on the right … put the card
   preview there"). The overlap came from a FIXED 260px beside a column that would not shrink; here the text
   column keeps its natural width and the preview takes what is left, between 150px and 240px, so it can
   never run under the piles. Below 720px there is no 150px left over and it stays hidden. */
@media (min-width:641px) and (max-width:900px){
  .page .rv-banner .body{flex:0 1 auto;}
  .rv-preview{flex:1 1 0; width:auto; min-width:150px; max-width:240px; height:170px; margin:0 auto;}
  .pv-card{padding:14px 15px;}
  .pv-card p{font-size:13.5px;}
}
@media (min-width:641px) and (max-width:719px){
  .rv-preview{display:none;}
}
@media (max-width:640px){
  .page .rv-banner, .page .rv-banner:hover{padding:22px 20px; border-radius:16px; flex-direction:column; align-items:stretch;}
  .review-group.has-active > #b-review.rv-banner{border-bottom-left-radius:16px; border-bottom-right-radius:16px;}
  .page .rv-banner h2.review-title{font-size:calc(32px * var(--fs));}
  .rv-preview{display:none;}
  .page .rv-banner .rv-stack{width:100%;}
  .page .rv-banner .meta{width:100%; grid-template-columns:1fr 1fr 1fr 1fr; gap:0 10px;}
  .page .rv-banner .stat b{font-size:calc(24px * var(--fs));}
  .page .rv-banner .stat.st-est{padding-left:10px;}
  .streak-ribbon{padding:12px 14px;}
  .sr-wk i{width:12px; height:12px;} .sr-chest{width:24px;}
  /* a phone keeps its two-row layout (Oct 2026, on request: "don't apply the streak changes to mobile"):
     the count, label and Longest on top, the week under them and the note at its right */
  .streak-ribbon{grid-template-columns:auto 1fr fit-content(40%); gap:8px 8px;}
  .streak-ribbon > .sr-n{grid-column:1; grid-row:1;} .streak-ribbon > .sr-t{grid-column:2; grid-row:1;}
  .streak-ribbon > .sr-best{grid-column:3; grid-row:1; align-self:center;}
  .streak-ribbon > .sr-days{grid-column:1 / 3; grid-row:2; margin-left:0;}
  .streak-ribbon > .sr-note, .streak-ribbon:not(:has(> .sr-best)) > .sr-note{grid-column:3; grid-row:2; align-self:center;}
  .review-group .active-deck{padding-right:12px; gap:10px;}
  .review-group .active-deck .dk-counts{grid-template-columns:repeat(3, 28px);}
  .review-group .active-deck .dkc{height:26px; font-size:calc(12px * var(--fs));}
  .review-group .active-deck .dk-prog .track{top:auto; height:5px;}
  .review-group .active-deck .dk-prog .fill-l{border-right:0; background:var(--dk-accent); border-radius:0 3px 3px 0;}
  /* the studied share is a bar here too, and at the row tint's 7% it vanished against the card (Oct 2026, on request) */
  .review-group .active-deck .dk-prog .fill{background:color-mix(in srgb, var(--dk-accent) 30%, transparent);}
  /* an unfolded collection and its subdecks are ONE tight block (Oct 2026, on request — see `dk-att` in
     adSyncFold): no gap and no corners between them, one hairline as the divider (the -9px is the grid's
     8px gap plus the 1px border the two rows would otherwise double), and a little extra room under the
     last subdeck so the next collection reads as the next thing rather than a part of this one */
  .review-group .active-deck.dk-att{margin-top:-9px; border-top-left-radius:0; border-top-right-radius:0;}
  .review-group .active-deck.dk-cont{border-bottom-left-radius:0; border-bottom-right-radius:0;}
  .review-group .active-deck.dk-att:not(.dk-cont){margin-bottom:6px;}
  .games-sec .game-grid{grid-template-columns:1fr;}
  .home-two{grid-template-columns:1fr;}
  .home-foot{justify-content:center; text-align:center;}
  .home-foot .home-about{text-align:center;}
  .home-foot .site-ver{margin:0; width:100%; text-align:center;}
}

/* ============================================================
   COLLECTIONS PAGE — the Oct 2026 redesign (see docs/mockups/collections-redesigns/final/)
   ============================================================
   The Ledger: one bordered block per section with its heading as a band, and every collection a FILL ROW —
   the row's tint, from the left, is the share of it studied (`--w`, written on the row by buildCollection
   and udeckRowHTML; the `.collection-deco` that used to wash the whole banner is now that fill). The size
   line under the title is the old studied/total bar's head, with the bar itself hidden. The Studio's list
   and editor sit under Your decks (communityLibraryHTML), the editor opening in place (`.studio-inline`). */
.vh{position:absolute; width:1px; height:1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap;}
/* EVERY SECTION IS A BLOCK OF ITS OWN (Oct 2026, on request: "a thin empty space between … like between the
   planned section and your decks section … corners of each section rounded … each section collapsable like the
   planned section"). They were one joined ledger with only the first head and the last row rounded; now each
   is a <details> like Planned — 16px clear of its neighbour, all four corners rounded, and all four still
   rounded while it is folded. */
.page .collection-group + .collection-group{margin-top:16px;}
.page .collection-group > .group-head, .page .collection-group > summary.group-head{
  margin:0; padding:8px 16px; background:var(--paper-2); border:1px solid var(--rule); border-bottom:0;
  font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.14em; text-transform:uppercase; color:var(--ink-quiet); font-weight:600; gap:10px;
}
.page .collection-group-fold > .group-head{border-radius:14px 14px 0 0;}
.page .collection-group-fold:not([open]) > .group-head{border-radius:14px; border-bottom:1px solid var(--rule);}
.page .group-label::before{display:none;}
.page .group-label{letter-spacing:.14em; color:var(--ink-quiet);}
.page .group-line{display:none;}
.page .group-count{color:var(--ink-faint); margin-right:auto;}
.page .collection-list{gap:0;}
/* every row is square-cornered — stated over `body[data-theme]` because the folio theme rounds `.collection` to
   20px at a higher specificity — and only the LAST row of the list rounds, at the bottom (Oct 2026, on request) */
.page .collection, .page .collection.open, .page .collection.placeholder,
body[data-theme] .page .collection, body[data-theme] .page .collection.open, body[data-theme] .page .collection.placeholder{border-radius:0; box-shadow:none; border-top:0; background:var(--card);}
.page .collection-group-fold .collection-list > .collection:last-child, .page .collection-group-soon .collection-list > .collection:last-child,
body[data-theme] .page .collection-group-fold .collection-list > .collection:last-child, body[data-theme] .page .collection-group-soon .collection-list > .collection:last-child{border-radius:0 0 14px 14px;}
.page .collection-row, .page .collection.placeholder .collection-row{padding:12px 16px; gap:14px;}
body .collection .collection-row > .collection-deco, body[data-theme] .collection .collection-row > .collection-deco, body[data-theme].night .collection .collection-row > .collection-deco{
  width:var(--w, 0%); background:linear-gradient(90deg, color-mix(in srgb, var(--coll-bg, #888) 20%, var(--card)), color-mix(in srgb, var(--coll-bg, #888) 9%, var(--card)));
}
.collection-row[style*="--w:0%"] > .collection-deco{display:none;}
/* the icon on its own, in the collection's hue — no tinted square behind it (Oct 2026, on request: the square hid the fill row) */
.page .coll-ic{min-width:44px; width:44px; height:44px; border-radius:0; background:transparent; color:var(--coll-bg, #888);}
.page .coll-ic svg{width:26px; height:26px;}
.page .collection-title-row{margin-bottom:2px; gap:8px;}
.page .collection-title{font-family:var(--sans); font-weight:700; font-size:calc(15.5px * var(--fs));}
.page .collection-row .xp{gap:0;}
.page .collection-row .xp-head{justify-content:flex-start; gap:6px;}
.page .collection-row .xp-lvl, .page .collection-row .xp-count{font-family:var(--sans); font-size:calc(12px * var(--fs)); letter-spacing:0; text-transform:none; color:var(--ink-quiet); font-weight:400;}
.page .collection-row .xp-lvl::after{content:":";}
.page .collection-row .xp-track{display:none;}
.page .coll-reach.is-done{margin-left:auto; color:var(--good); font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.08em; font-weight:700;}
.page .coll-reach.is-done .cr-long{display:inline;}
.page .collection-count{color:var(--ink-quiet);}
.page .collection-try{font-family:var(--mono); font-size:calc(10px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; font-weight:700; color:var(--indigo); padding:7px 11px;}
.page .collection-add, .page .node-add{width:34px; height:34px;}
.page .collection-add.added, .page .node-add.added{background:var(--good); border-color:var(--good); color:#fff;}
.page .chev{width:34px; height:34px;}
.page .collection.udeck > .collection-row{border:0; background:transparent;}
.page .collection.udeck .collection-deco{display:block;}
.page .collection.udeck-editing > .collection-row{background:var(--indigo-wash);}
.udeck-act{font-family:var(--mono); font-size:calc(9.5px * var(--fs)); letter-spacing:.1em; text-transform:uppercase; font-weight:700; padding:7px 10px; border-radius:8px;}
.studio-inline{padding:16px 18px 20px; background:var(--paper); border-top:1px solid var(--rule); border-bottom:1px solid var(--rule);}
.studio-inline .studio-head{margin-bottom:12px;}
.studio-inline .studio-title{font-size:calc(22px * var(--fs));}
.studio-orphans{margin:14px 0 0; padding:12px 16px; border:1px dashed var(--rule); border-radius:12px;}
.studio-orphans .studio-sec{font-family:var(--mono); font-size:calc(10.5px * var(--fs)); letter-spacing:.16em; text-transform:uppercase; color:var(--ink-quiet); font-weight:600;}
.studio-orphans .studio-list{gap:8px;}
.page .udeck-intro{max-width:70ch;}
@media (max-width:640px){
  .page .collection-row, .page .collection.placeholder .collection-row{padding:12px 12px; gap:10px;}
  .page .coll-ic{min-width:40px; width:40px; height:40px;}
  .page .collection-try{display:none;}
  body .collection .collection-row > .collection-deco, body[data-theme] .collection .collection-row > .collection-deco{top:auto; height:5px; border-right:0; background:var(--coll-bg, #888); border-radius:0 3px 3px 0;}
  .page .udeck-actions{display:grid; grid-template-columns:1fr 1fr; gap:8px;}
  .page .udeck-actions .btn{justify-content:center;} .page .udeck-actions .btn:first-child{grid-column:1 / -1;}
  /* NO "STUDIED N / M CARDS" LINE ON A PHONE (Oct 2026, on request): the row's tint already says how much of
     it is studied, and the line was what made a language row taller than the band under it allowed for */
  .page .collection-row .xp{display:none;}
  /* A READER'S OWN DECK ROW BREAKS INTO TWO LINES (Oct 2026, on request — the title had been squeezed into a
     column three words wide beside its pill, its count and five controls): the icon and the title's block
     take the first line, and the controls sit right-aligned on a line of their own beneath. The title row
     wraps too, so the pill and the count drop under a long title rather than shortening it. */
  .page .collection.udeck > .collection-row{flex-wrap:wrap;}
  .page .collection.udeck .collection-title-row{flex-wrap:wrap; row-gap:4px;}
  .page .collection.udeck .collection-actions{flex:0 0 100%; max-width:none; flex-wrap:wrap; justify-content:flex-end; gap:6px;}
  .page .collection.udeck .collection-actions .studio-deck-open{flex:0 0 auto;}   /* the pencil keeps the Studio list's `flex:1` elsewhere; here it would spread the controls out */
  .studio-inline{padding:14px 12px 18px;}
}
.page .coll-reach .cr-short{display:none;}
.page .coll-reach.is-done .cr-long{display:inline;}
.page .community-group, .page #sharedDecks{margin-top:22px;}
.page .community-group > .group-head{border-radius:14px; border-bottom:1px solid var(--rule); margin-bottom:12px;}
.page .community-group .studio-list{border:1px solid var(--rule); border-radius:14px; overflow:hidden; background:var(--card);}
.page .community-group .studio-list > .collection{border-left:0; border-right:0; border-radius:0;}
.page .community-group .studio-list > .collection:first-child{border-top:0;}
.page .community-group .studio-list > .lib-empty{border:0; margin:0;}
/* THE LEDGER'S ENDS (Oct 2026, on a bug report from a phone). Languages is a section of the ledger like History
   or Special, so its head is a band joined to its rows rather than the floating pill the community groups wear,
   and its last row closes the block. Planned is a block of its own below it: 16px clear of the ledger, every
   corner rounded while it is folded, and the top two only once it is open (its last row rounds then). And
   "Your decks" keeps its own 22px, which the ledger's `.collection-group + .collection-group{margin-top:0}`
   had been taking away, leaving its head pressed against Planned's. */
.page #langDecks{margin-top:16px;}
.page .coll-tabrow + #langDecks{margin-top:0;}
.page #langDecks > .group-head{margin-bottom:0;}
.page #langDecks[open] > .group-head{border-radius:14px 14px 0 0; border-bottom:0;}
.page .collection-group + .collection-group-soon{margin-top:16px;}
.page .collection-group-soon:not([open]) > summary.group-head{border-radius:14px; border-bottom:1px solid var(--rule);}
.page .collection-group-soon[open] > summary.group-head{border-radius:14px 14px 0 0;}
.page .collection-group + .collection-group.community-group:not(#langDecks){margin-top:22px;}

/* ============================================================
   LIBRARY PAGE — the Oct 2026 Timeline (see docs/mockups/library-redesigns/l-5-timeline.html)
   ============================================================
   The shelf as a line through time: a rule down the left of each section, era marks on it when the shelf
   is sorted by date, and every banner hung off it by a dot in the book's colour and a short tie. Two
   banners to a row on a wide screen, one below 1025px. The banner itself is the home page's FILL ROW: how
   far the book is read is painted across it from the left (`--w`, set by PAGES.library), with no edge
   line and no bar — the fill is the figure. The markup is in PAGES.library (timelineHTML). */
.book-timeline{position:relative; padding-left:34px;}
.book-timeline::before{content:""; position:absolute; left:11px; top:0; bottom:0; width:2px; background:var(--rule);}
.book-timeline .book-grid{gap:8px;}
.bk-mark{position:relative; grid-column:1 / -1; margin:14px 0 2px; font-family:var(--mono); font-size:calc(11px * var(--fs)); letter-spacing:.16em; text-transform:uppercase; color:var(--indigo); font-weight:700;}
.bk-mark:first-child{margin-top:2px;}
.bk-mark em{font-style:normal; color:var(--ink-faint); margin-left:6px;}
.bk-mark::before{content:""; position:absolute; left:-29px; top:3px; width:12px; height:12px; border-radius:50%; background:var(--indigo); box-shadow:0 0 0 4px var(--paper);}
.bk-slot{position:relative; min-width:0;}
.bk-slot > .book-tile{width:100%;}
.bk-slot::before{content:""; position:absolute; left:-27px; top:50%; width:8px; height:8px; margin-top:-4px; border-radius:50%; background:var(--tile, var(--indigo)); z-index:1;}
.bk-slot::after{content:""; position:absolute; left:-19px; top:50%; width:18px; height:1px; background:var(--rule);}
/* the banner: plain paper under a fill that is the reading progress, the wash and the bottom bar gone */
.page .book-tile, body.night .page .book-tile{background:var(--card);}
.page .book-tile::before{content:""; position:absolute; left:0; top:0; bottom:0; width:var(--w, 0%); pointer-events:none;
  background:linear-gradient(90deg, color-mix(in srgb, var(--bk-accent) 22%, var(--card)), color-mix(in srgb, var(--bk-accent) 9%, var(--card)));}
.page .book-tile[style*="--w:0%"]::before{display:none;}
.page .book-tile > .bk-spine, .page .book-tile > .bk-tile-body, .page .book-tile > .bk-star{position:relative; z-index:1;}
.page .book-tile > .bk-star{position:absolute;}
.page .bk-tile-bar{display:none;}
@media (min-width:1025px){
  .book-timeline .book-grid{grid-template-columns:1fr 1fr; column-gap:14px;}
  .bk-slot.bk-c2::before, .bk-slot.bk-c2::after{display:none;}
  /* at half the page's width the title block asks for less before the right-hand block drops under it —
     a long title wraps, where a dropped block made the banner half as tall again (157px against 120) */
  .book-timeline .bk-tile-main{flex-basis:140px;}
  .book-timeline .bk-tile-body{padding:12px 14px; gap:4px 14px;}
  .book-timeline .bk-tile-title{font-size:calc(17.5px * var(--fs));}
}
@media (max-width:640px){
  .book-timeline{padding-left:26px;}
  .book-timeline::before{left:7px;}
  .bk-mark::before{left:-25px; width:10px; height:10px;}
  .bk-slot::before{left:-23px;}
  .bk-slot::after{left:-15px; width:14px;}
}

