/* the-handoff.css - Context Handoff, three acts on two surfaces.

   ACTS I AND II run on a warm paper desk: ink on paper, hairlines rather
   than boxes, and exactly one thing per screen lifted off the surface. This
   is your desk. You are reading, deciding, and writing on it.

   ACT III runs on a second palette, scoped to .th-root.th-dark - a near-black
   machined console. The file rail inside it deliberately stays on the LIGHT
   surface. That contrast is the whole image of the game: your careful,
   considered file, being read by something that is not you and does not
   remember you. The cut between the two is the one moment that has to feel
   like handing something over, so the desk drains rather than fades.

   Both palettes share one accent. Warm colour only ever means "something is
   about to go wrong" or "something went wrong". It never decorates.

   Mobile is the design, not the fallback. Desktop widens the plate and opens
   the file permanently beside the question, because you should never have to
   remember what you already wrote.

   Every token is scoped to .th-root rather than :root. Four of the older game
   sheets declare a global :root and quietly repaint the shared site chrome
   while their route is mounted - this one deliberately does not, and
   test-the-handoff.js fails if that ever changes.

   EVERY token below is fair game for the supporter manifest, not just the
   accent pair. wwwroot/supporters/<gameId>.json is injected as a style block
   keyed on [data-supporter], so a sponsor reskins the whole game - ground,
   ink, semantics, radius, both type faces and the Act III console - without
   this file changing. The values here are the defaults the game falls back to
   the moment that file is deleted. Keep them that way: a sponsor colour
   written into this sheet is a colour you cannot switch off.

   Shape system, applied everywhere: 22px plates, 14px controls, 10px small,
   4px sub-12px bits, pill for chips. Blocks marked with a colour gutter square
   off the gutter edge and keep the radius on the other three.

   Type is three voices with three jobs, and the rule is worth stating because
   the sheet broke it for a year. --th-display carries structure (the wordmark,
   the score, act names, section labels). The system UI stack carries reading,
   because it is already the right face for a paragraph and costs no bytes.
   --th-mono is the machine and nothing else: the file, the terminal, the token
   meter, counters. Before this pass mono was on sixty selectors and twenty-one
   of them were uppercase and tracked, which meant every label on the page was
   shouting in the same small-caps voice and none of them landed. Both faces are
   already declared as @font-face in css/app.css, linked on every route, so they
   cost no new files.

   Sizes come from --th-t-* and tracking from --th-k-*. There were thirty-two
   distinct font-size values and eleven tracking values in here, which is what a
   year of nudging one element at a time looks like. A value that is not on the
   scale is a bug.

   Depth is a material, not an elevation ramp. --th-plate is flush, --th-raise
   is off the desk, --th-inset is cut into it, each tinted to the warm ground
   with an inner top highlight fixing the light source above. Exactly ONE
   element per screen is --th-raise: the live card in Act I, the task in Act II,
   the plate on the start and interstitial screens, and nothing at all on the
   end screen, where the score is the largest type on the page and needs no
   help. The old --th-e1/e2/e3 were the stock three-step Material shadow and
   every plate took one, so elevation communicated no hierarchy at all.

   The site design system bans blurred shadows; that ban is scoped to the
   Nintendo chrome, and per-game sheets keep their own world. This is a
   deliberate call, not an oversight. */

.th-root {
    /* surfaces. A warm paper desk, not a cool screen. The panel is paper rather
       than #FFF so that white can still mean something: the inner top highlight
       on --th-plate reads as light landing on a sheet, which pure white cannot
       do against pure white. */
    --th-bg: #EDEAE3;
    --th-panel: #FCFAF6;
    --th-panel-2: #F6F3EC;
    --th-fill: #E5E1D8;
    --th-line: #DCD7CB;
    --th-line-2: #C6BFAF;

    /* ink, warm-black rather than blue-black. Worst ground for each is
       --th-fill: 14.0 / 6.0 / 4.6 to 1. */
    --th-text: #171512;
    --th-muted: #56514A;
    --th-dim: #696257;

    /* the accent. Deepened from #0B7D71 when the ground went
       warm: at the old value accent text fell to 3.84:1 on --th-fill, which is
       under AA. This reads as ink on paper and clears AA on all four grounds
       (worst 4.89 on --th-fill), 6.39:1 for white on it. */
    --th-accent: #0A6B61;
    --th-accent-hi: #085C54;
    --th-accent-soft: rgba(10, 107, 97, .08);
    --th-accent-line: rgba(10, 107, 97, .28);

    /* semantics, pulled toward the ink. On paper a full-chroma status palette
       reads as dashboard chrome; at this chroma the same four meanings read as
       marks made on the page. All AA on --th-panel and --th-fill; white on
       --th-warn is 6.25:1 and on --th-bad 7.18:1. */
    --th-ok: #1C6B49;
    --th-warn: #8A5416;
    --th-bad: #A32741;
    --th-stale: #5B3A9E;
    --th-warn-soft: rgba(138, 84, 22, .08);
    --th-bad-soft: rgba(163, 39, 65, .08);
    --th-ok-soft: rgba(28, 107, 73, .08);
    --th-stale-soft: rgba(91, 58, 158, .08);
    --th-ring: rgba(23, 21, 18, .09);

    /* Four headings, four inks. The job in Act II is to send a note to one of
       four places, twelve times over, and every heading was the same teal - so
       "which one" had words but no shape, and the file being built read as one
       undifferentiated block. Each heading now carries an ink, and the SAME ink
       is on the answer button, on the band the note lands in, and on the tab
       above that band. Choosing and finding become one act of recognition
       instead of two of reading.
       Three of the four are colours the sheet already had. Only Conventions is
       new: 7.6:1 on --th-panel-2. This does not break the warm-colour law above
       - the single warm ink here sits on "Never do", where warm means exactly
       what it means everywhere else in the game. */
    --th-sec-setup: #0A6B61;
    --th-sec-conventions: #2F4B87;
    --th-sec-never: #A32741;
    --th-sec-issues: #5B3A9E;

    /* Three voices, three jobs. Display carries structure, the system stack
       carries reading, mono is the machine and nothing else - see the rationing
       note above .th-kicker. NeverMindDisplay and NeverMindMono are already
       declared as @font-face in css/app.css, which is linked on every route, so
       this costs no new files and no new blocks. */
    --th-display: "NeverMindDisplay", "Arial Black", Arial, sans-serif;
    --th-ui: -apple-system, BlinkMacSystemFont, "Segoe UI Variable Display", "Segoe UI", system-ui, Roboto, "Helvetica Neue", Arial, sans-serif;
    --th-mono: "NeverMindMono", ui-monospace, "SF Mono", SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;

    /* Type scale. This was 32 distinct font-size values, 25 of them crowded
       between .58rem and 1.05rem, which is what per-element nudging looks like
       after a year. Seven steps, and a size that is not on the scale is a bug. */
    --th-t-score: clamp(2.6rem, 13vw, 3.9rem);
    /* .th-title is white-space:nowrap on purpose, so this floor is load-bearing:
       measured in NeverMindDisplay Bold at -.02em, "Context Handoff" fits a 320px
       screen up to 29.2px and 8.2vw never exceeds the fit at any width. A 2rem
       floor overflows at 320px. */
    --th-t-display: clamp(1.75rem, 8.2vw, 3.1rem);
    --th-t-xl: 1.35rem;
    --th-t-lg: 1.0625rem;
    --th-t-md: .9375rem;
    --th-t-sm: .8125rem;
    --th-t-xs: .6875rem;
    --th-t-2xs: .625rem;

    /* Tracking. Was 11 values. Wide is spent once, on the caption under the
       final score. */
    --th-k-tight: -.02em;
    --th-k-none: 0;
    --th-k-label: .08em;
    --th-k-wide: .14em;

    --th-r-plate: 22px;
    --th-r-ctl: 14px;
    --th-r-sm: 10px;
    --th-r-xs: 4px;

    /* Material, not an elevation ramp. The old --th-e1/e2/e3 were the stock
       three-step Material shadow, untinted, and every plate in the game took
       one - so elevation said nothing about hierarchy. These are named for what
       a thing IS. Exactly one element per screen is --th-raise. The shadow is
       tinted to the warm ground and the inner top highlight fixes the light
       source above the desk. */
    --th-plate: inset 0 1px 0 rgba(255, 255, 255, .9), 0 1px 2px rgba(40, 34, 24, .05);
    --th-raise: inset 0 1px 0 rgba(255, 255, 255, .9), 0 2px 3px rgba(40, 34, 24, .05), 0 12px 28px rgba(40, 34, 24, .09);
    --th-inset: inset 0 1px 3px rgba(40, 34, 24, .10);

    /* Motion. One curve for everything that arrives or settles: a strong
       ease-out, so the move is mostly over by the time the eye lands on it. */
    --th-ease-out: cubic-bezier(.16, 1, .3, 1);

    max-width: 560px;
    margin: 0 auto;
    min-height: calc(100dvh - 56px);
    background: var(--th-bg);
    box-shadow: 0 0 0 100vmax var(--th-bg);
    padding: 1rem 1rem calc(1rem + env(safe-area-inset-bottom, 0px));
    color: var(--th-text);
    font-family: var(--th-ui);
    -webkit-tap-highlight-color: transparent;
    /* the handoff cut drains through this; see .th-handing */
    transition: filter .42s cubic-bezier(.4, 0, .2, 1);
}

.th-screen { display: none; }
.th-screen.on { display: flex; flex-direction: column; gap: .9rem; animation: th-in .35s var(--th-ease-out) both; }
/* thShow() focuses the incoming screen so it and its scroll position are
   announced on every transition; that focus is programmatic, not a tab stop. */
.th-screen:focus { outline: none; }

@keyframes th-in { from { opacity: 0; transform: translateY(10px); } to { opacity: 1; transform: none; } }

.th-plate {
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-plate);
    padding: 1.5rem 1.25rem;
    box-shadow: var(--th-plate);
}
/* One raised element per screen. On the start screen the plate is the screen. */
#th-start .th-plate { box-shadow: var(--th-raise); }

/* ---- arrival ------------------------------------------------------------
   Every other screen slides in as one block, which is right for them: they are
   one question. The start screen is a premise, an offer and a map of three
   acts, and arriving as a single slab it read as a page that had always been
   there. It assembles in reading order instead, and the three acts arrive as
   three, which is the one thing that block exists to say.

   Same th-in keyframe the screens already use, so this costs delays and no new
   motion vocabulary. One pass, no loop: WCAG 2.2.2 governs this screen, and the
   test suite pins it. */
#th-start.th-screen.on { animation: none; }

#th-start .th-kicker,
#th-start .th-title,
#th-start .th-lede,
#th-start .th-def,
#th-start .th-btn-primary,
#th-start .th-lengths,
#th-start .th-start-links,
#th-start .th-slide {
    animation: th-in .42s var(--th-ease-out) both;
}
#th-start .th-kicker             { animation-delay: .04s; }
#th-start .th-title              { animation-delay: .09s; }
#th-start .th-lede               { animation-delay: .15s; }
#th-start .th-def                { animation-delay: .20s; }
#th-start .th-btn-primary        { animation-delay: .27s; }
#th-start .th-lengths            { animation-delay: .31s; }
#th-start .th-start-links        { animation-delay: .36s; }
#th-start .th-slide:nth-child(1) { animation-delay: .40s; }
#th-start .th-slide:nth-child(2) { animation-delay: .47s; }
#th-start .th-slide:nth-child(3) { animation-delay: .54s; }

/* ============================================================ start ===== */

.th-kicker {
    margin: 0 0 .45rem;
    font-family: var(--th-display);
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-none);
    color: var(--th-accent);
    font-weight: 700;
}

/* One line. The wordmark used to break after "Context", which spent a whole
   line of the start screen on two words and pushed the three acts below the
   fold on a phone. clamp() bottoms out low enough that "Context Handoff" still
   fits on one line at 360px. */
.th-title {
    margin: 0 0 .7rem;
    font-size: var(--th-t-display);
    font-weight: 700;
    line-height: 1.05;
    letter-spacing: var(--th-k-tight);
    white-space: nowrap;
    font-family: var(--th-display);
}
.th-title span { color: var(--th-accent); }

.th-lede {
    margin: 0 0 1.15rem;
    color: var(--th-muted);
    font-size: var(--th-t-lg);
    line-height: 1.55;
}
.th-lede b { color: var(--th-text); font-weight: 600; }

/* The definition, once, under the premise. A step smaller than the lede so it
   reads as the footnote it is, and pulled up tight against it so the two lines
   are one block rather than two claims. */
.th-def {
    margin: -.75rem 0 1.15rem;
    color: var(--th-muted);
    font-size: var(--th-t-md);
    line-height: 1.55;
}
.th-def b { color: var(--th-text); font-weight: 600; }


/* ---- the topology picker ------------------------------------------------
   Two shapes, not two difficulty levels. Neither is wrong, so the only signal
   is which one is currently on - and the one warm figure, which is not a
   verdict on the shape but on this file inside it.

   The drawings are gone. Two 34px bar diagrams carried this distinction first
   and carried none of it; two animated line-art panels replaced them and were
   read as decoration too. The thing that actually differs between the shapes
   is a number about the file the player just wrote, so the chips carry that
   and nothing else, and the whole comparison fits a phone without scrolling
   between its two halves.

   It lives on the interstitial plate now, not on the file screen, so it brings
   no surface of its own: the plate under it is already the one raised element
   on that screen. */

/* The file, measured once, above both chips - the fact they are both about.
   Mono, because it is a count. */
.th-pick-file {
    margin: 0 0 .55rem;
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    font-variant-numeric: tabular-nums;
    color: var(--th-dim);
}

.th-patterns {
    display: grid;
    gap: .5rem;
}

/* Side by side the moment there is room, because the whole argument is a
   comparison. Below this the two chips stack, and each one still fits a
   phone viewport whole. */
@media (min-width: 34rem) {
    .th-patterns { grid-template-columns: 1fr 1fr; align-items: start; }
}

.th-pick {
    margin: 0 0 .4rem;
    animation: th-task-in .38s var(--th-ease-out) both;
}

.th-pat {
    display: block;
    text-align: left;
    font: inherit;
    padding: .85rem .95rem 1rem;
    background: var(--th-panel-2);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
    color: var(--th-text);
    cursor: pointer;
    touch-action: manipulation;
    transition: border-color .15s ease, background .15s ease, transform .12s var(--th-ease-out);
}
.th-pat:hover { border-color: var(--th-line-2); }
.th-pat:active { transform: scale(.985); }
.th-pat b { display: block; font-size: var(--th-t-md); font-weight: 600; }
.th-pat-say {
    display: block;
    margin-top: .2rem;
    color: var(--th-muted);
    font-size: var(--th-t-sm);
    line-height: 1.45;
}
/* This shape, costing this file. Written by thFillPatterns() against
   thLinesUsed(), never in the markup. The figures used to be the pattern's own
   constants - "8 lines · 4s to answer · ×1" - which is a fact about the game
   rather than about you, so picking a topology moved nothing on screen. */
.th-pat em {
    display: block;
    margin-top: .45rem;
    font-style: normal;
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-none);
    font-variant-numeric: tabular-nums;
    color: var(--th-dim);
}
.th-pat.on {
    border-color: var(--th-accent);
    background: var(--th-accent-soft);
}
.th-pat.on b { color: var(--th-accent); }
.th-pat.on em { color: var(--th-accent); }
/* Warm only ever means "about to go wrong" in this sheet, and it is exactly
   that here: over this shape's budget is Act III stopping partway down the
   file. It outranks the accent, because being selected does not make it fit. */
.th-pat em.th-pat-over,
.th-pat.on em.th-pat-over { color: var(--th-warn); }

/* ---- the three acts: named and drawn in one block ------------------------
   This was a four-slide carousel next to a separate three-item list, which put
   two different maps of the same game side by side and made a first-timer
   reconcile them before they had either. One block now, three panels, one per
   act, all visible at once - a player comparing act II to act III should not
   have to wait for a rotation to come back round.

   Hairline dividers rather than three floating cards: the acts are one
   sequence, and the 1px grid gap showing the border colour through is the
   cheapest way to say so.

   The scenes are hand-drawn SVG in this sheet's own line-art idiom, and now
   the only drawings left in it: the two topology panels were the other pair,
   and they were deleted for being read as decoration. These are read, because
   a player on the start screen has nothing else to do yet. Animation stays
   scoped to .th-slide.on, which every panel now carries permanently. */

/* The block follows the CTA now rather than preceding it, so the margin moved
   from its foot to its head. */
.th-how-track {
    display: grid;
    gap: 1px;
    margin: 1.3rem 0 0;
    background: var(--th-line);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
    overflow: hidden;
}
.th-slide {
    padding: .9rem 1rem 1rem;
    background: var(--th-panel);
}
@media (min-width: 900px) {
    .th-slide { padding: 1rem 1.1rem 1.15rem; }
}

/* The numeral and the act name on one line, so the caption below reads as the
   answer to "and what happens in that one?". */
.th-act {
    display: flex;
    align-items: baseline;
    gap: .55rem;
    margin: 0 0 .55rem;
}
.th-act i {
    font-family: var(--th-display);
    font-style: normal;
    font-size: var(--th-t-sm);
    letter-spacing: var(--th-k-none);
    color: var(--th-accent);
    min-width: 1.4rem;
    font-weight: 700;
}
.th-act b { font-family: var(--th-display); font-size: var(--th-t-md); font-weight: 700; }

.th-slide span { display: block; color: var(--th-muted); font-size: var(--th-t-sm); line-height: 1.55; }
.th-team-mini {
    display: flex;
    gap: .45rem;
    align-items: center;
    height: 118px;
    padding: 1rem;
}
.th-team-mini i {
    display: block;
    flex: 1;
    height: 56px;
    border: 2px solid var(--th-accent);
    border-radius: .55rem;
    background: var(--th-accent-soft);
}
/* IV. the team: three readers arrive drifted and settle into one line - the
   card's own "line up, or drift". One pass; thGuideReplay replays it. */
.th-team-mini i:nth-child(1) { --d: -.6rem; }
.th-team-mini i:nth-child(2) { --d: .45rem; }
.th-team-mini i:nth-child(3) { --d: -.25rem; }
.th-slide.on .th-team-mini i { animation: a5-align 3s var(--th-ease-out) both; }
@keyframes a5-align {
    0%, 25% { transform: translateY(var(--d)); }
    60%, 100% { transform: none; }
}

/* ---- the scenes ---------------------------------------------------------
   Quiet grey line art on white, with colour spent only on the thing the beat
   is about: the note being read, the meter, the stale line. */

/* Stacked one above another on a phone, three full-bleed 200x118 scenes are
   most of the screen, and the panel they explain is a caption under a poster.
   Capped and centred they are still comfortably above the size their own 9px
   mono labels need - 260/200 renders NEVER, THIS RUN and AGENTS.MD at ~11.7px -
   and the three acts come back to being three cards you can take in together.
   The cap lifts at 900px, where they go across and the width is the point. */
.th-art { display: block; width: 100%; max-width: 260px; height: auto; margin: 0 auto .75rem; }
@media (min-width: 900px) {
    .th-art { max-width: none; margin-inline: 0; }
}
.th-art text {
    font-family: var(--th-mono);
    font-size: 9px;
    letter-spacing: var(--th-k-label);
    text-anchor: middle;
    fill: var(--th-dim);
}

/* I. triage: one note, three lifetimes, visited in turn */
.th-a2 .a2-face { fill: var(--th-panel); stroke: var(--th-accent); stroke-width: 1.5; }
.th-a2 .a2-l { fill: var(--th-dim); }
.th-a2 .a2-t { opacity: .45; }
.th-a2 .a2-t rect { fill: var(--th-panel); stroke: currentColor; }
.th-a2 .a2-t text { fill: currentColor; font-size: 7px; letter-spacing: .02em; }
.th-a2 .a2-t1 { color: var(--th-bad); }
.th-a2 .a2-t2 { color: var(--th-warn); }
.th-a2 .a2-t3 { color: var(--th-accent); }
.th-a2 .a2-card { transform-box: fill-box; transform-origin: center; }

.th-slide.on .th-a2 .a2-card { animation: a2-send 6s ease-in-out both; }
.th-slide.on .th-a2 .a2-t { animation: a2-lit 6s ease-out both; }
.th-slide.on .th-a2 .a2-t2 { animation-delay: 2s; }
.th-slide.on .th-a2 .a2-t3 { animation-delay: 4s; }

@keyframes a2-send {
    0%        { transform: none; opacity: 1; }
    16%       { transform: translate(-67px, 66px) scale(.6); opacity: 0; }
    17%       { transform: none; opacity: 0; }
    28%       { transform: none; opacity: 1; }
    49%       { transform: translate(0, 66px) scale(.6); opacity: 0; }
    50%       { transform: none; opacity: 0; }
    61%       { transform: none; opacity: 1; }
    82%       { transform: translate(67px, 66px) scale(.6); opacity: 0; }
    83%       { transform: none; opacity: 0; }
    94%, 100% { transform: none; opacity: 1; }
}
@keyframes a2-lit {
    0%, 10%   { opacity: .45; }
    16%       { opacity: 1; }
    32%, 100% { opacity: .45; }
}

/* II. the file: the meter runs hot, the rambling note gets cut down */
.th-a3 .a3-win { fill: var(--th-fill); stroke: var(--th-line-2); }
.th-a3 .a3-name { text-anchor: start; fill: var(--th-muted); }
.th-a3 .a3-track { fill: var(--th-line-2); }
.th-a3 .a3-meter { fill: var(--th-accent); }
.th-a3 .a3-clip { transform-box: fill-box; transform-origin: left center; transform: scaleX(.55); }
.th-a3 .a3-blob { fill: var(--th-panel); stroke: var(--th-line-2); }
.th-a3 .a3-nl { fill: var(--th-dim); }
.th-a3 .a3-note { transform-box: fill-box; transform-origin: left top; }
.th-a3 .a3-key { transform-box: fill-box; transform-origin: center; opacity: .55; }
.th-a3 .a3-key rect { fill: var(--th-panel); stroke: var(--th-line-2); }
.th-a3 .a3-key text { fill: var(--th-muted); font-weight: 700; }

.th-slide.on .th-a3 .a3-clip { animation: a3-fill 5s ease-in-out both; }
.th-slide.on .th-a3 .a3-meter { animation: a3-hot 5s step-end both; }
.th-slide.on .th-a3 .a3-note { animation: a3-cut 5s var(--th-ease-out) both; }
.th-slide.on .th-a3 .a3-drop { animation: a3-drop 5s ease-out both; }
.th-slide.on .th-a3 .a3-key { animation: a3-press 5s ease-out both; }

@keyframes a3-fill {
    0%, 8%    { transform: scaleX(.05); }
    38%, 54%  { transform: scaleX(1); }
    70%, 100% { transform: scaleX(.5); }
}
@keyframes a3-hot {
    0%   { fill: var(--th-accent); }
    34%  { fill: var(--th-bad); }
    68%  { fill: var(--th-accent); }
}
@keyframes a3-cut {
    0%, 56%   { transform: scaleY(1); }
    70%, 100% { transform: scaleY(.33); }
}
@keyframes a3-drop {
    0%, 54%   { opacity: 1; }
    64%, 100% { opacity: 0; }
}
@keyframes a3-press {
    0%, 48%   { opacity: .55; transform: none; }
    56%       { opacity: 1; transform: translateY(2px); }
    72%, 100% { opacity: .55; transform: none; }
}

/* III. the run: the next session types what it knows, and one line is stale */
.th-a4 .a4-win { fill: var(--th-fill); stroke: var(--th-line-2); }
.th-a4 .a4-bar { stroke: var(--th-line-2); }
.th-a4 .a4-name { text-anchor: start; fill: var(--th-dim); }
.th-a4 .a4-l { fill: var(--th-dim); transform-box: fill-box; transform-origin: left center; }
.th-a4 .a4-stale { fill: var(--th-stale); }
.th-a4 .a4-cur { fill: var(--th-accent); }

.th-slide.on .th-a4 .a4-l { animation: a4-type 5s steps(22, end) both; }
.th-slide.on .th-a4 .a4-l2 { animation-delay: .55s; }
.th-slide.on .th-a4 .a4-l3 { animation: a4-type 5s steps(22, end) 1.1s both, a4-wrong 5s ease-out 1.1s both; }
.th-slide.on .th-a4 .a4-cur { animation: none; opacity: 1; }

@keyframes a4-type {
    0%        { transform: scaleX(0); }
    22%, 100% { transform: scaleX(1); }
}
@keyframes a4-wrong {
    0%, 26%   { opacity: 1; }
    34%       { opacity: .2; }
    42%, 100% { opacity: 1; }
}
@keyframes a4-blink { 0%, 55% { opacity: 1; } 56%, 100% { opacity: 0; } }

/* One column at every width. The second column was the carousel, and the whole
   point of folding it in is that there is now one thing to read, top to
   bottom. */
.th-start-grid { display: grid; gap: .9rem; align-items: start; }

/* Byline and seed share the closing line: the credit reads left, the workshop
   field sits right and small, where an optional field belongs. */
.th-start-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
    gap: .5rem 1rem;
    margin-top: 1.1rem;
}
.th-byline { margin: 0; font-size: var(--th-t-sm); color: var(--th-dim); }
.th-byline a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
.th-byline a:hover { color: var(--th-accent); }

.th-seed-col { display: flex; flex-direction: column; gap: .3rem; }
.th-seed-hint { margin: 0; max-width: 15rem; font-size: var(--th-t-2xs); color: var(--th-dim); line-height: 1.35; }

/* ---- run length -------------------------------------------------------
   Two chips under Start, answering "how long is this" before it is asked.
   Flat: #th-start-btn is this screen's one --th-raise element and the pair
   under it must not compete with it. Sharp corners, same --th-r-ctl as the
   dock, and the selected one is marked by the accent rule under it rather
   than by a fill - a filled chip next to a raised button reads as a second
   primary action. */
.th-lengths {
    display: grid;
    grid-template-columns: 1fr 1fr;
    gap: .45rem;
    max-width: 22rem;
    margin: .9rem auto 0;
}
.th-len {
    display: grid;
    gap: .1rem;
    padding: .5rem .55rem;
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-bottom: 2px solid var(--th-line);
    border-radius: var(--th-r-ctl);
    box-shadow: var(--th-plate);
    color: var(--th-muted);
    font-family: var(--th-mono);
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-none);
    text-align: left;
    cursor: pointer;
    touch-action: manipulation;
    transition: color .15s ease, border-color .15s ease, transform .12s var(--th-ease-out);
}
.th-len em {
    font-style: normal;
    font-family: var(--th-ui);
    font-size: var(--th-t-2xs);
    color: var(--th-dim);
    line-height: 1.3;
}
.th-len:hover { color: var(--th-text); }
.th-len.on {
    color: var(--th-text);
    border-bottom-color: var(--th-accent);
}

/* Offered only to players who have already been through the warm-up, so it
   reads as a shortcut rather than as permission to skip the explanation. */
/* The two offers under Start sit on one line. */
.th-start-links {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: .2rem 1.6rem;
    margin-top: .7rem;
}

.th-skip {
    display: block;
    padding: .35rem .2rem;
    background: none;
    border: 0;
    font: inherit;
    font-size: var(--th-t-sm);
    color: var(--th-dim);
    text-decoration: underline;
    text-underline-offset: 3px;
    cursor: pointer;
    touch-action: manipulation;
}
.th-skip:hover { color: var(--th-accent); }
.th-skip[hidden] { display: none; }

/* ========================================================== buttons ===== */

.th-btn {
    font: inherit;
    font-weight: 600;
    border: 1px solid transparent;
    border-radius: var(--th-r-ctl);
    padding: 1rem 1.5rem;
    cursor: pointer;
    touch-action: manipulation;
    transition: transform .12s var(--th-ease-out), filter .15s ease, border-color .15s ease, color .15s ease;
}
.th-btn:active { transform: scale(.975); }
.th-btn-primary {
    background: var(--th-accent);
    color: #FFFFFF;          /* 6.39:1 on --th-accent */
    width: 100%;
    letter-spacing: var(--th-k-none);
    box-shadow: var(--th-raise);
}
/* Was a 14px teal glow plus a brightness() hover. A coloured halo under a CTA
   is decoration pretending to be depth, and brightness() is whatever the
   compositor makes of it rather than a colour anyone chose. */
.th-btn-primary:hover { background: var(--th-accent-hi); }

/* Not a word - the brief for this screen was that nothing on it gets longer.
   "Start" names a thing; the glyph after it points somewhere, which is what the
   only control on the screen ought to do. inline-block so the nudge below is a
   transform rather than a margin that reflows the label. */
.th-btn-arw {
    display: inline-block;
    margin-left: .5rem;
    font-style: normal;
    transition: transform .18s var(--th-ease-out);
}
@media (hover: hover) {
    .th-btn-primary:hover .th-btn-arw { transform: translateX(3px); }
}
@media (prefers-reduced-motion: reduce) {
    .th-btn-arw { transition: none; }
    .th-btn-primary:hover .th-btn-arw { transform: none; }
}
.th-btn-ghost {
    background: var(--th-panel);
    border-color: var(--th-line-2);
    color: var(--th-muted);
    text-decoration: none;
    display: inline-block;
    text-align: center;
    box-shadow: var(--th-plate);
}
.th-btn-ghost:hover { color: var(--th-accent); border-color: var(--th-accent); }

/* ============================================================= HUD ===== */

.th-hud {
    display: flex;
    align-items: center;
    gap: .6rem;
    min-height: 2rem;
}
/* Triage runs on one vertical rhythm: label, note, readout, dock. Everything
   below used to carry margin: 0 and sit flush against the card, so the tally
   read as part of the note and the dock read as part of the tally. */
#th-triage .th-hud { margin-bottom: .75rem; }

/* ---- the charter drawer ----
   Act I asks where a note lives; nothing on screen used to say whose project it
   was, which made half the pool a coin flip rather than a judgement.

   It is a <details>, so open/closed is the browser's job and survives all nine
   notes without state here. Open on arrival - a drawer you must know to press
   is exactly the failure the old three-question panel had, in reverse. Sits
   between the HUD and the question, so it is read before the question and never
   over the card.

   Quiet on purpose: the note is the raised thing on this screen, and a charter
   that competes with it has replaced one confusion with another. */

.th-charter {
    margin: 0 0 .9rem;
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-sm);
    background: var(--th-panel-2);
}
.th-charter > summary {
    cursor: pointer;
    padding: .5rem .7rem;
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-dim);
}
.th-charter > summary:hover { color: var(--th-text); }
.th-charter > summary:focus-visible {
    outline: 2px solid var(--th-accent);
    outline-offset: 2px;
    border-radius: var(--th-r-sm);
}
.th-charter[open] > summary {
    border-bottom: 1px solid var(--th-line);
    margin-bottom: .55rem;
}

.th-charter-body { padding: 0 .7rem .6rem; }

/* The head is the axis, the sentence is what the team says about it. Inline,
   not a definition list: four axes is a paragraph's worth of reading, and any
   more structure than this starts competing with the note. */
.th-ch-line {
    margin: 0 0 .4rem;
    font-family: var(--th-ui);
    font-size: var(--th-t-xs);
    line-height: 1.45;
    color: var(--th-muted);
}
.th-ch-line b {
    color: var(--th-text);
    font-weight: 600;
}
.th-ch-line b::after {
    content: "  ";
    white-space: pre;
}

/* The line that stops the charter reading as the canonical answer. */
.th-ch-note {
    margin: .5rem 0 0;
    padding-top: .45rem;
    border-top: 1px dashed var(--th-line);
    font-family: var(--th-ui);
    font-size: var(--th-t-2xs);
    line-height: 1.45;
    font-style: italic;
    color: var(--th-dim);
}

/* ---- the spine ----
   Three counters used to sit on three screens with nothing joining them, so
   the game told you where you were inside an act and never how much of it was
   left. Static markup, one per screen, and aria-current is both the a11y
   contract and the styling hook. No separators between the words: two middots
   to say what a gap already says. */
.th-spine {
    display: flex;
    align-items: baseline;
    gap: 1.1rem;
    margin: 0 0 .6rem;
    padding: 0;
    list-style: none;
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-dim);
}
.th-spine li {
    position: relative;
    padding-bottom: .2rem;
    border-bottom: 2px solid transparent;
}
/* The rail. The three words told you which act you were in and then never moved
   again, so the only thing on screen that could answer "how much of this is
   left" was an act-local counter - and Act II relabels its counter three times,
   so even that could not say you were seven steps of twelve through it.
   thSpineFill writes --th-fill per act: behind you 1, ahead of you 0, and the
   act you are in somewhere between. Cool chrome throughout. Warm colour in this
   sheet only ever means something is about to go wrong, and progress is not
   that. */
.th-spine li::after {
    content: "";
    position: absolute;
    left: 0;
    bottom: -2px;
    height: 2px;
    width: calc(var(--th-fill, 0) * 100%);
    background: var(--th-dim);
    transition: width .45s var(--th-ease-out);
}
/* The act number, drawn by the counter rather than typed into three copies of
   the markup. The word alone said which act you were in but not how far along
   the three it sat; "2 FILE" says both. Dimmer than the word so it reads as an
   index, not as part of the name. */
.th-spine { counter-reset: th-act; }
.th-spine li::before {
    counter-increment: th-act;
    content: counter(th-act) "\2009";
    opacity: .6;
}
/* Act II's three jobs borrow this sheet but are not acts - no numbers. */
.th-pr-jobs li::before { content: none; }
.th-spine [aria-current="step"] {
    color: var(--th-accent);
    border-bottom-color: var(--th-accent);
}
.th-spine [aria-current="step"]::after { background: var(--th-accent); }

/* inside the console, where the accent is nearly invisible on #0B0E14 */
.th-spine-dark {
    padding: .25rem .5rem 0;
    color: var(--th-c-dim);
}
.th-spine-dark li::after { background: var(--th-c-dim); }
.th-spine-dark [aria-current="step"] {
    color: var(--th-c-hot);
    border-bottom-color: var(--th-c-hot);
}
.th-spine-dark [aria-current="step"]::after { background: var(--th-c-hot); }

/* The directive. It is not permanent chrome any more - the dock asks the same
   question in larger type under every note - so it earns its row during the
   warm-up and on note 01, then hides. When it is up it is the loudest line on
   the screen rather than the greyest, which is the point of having it at all. */
.th-ask-head {
    margin: 0 0 1.15rem;
    font-size: var(--th-t-lg);
    font-weight: 700;
    line-height: 1.35;
    color: var(--th-text);
    animation: th-in .25s var(--th-ease-out) both;
    font-family: var(--th-display);
}
.th-ask-head[hidden] { display: none; }

.th-pattern-tag {
    display: none;
    flex: none;
    padding: .22rem .5rem;
    border-radius: 999px;
    border: 1px solid var(--th-accent-line);
    background: var(--th-accent-soft);
    color: var(--th-accent);
    font-family: var(--th-display);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-none);
    font-weight: 700;
}

/* Leads the row now that the directive has moved below it, so the topology tag
   and the sound toggle sit against the right edge. */
.th-count {
    font-family: var(--th-mono);
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-none);
    font-variant-numeric: tabular-nums;
    color: var(--th-dim);
    flex: 1;
}
.th-count b { color: var(--th-text); }

.th-sound {
    flex: none;
    width: 2rem;
    height: 2rem;
    display: grid;
    place-items: center;
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: 999px;
    box-shadow: var(--th-plate);
    color: var(--th-muted);
    font-size: var(--th-t-sm);
    cursor: pointer;
    touch-action: manipulation;
}
/* Both glyphs are in the markup; the state picks one. Muted is its own drawing
   (the speaker with a cross), not the "on" drawing struck through and faded. */
.th-sound .th-ico-off,
.th-sound[aria-pressed="false"] .th-ico-on { display: none; }
.th-sound[aria-pressed="false"] .th-ico-off { display: inline; }
.th-sound[aria-pressed="false"] { color: var(--th-dim); }

/* ---- the chrome's one icon family ----------------------------------------
   Every glyph in the HUD, the dialogs and the hint is a 24-grid stroke drawing
   in currentColor, so a control and its mark are always the same ink and the
   row of discs reads as one set. Weight is fixed here, size by the container.
   The debrief's .th-sec-mark is the same idea on a 16 grid, left alone. */
.th-ico {
    width: 1.05rem;
    height: 1.05rem;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.75;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* ============================================================ deck ===== */

/* Two pseudo-cards behind the live one, so the pile visibly thins out. */
/* The card is sized by its note, not by a fixed box. A three-line note used to
   float in the middle of 16rem of nothing, which read as a broken screen. */
.th-deck {
    position: relative;
    min-height: 0;
    display: flex;
    /* the ::before pile sits about 14px proud of the card's bottom edge, so the
       clearance has to cover the pile before it buys any air at all */
    margin-bottom: 1.6rem;
}
.th-deck::before,
.th-deck::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: var(--th-r-plate);
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    box-shadow: var(--th-plate);
}
.th-deck::before { transform: translateY(14px) scale(.94); opacity: .6; }
.th-deck::after { transform: translateY(7px) scale(.97); opacity: .85; }
/* The pile used to vanish in one frame - display:none cannot be transitioned.
   Collapsing it instead means the last two notes visibly cost you the deck. */
.th-deck::before,
.th-deck::after { transition: transform .4s var(--th-ease-out), opacity .4s ease; }
.th-deck.th-last::before,
.th-deck.th-last::after { opacity: 0; transform: translateY(0) scale(1); }

.th-card, .th-ghost {
    position: relative;
    z-index: 2;
    flex: 1;
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-plate);
    padding: 1rem 1.1rem;
    display: flex;
    flex-direction: column;
    justify-content: flex-start;
    box-shadow: var(--th-raise);
    /* none, not pan-y: the down-swipe IS a move, so the browser must not steal
       it for scrolling. The page is still scrollable everywhere but the card. */
    touch-action: none;
    user-select: none;
    -webkit-user-select: none;
}
.th-card.th-drag { transition: none; will-change: transform; }
.th-card.th-snap { transition: transform .3s var(--th-ease-out); }
/* The next note is dealt onto the pile. It used to appear between two frames
   with no motion at all, which is most of why the screen read as dead. */
.th-card.th-deal { animation: th-deal .34s var(--th-ease-out) both; }
/* Three right calls in a row and the deck deals brisker. Nothing is announced
   and nothing changes colour - the game just stops making a player who has
   found the rhythm wait for the animation. It drops back on the first miss. */
.th-card.th-brisk.th-deal { animation-duration: .2s; }
@keyframes th-deal {
    from { opacity: 0; transform: translateY(16px) scale(.97) rotate(-.7deg); }
    to   { opacity: 1; transform: none; }
}

/* the edge lights up in the colour of the slot you are dragging toward */
.th-card.th-to-file { border-color: var(--th-accent); box-shadow: 0 0 0 3px var(--th-accent-soft), var(--th-raise); }
.th-card.th-to-run { border-color: var(--th-warn); box-shadow: 0 0 0 3px var(--th-warn-soft), var(--th-raise); }
.th-card.th-to-never { border-color: var(--th-bad); box-shadow: 0 0 0 3px var(--th-bad-soft), var(--th-raise); }
/* the deck variant is the warm-up's wrong answer, where the teach-card replaces
   the card's className before a shake on the card itself could play */
.th-card.th-reject,
.th-deck.th-reject { animation: th-shake .38s ease; }
@keyframes th-shake {
    0%, 100% { transform: none; }
    20% { transform: translateX(-9px); }
    45% { transform: translateX(8px); }
    70% { transform: translateX(-5px); }
}

/* the outgoing card, flung clear while the next one is already rendering */
.th-ghost {
    position: absolute;
    inset: 0;
    z-index: 3;
    pointer-events: none;
    opacity: 0;
    /* the pose the card was released in. thFly writes these on a swipe; a key
       or a tap leaves them at rest, which is the centre it always flew from */
    --gx: 0px;
    --gy: 0px;
    --gr: 0deg;
}
/* Off the hand at the speed it was thrown, not from a standstill: an ease-in
   here started slow at the one moment the player had just moved fast. */
.th-ghost.th-fly-file { animation: th-fly-r .34s cubic-bezier(.23, 1, .32, 1) forwards; }
.th-ghost.th-fly-never { animation: th-fly-l .34s cubic-bezier(.23, 1, .32, 1) forwards; }
.th-ghost.th-fly-run { animation: th-fly-d .34s cubic-bezier(.23, 1, .32, 1) forwards; }
@keyframes th-fly-r {
    from { opacity: 1; transform: translate(var(--gx), var(--gy)) rotate(var(--gr)); }
    35%  { opacity: 1; }
    to   { opacity: 0; transform: translate(120%, -12%) rotate(16deg); }
}
@keyframes th-fly-l {
    from { opacity: 1; transform: translate(var(--gx), var(--gy)) rotate(var(--gr)); }
    35%  { opacity: 1; }
    to   { opacity: 0; transform: translate(-120%, -12%) rotate(-16deg); }
}
@keyframes th-fly-d {
    from { opacity: 1; transform: translate(var(--gx), var(--gy)) rotate(var(--gr)) scale(1); }
    35%  { opacity: 1; }
    to   { opacity: 0; transform: translate(var(--gx), 85%) rotate(0deg) scale(.9); }
}

/* The note is a document, not a playing card: mono identifier over body copy
   at reading size. The class is .th-card-kind - thRenderCard() writes that, and
   the sheet styled a .th-card-kicker nothing has emitted for a while, so every
   note title rendered as bare body text. */
.th-card-kind {
    margin: 0 0 .6rem;
    font-family: var(--th-display);
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-none);
    color: var(--th-accent);
    font-weight: 700;
}
.th-card-text {
    margin: 0;
    font-size: var(--th-t-lg);
    line-height: 1.55;
}

/* ---- the word reveal ----
   A note used to arrive complete in one frame, with the three slots already
   lit, so the decision was available before the sentence was. It comes in at
   reading pace now.

   Words rather than characters, and the text is written out in full before the
   spans animate, so the line is already laid out: nothing reflows, no ghost
   layer is needed to hold the wrap, and the hard breaks in .th-t-line survive.
   thReveal() sets --i per word and --tw once on the parent. */

.th-tw > span {
    display: inline-block;
    animation: th-tw-in .8s ease both;
    /* the base delay lets th-deal / th-task-in land first - two motions
       starting on the same frame read as one blurred one */
    animation-delay: calc(160ms + var(--i) * var(--tw, 84ms));
}
@keyframes th-tw-in {
    from { opacity: 0; transform: translateY(.18em); }
    to   { opacity: 1; transform: none; }
}
/* Landed early, because the player did something. Reading faster than the
   reveal should never mean waiting for it: thRevealNow puts this on anything
   still arriving the moment a key or a pointer says the player is ahead. */
.th-tw.th-tw-done > span { animation: none; opacity: 1; transform: none; }

/* The tally is a readout, this is an instruction: two centred lines of the same
   weight competed, so this one steps back a size and stops breathing at full
   strength. */
.th-hint {
    margin: 0;
    text-align: center;
    font-family: var(--th-ui);
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-none);
    color: var(--th-dim);
    opacity: .8;
    /* Two breaths to draw the eye on arrival, then still. It sat under every
       note of the act on an endless loop, which WCAG 2.2.2 does not allow past
       five seconds and which dropped the line below AA at every trough. */
    animation: th-breathe 2.4s ease-in-out 2;
}
@keyframes th-breathe { 50% { opacity: .4; } }

/* ---- the cut, made visible ---------------------------------------------
   The old version simply swapped the text, so the note got shorter and nobody
   learned anything. Now the original is held struck through while the tight
   line lands under it, then it collapses away. */

.th-cutting .th-cut-old {
    margin: 0 0 .5rem;
    color: var(--th-dim);
    text-decoration: line-through;
    text-decoration-color: var(--th-bad);
    animation: th-cut-out .9s cubic-bezier(.4, 0, .2, 1) forwards;
}
@keyframes th-cut-out {
    0%   { opacity: 1; max-height: 14rem; }
    45%  { opacity: .55; max-height: 14rem; }
    100% { opacity: 0; max-height: 0; margin-bottom: 0; }
}
.th-cutting .th-cut-new {
    animation: th-cut-in .45s var(--th-ease-out) .4s both;
}
@keyframes th-cut-in {
    from { opacity: 0; transform: translateY(6px); }
    to   { opacity: 1; transform: none; }
}
.th-cutting .th-cut-note {
    border-top: 0;
    padding-top: .5rem;
    color: var(--th-accent);
    animation: th-cut-in .4s var(--th-ease-out) .55s both;
}

/* the warm-up card prices nothing, so its footer says so instead of showing pips */

/* ---- the hint ----------------------------------------------------------
   This was a three-button panel parked permanently between the note and the
   dock, with a spend counter on it. It was meant to teach the three axes the
   game turns on - does it last, is it dangerous, does it die tonight - by
   simply being on screen. What it actually did was put three questions in
   front of a player who was trying to answer a fourth, and a budget to manage
   on top. Players read it as clutter, which is a fair reading of anything you
   did not ask for.

   One button now, in the HUD. Same three questions, same three-per-run cost,
   but you go and get them. Deliberately neutral ink: painting the questions
   in the three slot colours would map question to answer, which is the one
   thing this must not do. Same reason the printed line is always a question. */

.th-hint-btn {
    margin-left: auto;
    flex: none;
    display: flex;
    align-items: center;
    gap: .3rem;
    padding: .34rem .72rem .34rem .55rem;
    font: inherit;
    /* reading voice, not the machine's: this is a word you press, and mono is
       reserved for the file, the terminal and the meters */
    font-family: var(--th-ui);
    font-size: var(--th-t-xs);
    font-weight: 600;
    letter-spacing: var(--th-k-none);
    line-height: 1;
    background: var(--th-panel);
    border: 1px solid var(--th-line-2);
    border-radius: 999px;
    color: var(--th-muted);
    cursor: pointer;
    touch-action: manipulation;
    transition: background .16s ease, border-color .16s ease, color .16s ease, transform .12s var(--th-ease-out);
}
/* The counter is the whole cost model, but it only shows up once the first one
   is spent - thRenderHint() sets [hidden] until then. The word carries the
   button on its own; the number is the receipt. */
.th-hint-btn i[hidden] { display: none; }
.th-hint-btn i {
    min-width: 1.05rem;
    padding: .08rem 0;
    text-align: center;
    border-radius: 999px;
    background: var(--th-accent-soft);
    color: var(--th-accent);
    font-style: normal;
    font-size: var(--th-t-2xs);
    font-variant-numeric: tabular-nums;
}
.th-hint-btn .th-ico { width: .95rem; height: .95rem; }
/* the receipt ticks as it counts down - thOnHint pulses it on a spend */
.th-hint-btn i.th-tick { animation: th-tick .36s var(--th-ease-out); }
@keyframes th-tick { 40% { transform: scale(1.3); } }
.th-hint-btn:hover:not(:disabled) { border-color: var(--th-accent-line); color: var(--th-text); }
.th-hint-btn.on {
    border-color: var(--th-accent-line);
    background: var(--th-accent-soft);
    color: var(--th-accent);
}
.th-hint-btn:disabled { opacity: .45; cursor: default; }
.th-hint-btn:disabled i { background: var(--th-fill); color: var(--th-dim); }
.th-hint-btn.th-reject { animation: th-shake .38s ease; }
.th-hint-btn:focus-visible {
    outline: none;
    border-color: var(--th-accent);
    box-shadow: 0 0 0 3px var(--th-accent-soft);
}

/* What the hint bought: the same three questions, sharpened to this note.
   Still questions. Nothing in here names a slot - see TH_CARDS.probes. */
/* One of three, spent, and the questions print below the card while the button
   that bought them sits up in the HUD. thOnHint scrolls this into view and hands
   it to thPulse, which reuses the sheet's own .th-flash - no new keyframe. */
/* One callout, not three strips: a flush plate (the card above owns this
   screen's one raise) with a small head naming what it is, then the three
   questions as rows split by hairlines. It drops in from its top edge - it came
   from above, where the button is - and the rows follow on a short stagger. */
.th-hint-out {
    margin: 0 0 1rem;
    padding: .7rem .95rem .35rem;
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
    box-shadow: var(--th-plate);
    transform-origin: 50% 0;
    animation: th-hint-drop .32s var(--th-ease-out) both;
}
.th-hint-out[hidden] { display: none; }
@keyframes th-hint-drop {
    from { opacity: 0; transform: translateY(-4px) scaleY(.97); }
    to   { opacity: 1; transform: none; }
}
.th-hint-h {
    display: flex;
    align-items: center;
    gap: .4rem;
    margin: 0 0 .15rem;
    color: var(--th-accent);
    font-size: var(--th-t-xs);
    font-weight: 600;
}
.th-hint-h .th-ico { width: .9rem; height: .9rem; }
.th-hint-out span {
    display: block;
    padding: .6rem 0;
    color: var(--th-text);
    font-size: var(--th-t-md);
    line-height: 1.5;
    animation: th-cut-in .35s var(--th-ease-out) both;
}
.th-hint-out span + span { border-top: 1px solid var(--th-line); }
.th-hint-out span:nth-of-type(1) { animation-delay: .06s; }
.th-hint-out span:nth-of-type(2) { animation-delay: .1s; }
.th-hint-out span:nth-of-type(3) { animation-delay: .14s; }

/* ---- the teach-card ------------------------------------------------------
   Verdict first, clause second, over the note the player is already looking at.
   It covers the deck for a beat rather than opening a panel, so nothing moves
   and there is nothing to dismiss. Any input skips it.

   It is .th-verdict-card, not .th-verdict - the end screen owns that name for
   its own line, and the two used to collide.

   "Defensible" and "Wrong home" are different outcomes and are painted
   differently. They used to share --th-warn and the chip text was the only way
   to tell them apart, which is not a way to tell them apart. */

.th-verdict-card {
    position: absolute;
    inset: 0;
    z-index: 4;
    display: flex;
    flex-direction: column;
    justify-content: center;
    gap: .45rem;
    padding: 1.2rem 1.1rem;
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-plate);
    box-shadow: var(--th-plate);
    animation: th-verdict-in .3s var(--th-ease-out) both;
}

/* No timer now - the card sits until Got it, Enter, Space or Escape. Only the
   button reads as clickable; the rest of the card is copy, not a big hit
   target for a dismiss that doesn't otherwise exist. */
.th-v-dismiss {
    align-self: flex-end;
    min-height: 44px;
    padding: .55rem 1.1rem;
    background: var(--th-panel-2);
    border-color: var(--th-line-2);
    color: var(--th-text);
    font-size: var(--th-t-sm);
}
.th-v-dismiss:hover { border-color: var(--th-accent); color: var(--th-accent); }
@keyframes th-verdict-in {
    from { opacity: 0; transform: scale(.97); }
    to   { opacity: 1; transform: none; }
}
/* Leaving is quicker than arriving, and never in the way: the next note is
   already dealt underneath, so the card goes transparent to input at once. */
.th-verdict-card.th-v-out {
    pointer-events: none;
    animation: th-verdict-out .15s var(--th-ease-out) forwards;
}
@keyframes th-verdict-out {
    to { opacity: 0; transform: scale(.97); }
}

.th-verdict-card b {
    align-self: flex-start;
    padding: .28rem .6rem;
    border-radius: var(--th-r-sm);
    font-family: var(--th-display);
    font-size: var(--th-t-xs);
    font-weight: 700;
    letter-spacing: var(--th-k-none);
    animation: th-chip-in .32s var(--th-ease-out) .07s both;
}
@keyframes th-chip-in {
    from { opacity: 0; transform: scale(.84); }
    to   { opacity: 1; transform: none; }
}

.th-verdict-card span {
    font-size: var(--th-t-md);
    line-height: 1.5;
    color: var(--th-text);
}

.th-verdict-card u {
    text-decoration: none;
    margin-top: .2rem;
    font-family: var(--th-ui);
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-none);
    color: var(--th-dim);
}

/* right: the accent, which everywhere else in this game means "this is the
   thing that works" */
.th-v-ok            { border-color: var(--th-accent); }
.th-v-ok b          { background: var(--th-accent-soft); color: var(--th-accent); }

/* defensible: a second reasonable home, so it reads as a near miss rather than
   a mistake. Solid amber, filled chip. */
.th-v-part          { border-color: var(--th-warn); }
.th-v-part b        { background: var(--th-warn); color: #FFFFFF; }

/* wrong home: no credit, but nothing was damaged. Dashed border, hollow chip -
   visibly a different outcome to defensible without shouting like a secret. */
.th-v-warn          { border-color: var(--th-warn); border-style: dashed; }
.th-v-warn b        { background: transparent; color: var(--th-warn); box-shadow: inset 0 0 0 1px var(--th-warn); }

/* a credential written down: the only hard-wrong in the deck */
.th-v-bad           { border-color: var(--th-bad); }
.th-v-bad b         { background: var(--th-bad); color: #FFFFFF; }

/* ---- the dock: three thumb targets, in swipe order --------------------- */

.th-slots {
    position: sticky;
    bottom: 0;
    z-index: 5;
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: .45rem;
    padding: .6rem 0 calc(.6rem + env(safe-area-inset-bottom, 0px));
    /* keep the zero-alpha stop in step with --th-bg: `transparent` interpolates
       through grey in sRGB and leaves a fringe across the card above it */
    background: linear-gradient(180deg, rgba(237, 234, 227, 0) 0%, var(--th-bg) 30%);
}
.th-slot {
    position: relative;
    display: grid;
    gap: .12rem;
    padding: .75rem .4rem;
    min-height: 4.5rem;
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
    box-shadow: var(--th-plate);
    color: var(--th-text);
    font-family: var(--th-mono);
    cursor: pointer;
    touch-action: manipulation;
    transition: transform .12s var(--th-ease-out), background .15s ease,
                border-color .15s ease, box-shadow .15s ease;
}
.th-slot i { font-style: normal; font-size: var(--th-t-sm); line-height: 1; color: var(--th-dim); }
.th-slot b { font-size: var(--th-t-xs); letter-spacing: var(--th-k-none); }
.th-slot span { font-size: var(--th-t-2xs); color: var(--th-dim); line-height: 1.3; }
.th-slot u {
    position: absolute;
    top: .35rem;
    right: .4rem;
    text-decoration: none;
    font-size: var(--th-t-2xs);
    font-variant-numeric: tabular-nums;
    color: var(--th-dim);
    font-family: var(--th-ui);
    letter-spacing: var(--th-k-none);
    font-weight: 700;
}
/* The stub. The three counts in this dock all report the past; this is the only
   thing on the screen that points forward - the file, gaining a line every time
   a note is sent here, so the object Act II opens on is one the player watched
   being built. Six marks maximum, drawn by a repeating gradient rather than by
   six elements, and clipped to the number written on data-lines.
   No colour of its own: it is the file, not a status. */
.th-stub {
    position: absolute;
    left: .4rem;
    top: .4rem;
    width: .8rem;
    height: 0;
    background-image: repeating-linear-gradient(
        180deg, var(--th-dim) 0 1px, transparent 1px 3px);
    opacity: .55;
    transition: height .35s var(--th-ease-out);
}
.th-stub[data-lines="1"] { height: 3px; }
.th-stub[data-lines="2"] { height: 6px; }
.th-stub[data-lines="3"] { height: 9px; }
.th-stub[data-lines="4"] { height: 12px; }
.th-stub[data-lines="5"] { height: 15px; }
.th-stub[data-lines="6"] { height: 18px; }

.th-slot:active { transform: scale(.96); }
/* the lifetime is carried by a colour gutter on the swipe edge plus the label,
   never by a filled block - this dock sits under the card all game */
.th-slot[data-slot="never"] { border-left: 3px solid var(--th-bad); border-radius: 0 var(--th-r-ctl) var(--th-r-ctl) 0; }
.th-slot[data-slot="run"] { border-bottom: 3px solid var(--th-warn); }
.th-slot[data-slot="file"] { border-right: 3px solid var(--th-accent); border-radius: var(--th-r-ctl) 0 0 var(--th-r-ctl); }
.th-slot[data-slot="never"] b { color: var(--th-bad); }
.th-slot[data-slot="run"] b { color: var(--th-warn); }
.th-slot[data-slot="file"] b { color: var(--th-accent); }
.th-slot:disabled { opacity: .4; cursor: default; }

.th-slot.armed[data-slot="never"] { border-color: var(--th-bad); background: var(--th-bad-soft); }
.th-slot.armed[data-slot="run"] { border-color: var(--th-warn); background: var(--th-warn-soft); }
.th-slot.armed[data-slot="file"] { border-color: var(--th-accent); background: var(--th-accent-soft); }
/* The dock leans toward the card it is about to take, and its colour gutter
   thickens. The tint alone was too quiet to read as a target. The thickening
   is an inset shadow against the border rather than a wider border: a wider
   border re-laid the slot out every frame and shoved its label sideways. */
.th-slot.armed { transform: translateY(-3px); box-shadow: var(--th-plate), 0 0 0 4px var(--th-ring); }
.th-slot.armed[data-slot="never"] { box-shadow: var(--th-plate), inset 4px 0 0 var(--th-bad), 0 0 0 4px var(--th-ring); }
.th-slot.armed[data-slot="run"] { box-shadow: var(--th-plate), inset 0 -4px 0 var(--th-warn), 0 0 0 4px var(--th-ring); }
.th-slot.armed[data-slot="file"] { box-shadow: var(--th-plate), inset -4px 0 0 var(--th-accent), 0 0 0 4px var(--th-ring); }
.th-slot.hit { animation: th-hit .4s var(--th-ease-out); }
@keyframes th-hit {
    0% { transform: scale(1.06); box-shadow: 0 0 0 5px var(--th-ring), var(--th-plate); }
    100% { transform: none; }
}

/* ============================================================= end ===== */

/* The tier chip. This was a 4.6rem circle carrying a single letter - a grade on
   a scale that was never printed anywhere, so "C" told the player nothing except
   that they had been marked. Worse, the .th-rank-C / .th-rank-D warm variant was
   never applied by any code path, so the two worst ranks rendered on the accent
   and read as a reward.
   A word costs one line of markup and needs no legend. These are the same five
   bands, and deliberately the same five strings certificate.html prints, so the
   chip here and the certificate below it cannot disagree. */
.th-tier {
    display: flex;
    align-items: center;
    gap: .45rem;
    width: max-content;
    margin: 0 auto .5rem;
    padding: .38rem .7rem .34rem;
    border: 1px solid var(--th-accent-line);
    border-radius: var(--th-r-sm);
    background: var(--th-accent-soft);
    color: var(--th-accent);
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    font-weight: 700;
}
/* An empty chip is a floating hairline box for however long the screen takes to
   populate, and on the very first paint there is no tier yet. */
.th-tier:empty { display: none; }
/* A lozenge, not a bullet: the same mark the deck stamps a filed note with. */
.th-tier::before {
    content: "";
    width: 6px;
    height: 6px;
    background: currentColor;
    transform: rotate(45deg);
}
/* The stamp is on the class the engine adds, not on .th-tier itself. On the
   element it played once and never again, because a replay reuses the element
   and a CSS animation does not re-trigger on its own. thPulse forces the
   reflow. */
.th-tier.th-stamp { animation: th-stamp .5s var(--th-ease-out) both; }
@keyframes th-stamp { from { opacity: 0; transform: scale(1.9) rotate(-12deg); } }

.th-score-wrap { position: relative; text-align: center; padding: .3rem 0 .9rem; }
.th-score-wrap b {
    display: block;
    font-size: var(--th-t-score);
    font-weight: 700;
    line-height: 1;
    letter-spacing: var(--th-k-tight);
    color: var(--th-text);
    font-variant-numeric: tabular-nums;
    font-family: var(--th-display);
}
/* Two short rules flanking the caption. The score is a measurement and this is
   the only place on the end screen that says so out loud. */
.th-score-wrap > span {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .55rem;
    margin-top: .45rem;
    font-family: var(--th-display);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-wide);
    color: var(--th-dim);
    font-weight: 700;
}
.th-score-wrap > span::before,
.th-score-wrap > span::after {
    content: "";
    width: 26px;
    height: 1px;
    background: var(--th-line-2);
}

/* The take-away. Accent fill makes it the only saturated object on a page of
   paper, which is emphasis enough - it does not also need to float, so it takes
   --th-plate and not --th-raise like the primary button further down. */
.th-btn-cert {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: .55rem;
    width: 100%;
    margin: 1.1rem 0 0;
    padding: .85rem 1.2rem;
    background: var(--th-accent);
    border-color: var(--th-accent-hi);
    color: #FFFFFF;          /* 6.39:1 on --th-accent */
    letter-spacing: var(--th-k-none);
    box-shadow: var(--th-plate);
}
.th-btn-cert:hover { background: var(--th-accent-hi); }
.th-btn-cert svg {
    width: 17px;
    height: 17px;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.4;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.th-burst { position: absolute; inset: 0; pointer-events: none; overflow: visible; }
.th-burst i {
    position: absolute;
    top: 45%;
    left: 50%;
    width: 6px;
    height: 6px;
    border-radius: 1px;
    background: var(--th-accent);
    animation: th-burst .9s cubic-bezier(.15, .8, .3, 1) forwards;
    animation-delay: calc(var(--i) * 14ms);
}
.th-burst i:nth-child(3n) { background: var(--th-warn); }
.th-burst i:nth-child(3n+1) { background: var(--th-stale); }
@keyframes th-burst {
    from { opacity: 1; transform: translate(-50%, -50%) rotate(0); }
    to { opacity: 0; transform: translate(calc(-50% + var(--x)), calc(-50% + var(--y))) rotate(220deg); }
}

/* Scoped to the id on purpose. As a bare .th-verdict this collided with the
   teach-card overlay above, which silently inherited a 40ch centred column. */
#th-verdict {
    margin: .8rem auto 0;
    max-width: 40ch;
    text-align: center;
    color: var(--th-muted);
    font-size: var(--th-t-md);
    line-height: 1.55;
}

.th-report { display: block; }

/* ---- the three acts -----------------------------------------------------
   This was `repeat(2, 1fr)` holding three children, with no media query to
   catch it, so Triage and The File paired up and The Run orphaned onto a second
   row at half width. Three scores laid out as two-and-a-bit is the single
   loudest reason the report read as broken, and no amount of type fixed it.

   Stacked full-width rows instead, one per act, played in the order they were
   played. The numeral is not ornament - Triage, The File and The Run ARE Acts I,
   II and III, so the marker carries real sequence, and each act keeps the ink it
   wore while the player was inside it. */

.th-acts { display: grid; gap: .5rem; margin: 1.3rem 0 1.25rem; }

.th-a-row {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr) auto;
    grid-template-areas:
        "act name score"
        "act bar  bar";
    align-items: center;
    gap: .1rem .65rem;
    padding: .6rem .75rem .65rem;
    background: var(--th-panel-2);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
}
/* One ink for all three bars, deliberately. Giving each act its own colour is
   the obvious move and it is wrong here: the four inks this sheet owns already
   mean the four file headings, and the same blue reading "Conventions" in Act II
   and "Act II" on the report teaches the player a contradiction. The acts are
   already distinguished by a numeral, a name and a position. */
.th-a-act {
    grid-area: act;
    display: grid;
    place-items: center;
    width: 1.75rem;
    height: 1.75rem;
    border: 1px solid var(--th-line-2);
    border-radius: 999px;
    color: var(--th-dim);
    font-family: var(--th-display);
    font-size: var(--th-t-xs);
    font-weight: 700;
    line-height: 1;
}
.th-a-name {
    grid-area: name;
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-dim);
}
.th-a-score {
    grid-area: score;
    font-family: var(--th-display);
    font-size: var(--th-t-md);
    font-weight: 700;
    letter-spacing: var(--th-k-none);
    color: var(--th-text);
    font-variant-numeric: tabular-nums;
}
.th-a-score em {
    font-style: normal;
    font-size: var(--th-t-xs);
    color: var(--th-dim);
}
.th-a-bar {
    grid-area: bar;
    display: block;
    height: 5px;
    margin-top: .4rem;
    border-radius: 999px;
    background: var(--th-fill);
    overflow: hidden;
}
.th-a-bar u {
    display: block;
    height: 100%;
    width: var(--w, 0%);
    background: var(--th-accent);
    text-decoration: none;
    animation: th-grow .8s var(--th-ease-out) both;
    animation-delay: var(--d, 0ms);
}
@keyframes th-grow { from { width: 0; } }

/* ---- section cards ------------------------------------------------------
   The demarcation fix. Every heading in this report was a bare <p class="th-r-sub">
   and that class had no rule anywhere in the sheet, so three distinct sections
   arrived as one undifferentiated grey column and the page read as unfinished.
   A card each, with a stamped head: hairline mark, label, and a rule running out
   to the edge. The marks are stroke paths on currentColor rather than emoji -
   this page is paper and ink, and a colour emoji lands on it as a sticker. */

.th-sec {
    margin: 0 0 .55rem;
    padding: .1rem .8rem;
    background: var(--th-panel-2);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
    animation: th-rise .5s var(--th-ease-out) both;
    animation-delay: var(--d, 0ms);
}
@keyframes th-rise { from { opacity: 0; transform: translateY(10px); } }

/* The head is the fold's handle now. Same stamp as before - glyph, label, rule -
   plus the closed-state number on the right and a chevron that turns. */
.th-sec-head {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin: 0;
    padding: .7rem 0;
    cursor: pointer;
    list-style: none;
    font-family: var(--th-display);
    font-size: var(--th-t-xs);
    font-weight: 700;
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-accent);
}
.th-sec-head::-webkit-details-marker { display: none; }
.th-sec-head:focus-visible { outline: 2px solid var(--th-accent); outline-offset: 2px; }
/* The sub-line rides under the label instead of taking a paragraph of its own -
   shut, the whole head is one line tall. */
.th-sec-label { display: block; flex: 1 1 auto; min-width: 0; }
.th-sec-label i {
    display: block;
    margin-top: .1rem;
    font-family: var(--th-ui);
    font-size: var(--th-t-2xs);
    font-weight: 400;
    letter-spacing: var(--th-k-none);
    text-transform: none;
    color: var(--th-dim);
}
/* The rule is the first thing to give up its width - on a narrow head the
   label and the number matter, the hairline does not. */
.th-sec-head u { flex: 0 1 2.5rem; min-width: 0; height: 1px; background: var(--th-line-2); text-decoration: none; }
.th-sec-meta {
    flex: none;
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    font-weight: 400;
    letter-spacing: var(--th-k-label);
    color: var(--th-dim);
    font-variant-numeric: tabular-nums;
}
/* A chevron, drawn in borders. Turns down when the fold opens. */
.th-sec-head::after {
    content: "";
    flex: none;
    width: 7px;
    height: 7px;
    margin: -3px .1rem 0 .15rem;
    border-right: 1.5px solid var(--th-dim);
    border-bottom: 1.5px solid var(--th-dim);
    transform: rotate(-45deg);
    transition: transform .18s ease;
}
.th-sec[open] > .th-sec-head::after { transform: rotate(45deg); margin-top: -5px; }
.th-sec-body { padding: 0 0 .85rem; }
.th-sec-mark {
    width: 16px;
    height: 16px;
    flex: none;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.3;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* ---- the file, graded by the thing that read it -------------------------
   The payoff shot: the artefact the player actually wrote, each line tagged
   with what the next session did with it. Closes on the one line worth
   keeping after the tab shuts. */

.th-readout {
    margin: 0;               /* the enclosing .th-sec owns the gap and the plate */
    padding: .8rem .85rem;
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
}
/* The <ul>/<li> readout, the .th-fr-* status inks and .th-readout-head were
   deleted here: the engine has emitted <details class="th-ro"> since the
   expander landed, and none of those rules had matched an element in months.
   The live styling is under "the debrief readout" further down the sheet. */

.th-takeaway {
    margin: .9rem 0 0;
    padding-top: .75rem;
    border-top: 1px solid var(--th-line);
    font-size: var(--th-t-md);
    font-weight: 600;
    line-height: 1.5;
    color: var(--th-text);
}

/* The .th-grp / .th-rlist / .th-r* / .th-played debrief was deleted here -
   lines styling a shape the engine stopped emitting when the report moved to
   section cards and <details> expanders. Nothing in the JS or the markup has
   referenced any of it since. */

/* The two aids, reported rather than scored. Spending nothing is worth saying
   out loud; spending all three is not a failure either. */
.th-aids {
    display: flex;
    flex-wrap: wrap;
    gap: .4rem;
    margin: .7rem 0 0;
    font-family: var(--th-ui);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-none);
    color: var(--th-dim);
}
.th-aids span {
    padding: .3rem .6rem;
    border-radius: 999px;
    background: var(--th-fill);
    font-variant-numeric: tabular-nums;
}
.th-aids b { color: var(--th-text); }
.th-actions { display: grid; gap: .45rem; margin: 1.1rem 0 0; }

/* ---- the shared result-screen widgets ----------------------------------
   .tt-lb and .tt-ng are theme-neutral Nintendo chrome: indigo borders, orange
   submit, platinum inputs, Arial. Correct everywhere else on the site, and a
   different product when dropped into this page. Overridden here only, scoped
   under .th-root so no other game is touched. */

.th-root .tt-lb,
.th-root .tt-ng { font-family: var(--th-ui); max-width: none; }

.th-root .tt-lb {
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-plate);
    box-shadow: var(--th-plate);
    padding: 1.25rem;
    color: var(--th-text);
}
.th-root .tt-lb-title { color: var(--th-text); font-weight: 600; }
.th-root .tt-lb-sub,
.th-root .tt-lb-status { color: var(--th-dim); font-family: var(--th-mono); letter-spacing: var(--th-k-label); }
.th-root .tt-lb-status.err { color: var(--th-bad); }
.th-root .tt-lb-refresh {
    background: var(--th-panel-2);
    border: 1px solid var(--th-line);
    border-radius: 999px;
    box-shadow: none;
    color: var(--th-muted);
}
.th-root .tt-lb-refresh:hover { background: var(--th-fill); }

.th-root .tt-lb-score-chip {
    background: var(--th-panel-2);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
    box-shadow: none;
}
.th-root .tt-lb-score-num {
    font-family: var(--th-mono);
    font-variant-numeric: tabular-nums;
    color: var(--th-accent);
}
.th-root .tt-lb-score-cap { color: var(--th-dim); font-family: var(--th-mono); }
.th-root .tt-lb-name {
    background: var(--th-panel);
    border: 1px solid var(--th-line-2);
    border-radius: var(--th-r-ctl);
    color: var(--th-text);
}
.th-root .tt-lb-name::placeholder { color: var(--th-dim); }
.th-root .tt-lb-name:focus {
    border-color: var(--th-accent);
    background: var(--th-panel);
    box-shadow: 0 0 0 3px var(--th-accent-soft);
}
.th-root .tt-lb-submit {
    background: var(--th-accent);
    color: #FFFFFF;
    border: 1px solid transparent;
    border-radius: var(--th-r-ctl);
    box-shadow: none;
    font-family: var(--th-ui);
    letter-spacing: var(--th-k-label);
}
.th-root .tt-lb-submit:hover,
.th-root .tt-lb-submit:active { background: var(--th-accent); filter: brightness(1.08); box-shadow: none; }
.th-root .tt-lb-submit-score { background: rgba(255, 255, 255, .22); font-family: var(--th-mono); }

.th-root .tt-lb-posted {
    background: var(--th-accent-soft);
    border: 1px solid var(--th-accent-line);
    border-radius: var(--th-r-ctl);
    box-shadow: none;
    color: var(--th-text);
}
.th-root .tt-lb-posted-check { background: var(--th-accent); }
.th-root .tt-lb-posted-text strong { color: var(--th-accent); }

.th-root .tt-lb-row {
    background: var(--th-panel-2);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
}
.th-root .tt-lb-row.r1 { background: var(--th-warn-soft); border-color: rgba(138, 84, 22, .28); }
.th-root .tt-lb-row.r2 { background: var(--th-panel-2); border-color: var(--th-line-2); }
.th-root .tt-lb-row.r3 { background: var(--th-fill); border-color: var(--th-line-2); }
.th-root .tt-lb-row.me { background: var(--th-accent-soft); border-color: var(--th-accent-line); }
.th-root .tt-lb-rank { color: var(--th-dim); font-family: var(--th-mono); }
.th-root .tt-lb-name-txt { color: var(--th-text); font-weight: 600; }
.th-root .tt-lb-row.me .tt-lb-name-txt { color: var(--th-accent); }
.th-root .tt-lb-when,
.th-root .tt-lb-loading,
.th-root .tt-lb-empty { color: var(--th-dim); }
.th-root .tt-lb-tag { color: var(--th-dim); font-family: var(--th-mono); }
.th-root .tt-lb-score {
    color: var(--th-text);
    font-family: var(--th-mono);
    font-variant-numeric: tabular-nums;
}
.th-root .tt-lb-reveal-rank { color: var(--th-text); }
.th-root .tt-lb-reveal-rank strong { color: var(--th-accent); font-family: var(--th-mono); }
.th-root .tt-lb-reveal-pct {
    color: var(--th-accent);
    background: var(--th-accent-soft);
    border: 1px solid var(--th-accent-line);
    border-radius: 999px;
}
.th-root .tt-lb-reveal-muted { color: var(--th-dim); }
.th-root .tt-lb-confetti { border-radius: var(--th-r-plate); }

.th-root .tt-ng-progress { color: var(--th-dim); font-family: var(--th-mono); letter-spacing: var(--th-k-label); }
.th-root .tt-ng-progress strong { color: var(--th-text); }
.th-root .tt-ng-card {
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
    box-shadow: var(--th-plate);
}
.th-root .tt-ng-card:hover { border-color: var(--th-accent); box-shadow: var(--th-plate); }
.th-root .tt-ng-card-name { color: var(--th-text); font-weight: 600; }
.th-root .tt-ng-card-hook { color: var(--th-muted); }
.th-root .tt-ng-card-cta { color: var(--th-dim); font-family: var(--th-mono); letter-spacing: var(--th-k-label); }
.th-root .tt-ng-card:hover .tt-ng-card-cta { color: var(--th-accent); }
.th-root .tt-ng-all { color: var(--th-dim); font-family: var(--th-mono); letter-spacing: var(--th-k-label); }
.th-root .tt-ng-all:hover { color: var(--th-accent); }


/* ===================================================== start additions ===== */

/* A room that wants to compare answers plays one seed. Everyone else gets a
   fresh nine notes, so the field is optional and reads as optional. */
.th-seed-row {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin: 0;
}
.th-seed-row label {
    font-family: var(--th-ui);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-none);
    text-transform: uppercase;
    color: var(--th-dim);
}
.th-seed-in {
    font: inherit;
    font-family: var(--th-mono);
    font-size: var(--th-t-sm);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    width: 6.5rem;
    padding: .4rem .6rem;
    background: var(--th-fill);
    border: 1px solid var(--th-line-2);
    border-radius: var(--th-r-sm);
    color: var(--th-text);
}
.th-seed-in::placeholder { color: var(--th-dim); text-transform: none; letter-spacing: var(--th-k-none); }
.th-seed-in:focus-visible {
    outline: none;
    border-color: var(--th-accent);
    box-shadow: 0 0 0 3px var(--th-accent-soft);
}

/* The running tally under the deck. One number, and only the one that matters -
   how much you are about to make every future run read. */
.th-tally-line {
    margin: 0 0 .5rem;
    text-align: center;
    font-family: var(--th-mono);
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-dim);
    min-height: 1rem;
}

/* ============================================== act II: the file ========== */

.th-f-stage { display: grid; gap: .9rem; }

/* ---- the document ----
   Rendered from state on every change, so what is on screen can never drift
   from what is being scored. */

/* Cut into the desk, not resting on it. The file you are authoring is the
   one surface here that is not a control, and recessing it is the cheapest way
   to say so - it also stops it competing with .th-f-task, which is the thing
   actually asking you a question. */
.th-f-doc {
    background: var(--th-panel-2);
    border: 1px solid var(--th-line-2);
    border-radius: var(--th-r-plate);
    padding: .7rem .8rem;
    box-shadow: var(--th-inset);
    font-family: var(--th-mono);
    /* The desktop block already set this a step down because the rail has one
       job - show the shape of the file in one look - and reading size wrapped
       every line into three. The phone was still on reading size, which is the
       width where that costs most. */
    font-size: var(--th-t-xs);
    line-height: 1.45;
}
.th-f-name {
    margin: 0 0 .5rem;
    padding-bottom: .4rem;
    border-bottom: 1px solid var(--th-line);
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-dim);
}

/* ---- the rail during a precedence step ----
   The three conflicts open the act, so the player's own file is still empty
   while they run and a blank page beside the question said nothing. The two
   files the question IS about go here instead, drawn from the conflict's own
   paths - and they stay up for all three steps, which is the half the primer
   card could never do on its own.

   It scrolls sideways rather than wrapping, same as the card's tree: a wrapped
   tree is not a tree. */
.th-f-tree {
    margin: 0;
    font: inherit;
    white-space: pre;
    overflow-x: auto;
    color: var(--th-muted);
}
/* Which one is nearer is the entire lesson, so the two files it is deciding
   between are the only lines in ink. */
.th-f-tree b {
    font-weight: 700;
    color: var(--th-text);
}
.th-f-tree .th-f-t-near { color: var(--th-accent); }
/* ---- the four inks, wired to both ends ----
   The only place in the sheet where a section id becomes a colour. The doc band
   and the answer button read the same custom property, so a heading can never
   be one colour in the file and another on the button that files it there. */
.th-f-sec[data-sec="setup"],       .th-t-secs .th-t-opt[data-val="setup"]       { --th-sec: var(--th-sec-setup); }
.th-f-sec[data-sec="conventions"], .th-t-secs .th-t-opt[data-val="conventions"] { --th-sec: var(--th-sec-conventions); }
.th-f-sec[data-sec="never"],       .th-t-secs .th-t-opt[data-val="never"]       { --th-sec: var(--th-sec-never); }
.th-f-sec[data-sec="issues"],      .th-t-secs .th-t-opt[data-val="issues"]      { --th-sec: var(--th-sec-issues); }

/* Each heading is a band on the page, not a line of text with a gap under it.
   A band has an inside, so an empty one reads as a place waiting for something
   rather than as a heading nothing follows. */
.th-f-sec {
    margin-bottom: .3rem;
    padding: .25rem .45rem .3rem;
    border-left: 3px solid var(--th-sec, var(--th-line-2));
    border-radius: 0 var(--th-r-sm) var(--th-r-sm) 0;
    background: color-mix(in srgb, var(--th-sec, transparent) 5%, transparent);
    transition: background-color .2s ease;
}
.th-f-sec:last-child { margin-bottom: 0; }
.th-f-head {
    margin: 0 0 .25rem;
    color: var(--th-sec, var(--th-accent));
    font-weight: 700;
    letter-spacing: var(--th-k-none);
}
/* Empty on purpose - the heading above it names the section and the answer
   buttons carry the descriptions, so printing them here as well put the same
   four strings on screen twice. The dotted rule is the whole message, so it
   needs a height of its own now that no text gives it one. */
.th-f-empty {
    margin: 0;
    min-height: .85rem;
    border-top: 1px dotted var(--th-sec, var(--th-line-2));
    opacity: .4;
}
.th-f-line {
    margin: 0 0 .1rem;
    padding: .05rem .35rem .05rem .4rem;
    border-left: 1px solid var(--th-line-2);
    color: var(--th-text);
    /* A line lands rather than appears. It is the only motion in the panel, so
       it is the thing the eye follows after a choice. */
    animation: th-drop-land .34s var(--th-ease-out) both;
}
/* still carrying words the agent has to interpret */
.th-f-line.weak {
    border-left: 2px solid var(--th-warn);
    background: var(--th-warn-soft);
    color: var(--th-muted);
}
.th-f-line.live {
    border-left: 2px solid var(--th-accent);
    background: var(--th-accent-soft);
    color: var(--th-text);
}

/* ---- the gutter ----
   One number per line the ledger charges for, so the bill can be read off the
   page itself and not only off the strip under the question. Headings are free
   and get no number: the gap in the count IS the fact that a heading costs
   nothing. The numbers come from thRenderDoc(), not from a CSS counter,
   because a note that costs three lines is drawn as three lines and a counter
   would number the note instead of the lines. */
.th-f-row {
    display: grid;
    grid-template-columns: 1.7em 1fr;
    gap: .55rem;
    align-items: baseline;
}
.th-f-row i {
    font-style: normal;
    text-align: right;
    padding-right: .4rem;
    border-right: 1px solid var(--th-line-2);
    color: var(--th-dim);
    opacity: .6;
    letter-spacing: 0;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
    user-select: none;
}
.th-f-line.weak i { color: var(--th-warn); opacity: .85; }
.th-f-line.live i { color: var(--th-accent); opacity: 1; }
/* the heading keeps the gutter width so the file has one left edge, and says
   nothing in it - because a heading is not billed */
.th-f-head { padding-left: calc(1.7em + .55rem); }
@keyframes th-drop-land {
    from { opacity: 0; transform: translateY(-6px); }
    to   { opacity: 1; transform: none; }
}

/* ---- the ledger ----
   A file is read on every run, forever, so its cost is a rate. It used to be a
   small plate at the foot of the file rail, which is the one place on this
   screen nobody looks twice: last, smallest, under a panel that scrolls. It is
   a strip under the question now, in the column already being read, and the
   weekly figure - the only one that has ever changed how anybody writes - is
   set as the lead rather than as the tail of a sentence.

   Flat and cut into the desk, never raised. The task above it is the one
   raised element on this screen and the ledger must not compete with it. */

.th-f-meter {
    margin-top: 1rem;
    padding: .85rem 1rem .9rem;
    background: var(--th-panel-2);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
    box-shadow: var(--th-inset);
}
/* It is hidden until the first trim, so it has to arrive rather than appear -
   a plate that materialises between two frames reads as a layout bug. */
.th-f-meter.th-m-in { animation: th-m-in .42s var(--th-ease-out) both; }
@keyframes th-m-in {
    from { opacity: 0; transform: translateY(8px); }
    to   { opacity: 1; transform: none; }
}
/* why it is on screen at all, said once, in the plate it belongs to */
.th-f-m-head {
    margin: 0 0 .6rem;
    font-family: var(--th-display);
    font-size: var(--th-t-xs);
    font-weight: 700;
    line-height: 1.3;
    letter-spacing: var(--th-k-none);
    color: var(--th-muted);
}

/* The track is the file as it arrived, not the budget, with the budget marked
   on it. Scaled to the budget the fill was clipped full at 16 lines and still
   clipped full at 9, so the one number the player was working on was the one
   thing the bar could not show. It drains toward the notch instead. */
.th-f-bar {
    position: relative;
    height: 6px;
    border-radius: 999px;
    background: var(--th-fill);
    box-shadow: var(--th-inset);
    overflow: hidden;
}
.th-f-bar i {
    display: block;
    height: 100%;
    width: var(--fill, 0%);
    border-radius: 999px;
    background: var(--th-accent);
    transition: width .5s var(--th-ease-out), background-color .3s ease;
}
.th-f-bar.th-trim i { background: var(--th-warn); }
.th-f-bar.over i { background: var(--th-bad); }
/* the budget, drawn on the track. Inset to top/bottom 0 - the bar clips, so a
   negative offset would take the notch's ends with it. */
.th-f-bar::after {
    content: '';
    position: absolute;
    top: 0;
    bottom: 0;
    left: var(--target, 100%);
    width: 2px;
    background: var(--th-text);
    opacity: .45;
}

/* Three figures on one line, read left to right from what is visible in the
   file to what is not: the lines you can count, one run, one week. */
.th-f-m-figs {
    display: grid;
    grid-template-columns: repeat(3, 1fr);
    gap: .5rem;
    margin-top: .75rem;
}
.th-f-m-fig {
    margin: 0;
    display: flex;
    flex-direction: column;
    gap: .1rem;
    padding-left: .8rem;
    border-left: 1px solid var(--th-line);
}
.th-f-m-fig:first-child { padding-left: 0; border-left: 0; }
.th-f-m-fig b {
    font-family: var(--th-mono);
    font-size: var(--th-t-md);
    font-weight: 700;
    line-height: 1.15;
    letter-spacing: var(--th-k-none);
    color: var(--th-text);
    font-variant-numeric: tabular-nums;
}
.th-f-m-fig span {
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-dim);
}
/* the one figure that changes how people write, so the one figure set large.
   Everything else on this strip is context for it. */
.th-f-m-lead b { font-size: var(--th-t-xl); }
.th-f-m-lead span { color: var(--th-muted); }

/* Over budget with a trim still ahead is not a failure, it is the starting
   condition of the act. Amber says "not yet"; red waits until nothing is left
   to cut with. Both are AA on --th-panel-2. */
.th-f-meter b.th-trim { color: var(--th-warn); }
.th-f-meter b.over { color: var(--th-bad); }

/* ---- the task ----
   One question at a time. The panel is replaced wholesale on every step, so it
   animates in rather than mutating in place. */

.th-f-task {
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-plate);
    padding: 1.15rem 1.1rem;
    box-shadow: var(--th-raise);
}
.th-f-task.th-task-in { animation: th-task-in .38s var(--th-ease-out) both; }
@keyframes th-task-in {
    from { opacity: 0; transform: translateY(10px); }
    to   { opacity: 1; transform: none; }
}
/* answered: the options stop inviting a second thought */
.th-f-task.th-t-done .th-t-opt { pointer-events: none; }
.th-f-task.th-t-done .th-t-opt:not(.picked):not(.missed) { opacity: .45; }

.th-t-ask {
    margin: 0 0 .8rem;
    font-size: var(--th-t-lg);
    font-weight: 600;
    line-height: 1.35;
}
.th-t-flag {
    margin: 0 0 .8rem;
    padding: .6rem .75rem;
    border-left: 3px solid var(--th-warn);
    border-radius: 0 var(--th-r-sm) var(--th-r-sm) 0;
    background: var(--th-warn-soft);
    color: var(--th-warn);
    font-size: var(--th-t-sm);
    line-height: 1.5;
}
.th-t-line {
    margin: 0 0 .9rem;
    padding: .7rem .85rem;
    background: var(--th-fill);
    border-left: 3px solid var(--th-accent);
    border-radius: 0 var(--th-r-sm) var(--th-r-sm) 0;
    font-family: var(--th-mono);
    font-size: var(--th-t-sm);
    line-height: 1.55;
}

.th-t-opts { display: grid; gap: .45rem; }
.th-t-secs { grid-template-columns: 1fr 1fr; }

.th-t-opt {
    font: inherit;
    text-align: left;
    display: grid;
    gap: .15rem;
    padding: .75rem .8rem;
    background: var(--th-panel-2);
    border: 1px solid var(--th-line-2);
    border-radius: var(--th-r-ctl);
    color: var(--th-text);
    cursor: pointer;
    touch-action: manipulation;
    transition: border-color .15s ease, background-color .15s ease, transform .12s var(--th-ease-out);
}
.th-t-opt:hover { border-color: var(--th-accent); background: var(--th-panel); }
.th-t-opt:active { transform: scale(.985); }
.th-t-opt:focus-visible { outline: none; box-shadow: 0 0 0 3px var(--th-accent-soft); border-color: var(--th-accent); }
.th-t-opt b { font-size: var(--th-t-md); font-weight: 600; line-height: 1.4; }
.th-t-opt span { font-size: var(--th-t-sm); line-height: 1.45; color: var(--th-muted); }
.th-t-opt.picked {
    border-color: var(--th-ok);
    background: var(--th-ok-soft);
}
.th-t-opt.missed {
    border-color: var(--th-bad);
    background: var(--th-bad-soft);
}

/* ---- the four headings, as answers ----
   Same ink, same gutter, same squared-off gutter edge as the band it files into.
   The heading itself goes mono because it is markdown - it is the literal string
   that will appear in the file, and setting it in the reading face made it a
   label for a place rather than the place itself. */
.th-t-secs .th-t-opt {
    border-left: 3px solid var(--th-sec);
    border-radius: 0 var(--th-r-ctl) var(--th-r-ctl) 0;
}
.th-t-secs .th-t-opt b {
    font-family: var(--th-mono);
    font-size: var(--th-t-sm);
    font-weight: 700;
    color: var(--th-sec);
}
.th-t-secs .th-t-opt:hover {
    border-color: var(--th-sec);
    background: color-mix(in srgb, var(--th-sec) 7%, var(--th-panel));
}
.th-t-secs .th-t-opt:focus-visible {
    border-color: var(--th-sec);
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--th-sec) 22%, transparent);
}
/* The answer, pointed at the place it lands. Four pairs rather than one rule,
   because CSS cannot match "the band whose ink equals this button's" - but the
   payoff is that the question stops being abstract: you hover an answer and the
   file shows you the shelf. Hover only, so it costs a touch device nothing. */
@media (hover: hover) {
    .th-f-stage:has(.th-t-opt[data-val="setup"]:hover)       .th-f-sec[data-sec="setup"],
    .th-f-stage:has(.th-t-opt[data-val="conventions"]:hover) .th-f-sec[data-sec="conventions"],
    .th-f-stage:has(.th-t-opt[data-val="never"]:hover)       .th-f-sec[data-sec="never"],
    .th-f-stage:has(.th-t-opt[data-val="issues"]:hover)      .th-f-sec[data-sec="issues"] {
        background: color-mix(in srgb, var(--th-sec) 16%, transparent);
    }
}

/* the rewrite options read as lines of a file, because that is what they are */
.th-t-rw b { font-family: var(--th-mono); font-size: var(--th-t-sm); font-weight: 400; }
.th-t-rw span {
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-dim);
}

/* two files, side by side, arguing. The nested one is indented because that is
   how it looks in a repo and the indent is half the lesson. */
.th-t-conf { gap: .55rem; }
/* What the file governs, said before the file is named. The indent alone was
   the only mark on the nested card below 560px, and an indent is only legible
   to someone who already knows what it means. */
.th-t-file em {
    display: block;
    font-style: normal;
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-dim);
    margin-bottom: .35rem;
}
.th-t-file i {
    display: block;
    font-family: var(--th-mono);
    font-style: normal;
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-label);
    color: var(--th-accent);
    margin-bottom: .3rem;
}
.th-t-file b { font-family: var(--th-mono); font-size: var(--th-t-sm); font-weight: 400; line-height: 1.5; }
/* The nesting cue is a gutter inside the card, never an offset outside it.
   Nudging the nested card down the page put two answers to one question on two
   different lines, which reads as a layout fault long before it reads as a
   repo tree. Same top edge, same height, and the deeper file carries the rail. */
.th-t-conf .th-t-file:last-child {
    margin-left: 1.1rem;
    border-left: 3px solid var(--th-line-2);
    border-radius: 0 var(--th-r-ctl) var(--th-r-ctl) 0;
}
.th-t-foot {
    margin: .8rem 0 0;
    text-align: center;
    font-size: var(--th-t-sm);
    color: var(--th-muted);
}

/* the reason, after the choice, never before it */
.th-t-note {
    margin: .8rem 0 0;
    padding-top: .75rem;
    border-top: 1px solid var(--th-line);
    font-size: var(--th-t-sm);
    line-height: 1.55;
    color: var(--th-muted);
    animation: th-cut-in .35s var(--th-ease-out) both;
}
.th-t-note.good { color: var(--th-ok); }
/* A held note is a tab stop, because tabbing to it is what stops its clock. It
   stays a paragraph - the focus ring is the house one, and nothing about the
   note lifts, boxes or fills. See thHoldNote(). */
.th-t-note[tabindex]:focus-visible {
    outline: 2px solid var(--th-accent);
    outline-offset: 3px;
}

/* ============================================== act III: the run ========== */

/* The second palette. Scoped to a class on the root rather than a media query
   or a global :root, so the shared site chrome is never repainted and Acts I
   and II keep their own world untouched. The file rail deliberately stays on
   the LIGHT surface: your careful file, being read by something that is not
   you. That contrast is the whole image. */

.th-root.th-dark {
    --th-c-bg: #0B0E14;
    --th-c-panel: #12161F;
    --th-c-line: #1E2530;
    --th-c-text: #D6DBE4;      /* 12.1:1 on --th-c-bg */
    /* Was #8A94A6 (5.2:1) - passed AA but review flagged it as low, and it is
       the colour carrying most of the console: log dim lines, the beat count,
       both levers. Lifted to ~10.7:1 on --th-c-bg. */
    --th-c-dim: #B9C2D0;
    --th-c-hot: #F0A02C;       /* about to do something. 8.9:1 */
    --th-c-bad: #F2545B;       /* did it. 5.4:1 */
    --th-c-ok: #4ADE9B;        /* 10.2:1 */
    background: var(--th-c-bg);
    box-shadow: 0 0 0 100vmax var(--th-c-bg);
    transition: background-color .4s ease, box-shadow .4s ease, filter .42s cubic-bezier(.4, 0, .2, 1);
}

/* ---- the handoff cut ----
   Act II to Act III. The desk drains, the file survives the drain, and the
   console is already typing before the room goes dark. A transition rather
   than a keyframe: the keyframe came back to full colour before the swap, so
   the desk dipped and recovered instead of draining, and it snapped if the cut
   was interrupted. Held grey through the swap, it eases back only once the
   room is already going dark. */
.th-root.th-handing { filter: saturate(.08) brightness(.74); }

.th-r-stage { display: grid; gap: .8rem; }

/* ---- the file rail ---- */

.th-r-rail {
    background: var(--th-panel);
    border-radius: var(--th-r-ctl);
    padding: .6rem .7rem;
    font-family: var(--th-mono);
    /* Bottom of the scale, and deliberately: this panel is not being read, it
       is being watched. The player wrote every line in Act II - what Act III
       asks of it is "which line is the head on and how much is left", and that
       is a question about shape. At reading size the six notes filled the
       column and the shape of the file stopped fitting in one look. */
    font-size: var(--th-t-2xs);
    line-height: 1.4;
    color: var(--th-text);
    /* Phone first: the rail, the bar, the log and the patch panel are one
       column here, and the patch is the thing that has to be reachable. The
       900px block gives the rail back its height once it has its own column. */
    max-height: 10rem;
    overflow-y: auto;
}
.th-r-name {
    margin: 0 0 .35rem;
    padding-bottom: .3rem;
    border-bottom: 1px solid var(--th-line);
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-none);
    color: var(--th-dim);
    font-family: var(--th-display);
    font-weight: 700;
}
.th-r-head { margin: .35rem 0 .12rem; color: var(--th-accent); font-weight: 700; }
.th-r-head:first-of-type { margin-top: 0; }
.th-r-loose { color: var(--th-bad); }

.th-r-line {
    margin: 0;
    padding: .1rem .35rem .1rem .45rem;
    border-left: 2px solid transparent;
    border-radius: 0 var(--th-r-xs) var(--th-r-xs) 0;
    color: var(--th-muted);
    transition: background-color .18s ease, color .18s ease, border-color .18s ease;
}
/* Reserved on every line, filled only on .read - so the marker (the second
   sense, next to the border/tint colour already carries) does not shift the
   text over when it appears. */
.th-r-line::before {
    content: '';
    display: inline-block;
    width: .75em;
}
/* The read-head. Every line, one at a time, in file order - the whole argument
   for keeping the file short, made without a sentence. */
.th-r-line.read {
    border-left-color: var(--th-accent);
    background: var(--th-accent-soft);
    color: var(--th-text);
    animation: th-readhead .42s ease both;
}
.th-r-line.read::before {
    content: '▸';
    color: var(--th-accent);
}
@keyframes th-readhead {
    from { transform: translateX(-3px); }
    to   { transform: none; }
}
/* The line the agent honours during the read and abandons in the last beat.
   It has to be visibly obeyed before it can be visibly dropped, so this stays
   lit for the whole run. Declared above .patched so that once the drift beat is
   answered the patched label wins. */
.th-r-line.honoured {
    border-left-color: var(--th-ok);
    color: var(--th-text);
}
.th-r-line.honoured:not(.patched)::after {
    content: ' honoured';
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-ok);
}
.th-r-line.patched {
    border-left-color: var(--th-ok);
    color: var(--th-text);
}
.th-r-line.patched::after {
    content: ' patched';
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-ok);
}

/* The patch panel below quotes this exact line - a dashed outline ties the
   quote back to its place in the file without relying on the quote alone. */
.th-r-line.th-r-focus {
    outline: 1px dashed var(--th-warn);
    outline-offset: -1px;
    background: var(--th-warn-soft);
}

/* ---- the console ----
   A machined case with the log recessed into it. */

.th-r-console {
    background: var(--th-c-panel);
    border: 1px solid var(--th-c-line);
    border-radius: var(--th-r-plate);
    padding: .55rem;
    position: relative;
}

.th-r-bar {
    display: flex;
    align-items: center;
    gap: .5rem;
    padding: .25rem .5rem .55rem;
}
.th-r-beat {
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-c-dim);
    font-variant-numeric: tabular-nums;
}
.th-r-levers { display: flex; gap: .35rem; margin-left: auto; }
.th-lever {
    font: inherit;
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    padding: .32rem .6rem;
    /* Was transparent-on-line-colour - a 1px border in --th-c-line on the
       console background, both dark, was the "1x in the corner is almost
       invisible" review note. A fill of its own gives it an edge at rest. */
    background: rgba(255, 255, 255, .05);
    border: 1px solid var(--th-c-dim);
    border-radius: 999px;
    color: var(--th-c-text);
    cursor: pointer;
    touch-action: manipulation;
    transition: border-color .15s ease, color .15s ease, background-color .15s ease, transform .12s var(--th-ease-out);
}
.th-lever:hover { border-color: var(--th-c-hot); color: var(--th-c-hot); }
/* Same press as every other control here; .th-btn and .th-slot set the scale. */
.th-len:active, .th-hint-btn:active:not(:disabled), .th-lever:active { transform: scale(.97); }
/* The round chrome answers on the press, not the release - a disc that only
   reacts once you let go feels dead under the thumb. */
.th-help, .th-book, .th-sound, .th-g-x, .th-g-replay {
    transition: transform .1s ease-out, background-color .16s ease, border-color .16s ease, color .16s ease;
}
.th-help:active, .th-book:active, .th-sound:active, .th-g-x:active, .th-g-replay:active { transform: scale(.92); }
#th-start .th-help:active, .th-book-link:active { transform: none; opacity: .7; }
.th-lever i { font-style: normal; }
.th-lever[aria-pressed="true"] {
    border-color: var(--th-c-hot);
    color: var(--th-c-hot);
    background: rgba(240, 160, 44, .14);
}

/* ---- the burn meter ----
   Act II ends with a static bill: what this file costs, every run. Act III
   spends it in front of you, one line at a time, and keeps spending on every
   re-read after a patch. The strip sits between the beat bar and the log
   because that is the order the player reads the console in: what beat, what
   it costs, what happened.

   Warm is rationed to the thing that is costing you. The figure and the
   topology chip are the only two amber things on the run screen, and the log
   below already uses --th-c-hot for "about to do something" - which is exactly
   what a climbing meter is. */

.th-r-burn {
    background: var(--th-c-bg);
    border-radius: var(--th-r-ctl);
    padding: .55rem .7rem .6rem;
    margin-bottom: .55rem;
}
.th-r-b-top {
    display: flex;
    align-items: center;
    gap: .5rem;
    margin: 0;
}
.th-r-b-lab {
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-c-dim);
}
/* The multiplier, named. A supervisor is not a faster sequential run, it is the
   same file bought three times, and this chip is the only place the game says
   so while the number beside it is moving. */
.th-r-b-readers {
    margin-left: auto;
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    font-weight: 700;
    letter-spacing: var(--th-k-label);
    padding: .16rem .45rem;
    border: 1px solid var(--th-c-hot);
    border-radius: 999px;
    color: var(--th-c-hot);
    white-space: nowrap;
}

.th-r-b-fig {
    display: flex;
    align-items: baseline;
    gap: .5rem;
    margin: .15rem 0 .4rem;
}
.th-r-b-fig b {
    font-family: var(--th-mono);
    font-size: var(--th-t-xl);
    font-weight: 700;
    line-height: 1.1;
    letter-spacing: var(--th-k-none);
    color: var(--th-c-hot);
    /* the digits must not reflow on every tick */
    font-variant-numeric: tabular-nums;
}
.th-r-b-fig i {
    font-style: normal;
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-c-dim);
}
/* One frame of brightening per line read. Not a lift and not a flash - the
   number has to stay legible while it moves, because reading it is the point. */
.th-b-tick { animation: th-b-tick .3s ease both; }
@keyframes th-b-tick {
    0%   { color: var(--th-c-text); }
    100% { color: var(--th-c-hot); }
}

/* Scaled to one full read of an on-budget file by the widest topology, so the
   two shapes fill it at different rates. Scaled to each pattern's own allowance
   they would look identical and the bar would say nothing. */
.th-r-b-bar {
    height: 3px;
    border-radius: 999px;
    background: var(--th-c-line);
    overflow: hidden;
}
.th-r-b-bar i {
    display: block;
    height: 100%;
    width: var(--fill, 0%);
    background: var(--th-c-hot);
    transition: width .4s var(--th-ease-out), background-color .3s ease;
}
.th-r-b-bar.over i { background: var(--th-c-bad); }

/* The other topology, priced on the same file, live. It is the only sentence
   on this screen written in lower case - it is an aside, not a readout. */
.th-r-b-alt {
    margin: .4rem 0 0;
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    line-height: 1.4;
    color: var(--th-c-dim);
}

.th-r-term {
    background: var(--th-c-bg);
    border-radius: var(--th-r-ctl);
    padding: .65rem .7rem;
    min-height: 11rem;
    max-height: 22rem;
    overflow-y: auto;
    font-family: var(--th-mono);
    /* A terminal is a transcript, not an essay. At 13/1.7 four beats did not
       fit a desk screen and the log spent the run scrolled away from its own
       command line. One step down and a tighter leading fits the whole run. */
    font-size: var(--th-t-xs);
    line-height: 1.45;
}
.th-r-log {
    margin: 0;
    color: var(--th-c-dim);
    animation: th-line-in .22s var(--th-ease-out) both;
    word-break: break-word;
}
@keyframes th-line-in {
    from { opacity: 0; transform: translateY(3px); }
    to   { opacity: 1; transform: none; }
}
.th-r-log.cmd { color: var(--th-c-text); }
.th-r-log.dim { color: var(--th-c-dim); }
.th-r-log.ok { color: var(--th-c-ok); }
.th-r-log.hot { color: var(--th-c-hot); }
.th-r-log.bad {
    color: var(--th-c-bad);
    font-weight: 700;
    letter-spacing: var(--th-k-label);
    margin: .4rem 0;
}
.th-r-log.th-flash { animation: th-flash .7s ease both; }
@keyframes th-flash {
    0%, 100% { opacity: 1; }
    30%      { opacity: .25; }
}

/* the damage landing. Two frames, no blur, no lift. */
#th-run.th-damage .th-r-console { animation: th-damage .5s cubic-bezier(.36, .07, .19, .97) both; }
@keyframes th-damage {
    0%      { transform: none; box-shadow: none; }
    18%     { transform: translateX(-5px); box-shadow: 0 0 0 2px var(--th-c-bad); }
    38%     { transform: translateX(4px); }
    58%     { transform: translateX(-2px); box-shadow: 0 0 0 2px var(--th-c-bad); }
    100%    { transform: none; box-shadow: none; }
}

/* held: the run stops so a room can argue about a line. Was opacity: .5 on
   the log/burn meter - a contrast cut on a screen review already flagged as
   too dark, and a state told only by fading things out. A dashed border on
   the console plus " · HELD" after the beat count instead - the HOLD lever
   already flips its own label to RESUME, so this is the second place the
   same state shows in text, not colour. */
#th-run.th-held .th-r-console { border-color: var(--th-c-hot); border-style: dashed; }
#th-run.th-held .th-r-beat::after { content: ' · HELD'; color: var(--th-c-hot); }

/* ---- the patch ----
   Rises over the log rather than replacing it, so the damage stays visible
   while you decide what to do about it. */

.th-r-patch {
    margin-top: .55rem;
    padding: .95rem .9rem;
    background: var(--th-c-panel);
    border: 1px solid var(--th-c-hot);
    border-radius: var(--th-r-ctl);
}
.th-r-patch[hidden] { display: none; }
.th-r-patch.th-patch-up { animation: th-patch-up .4s var(--th-ease-out) both; }
@keyframes th-patch-up {
    from { opacity: 0; transform: translateY(14px); }
    to   { opacity: 1; transform: none; }
}
.th-p-say {
    margin: 0 0 .7rem;
    font-size: var(--th-t-sm);
    line-height: 1.55;
    color: var(--th-c-text);
}
.th-p-ask {
    margin: 0 0 .6rem;
    font-family: var(--th-display);
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-none);
    color: var(--th-c-hot);
    font-weight: 700;
}
.th-p-opts { display: grid; gap: .4rem; }
.th-p-opt {
    font: inherit;
    font-family: var(--th-mono);
    font-size: var(--th-t-sm);
    line-height: 1.5;
    text-align: left;
    padding: .65rem .75rem;
    background: var(--th-c-bg);
    border: 1px solid var(--th-c-line);
    border-radius: var(--th-r-sm);
    color: var(--th-c-text);
    cursor: pointer;
    touch-action: manipulation;
    transition: border-color .15s ease, transform .12s var(--th-ease-out);
}
.th-p-opt:hover { border-color: var(--th-c-hot); }
.th-p-opt:active { transform: scale(.985); }
.th-p-opt:focus-visible { outline: none; border-color: var(--th-c-hot); box-shadow: 0 0 0 3px rgba(240, 160, 44, .2); }
.th-p-opt.picked { border-color: var(--th-c-ok); color: var(--th-c-ok); }
.th-p-opt.missed { border-color: var(--th-c-bad); color: var(--th-c-bad); opacity: .6; }
.th-p-opt:disabled { cursor: not-allowed; }

/* ==================================================== end additions ======= */

/* The seed, offered as a link rather than a number to copy by hand. */
.th-seed {
    margin: 1rem 0 0;
    padding: .85rem .95rem;
    background: var(--th-panel-2);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .5rem .7rem;
}
.th-seed b {
    font-family: var(--th-mono);
    font-size: var(--th-t-sm);
    letter-spacing: var(--th-k-label);
    color: var(--th-accent);
}
.th-seed span { flex: 1 1 12rem; font-size: var(--th-t-sm); line-height: 1.45; color: var(--th-muted); }
.th-seed-replay,
.th-seed-copy {
    font: inherit;
    font-size: var(--th-t-sm);
    padding: .4rem .8rem;
    background: var(--th-panel);
    border: 1px solid var(--th-line-2);
    border-radius: 999px;
    color: var(--th-muted);
    cursor: pointer;
    touch-action: manipulation;
    transition: border-color .15s ease, color .15s ease;
}
.th-seed-replay:hover,
.th-seed-copy:hover { border-color: var(--th-accent); color: var(--th-accent); }

/* Of the two, replay is the one the panel is arguing for - the seed is only
   worth printing because the same nine notes can be played again. */
.th-seed-replay { border-color: var(--th-accent-line); color: var(--th-accent); }

/* what the file costs, every run, forever */
.th-cost {
    margin: 0;               /* the enclosing .th-sec owns the gap now */
    padding: .7rem .85rem;
    background: var(--th-fill);
    border-radius: var(--th-r-sm);
    font-family: var(--th-mono);
    font-size: var(--th-t-sm);
    letter-spacing: var(--th-k-none);
    color: var(--th-muted);
    font-variant-numeric: tabular-nums;
}
.th-cost b { color: var(--th-text); }
.th-cost i { font-style: normal; color: var(--th-line-2); padding: 0 .2rem; }

.th-misses {
    margin: 0;               /* the enclosing .th-sec owns the gap now */
    padding: 0;
    list-style: none;
    display: grid;
    gap: .5rem;
}
.th-misses li {
    padding: .7rem .8rem;
    background: var(--th-panel);
    border-left: 3px solid var(--th-warn);
    border-radius: 0 var(--th-r-sm) var(--th-r-sm) 0;
    font-size: var(--th-t-sm);
    line-height: 1.55;
    color: var(--th-muted);
}
.th-misses b { color: var(--th-text); }
.th-misses u {
    text-decoration: none;
    font-family: var(--th-mono);
    font-size: var(--th-t-sm);
    letter-spacing: var(--th-k-none);
    color: var(--th-accent);
}


/* ==================================================== clarity pass ======== */

/* ---- Act I: the flash ---------------------------------------------------
   A right answer used to raise the same full-cover teach-card a wrong one did,
   for 700ms, on every one of nine notes. Nine judgements became nine waits,
   and a player who was right got a sentence explaining something they had just
   demonstrated they understood. Right and defensible are a colour now and the
   next note is already dealing; only a wrong call still stops you.

   The wash is on the deck rather than the card, so it survives the card being
   replaced by the next note mid-animation. */

.th-deck.th-flash-ok,
.th-deck.th-flash-part,
.th-deck.th-flash-bad { position: relative; }

.th-deck.th-flash-ok::after,
.th-deck.th-flash-part::after,
.th-deck.th-flash-bad::after {
    content: "";
    position: absolute;
    inset: 0;
    z-index: 3;
    border-radius: var(--th-r-plate);
    border: 2px solid currentColor;
    background: currentColor;
    opacity: 0;
    transform: none;
    pointer-events: none;
    animation: th-wash .46s var(--th-ease-out) both;
}
.th-deck.th-flash-ok::after   { color: var(--th-accent); }
.th-deck.th-flash-part::after { color: var(--th-warn); }
.th-deck.th-flash-bad::after  { color: var(--th-bad); }

@keyframes th-wash {
    0%   { opacity: 0; }
    22%  { opacity: .16; }
    100% { opacity: 0; }
}

/* ---- between the acts ---------------------------------------------------
   The two stops. Everything the game teaches used to arrive in the debrief,
   after the tab was half closed - a game that spends three acts building a
   lesson should say it while the player is still in the room. The line is
   chosen by the same ranking the end screen uses, narrowed to the act just
   played, so it names a mistake this player actually made. */

#th-principle.on {
    justify-content: center;
    min-height: 60vh;
}

.th-pr-plate {
    margin: auto;
    max-width: 40rem;
    width: 100%;
    padding: 1.9rem 1.5rem 1.6rem;
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-left: 4px solid var(--th-accent);
    border-radius: 0 var(--th-r-plate) var(--th-r-plate) 0;
    box-shadow: var(--th-raise);
    animation: th-cut-in .42s var(--th-ease-out) both;
}

/* An act marker, so it is set as one: display face, tracked, and carrying a
   hairline out to the edge of the plate. It used to be a small grey sentence
   floating above a large black one, which read as a caption rather than as the
   name of the act you have just finished. */
.th-pr-act {
    display: flex;
    align-items: center;
    gap: .6rem;
    margin: 0 0 1rem;
    font-family: var(--th-display);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-dim);
    font-weight: 700;
}
.th-pr-act::before {
    content: "";
    flex: 1 0 auto;
    order: 9;
    height: 1px;
    background: var(--th-line);
}

/* The repo tree, on the one card that has to establish a place rather than a
   principle. Machine voice, because it is a machine's view of a directory, and
   it scrolls sideways rather than wrapping - a wrapped tree is not a tree. */
.th-pr-art {
    margin: 0 0 1.1rem;
    padding: .75rem .85rem;
    background: var(--th-fill);
    border-left: 3px solid var(--th-accent);
    border-radius: 0 var(--th-r-sm) var(--th-r-sm) 0;
    box-shadow: var(--th-inset);
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    line-height: 1.7;
    color: var(--th-text);
    white-space: pre;
    overflow-x: auto;
}
.th-pr-art[hidden] { display: none; }

/* The three jobs of Act II, on the way in. Borrows .th-spine wholesale - the
   acts are named that way at the top of every screen, and this is the same
   claim one level down, so it should not arrive in a second voice. */
.th-pr-jobs { margin: 0 0 1.1rem; }
.th-pr-jobs[hidden] { display: none; }

/* The lesson, at the size of a lesson. This is the one line the player is
   meant to leave with, so nothing else on the screen competes with it. It
   arrives word by word, at reading pace, on the same thReveal() the notes in
   Act I use - the lesson landing in one frame while the demo under it was
   still setting up meant the sentence was over before anyone had started it. */
.th-pr-line {
    margin: 0 0 1.4rem;
    font-size: var(--th-t-xl);
    font-weight: 600;
    line-height: 1.4;
    letter-spacing: var(--th-k-tight);
    color: var(--th-text);
    text-wrap: balance;
}

/* Nothing went wrong in that act, so the card is a result rather than a
   correction and stops using the warning gutter to say so. */
#th-principle.th-pr-clean .th-pr-plate { border-left-color: var(--th-ok); }
#th-principle.th-pr-clean .th-pr-act::after {
    content: "clean";
    padding: .18em .5em;
    border-radius: var(--th-r-xs);
    background: var(--th-ok-soft);
    color: var(--th-ok);
}

/* ---- the demonstration ---------------------------------------------------
   The other half of the teaching. Every heuristic in this game is finally a
   claim about one line of a file, and a claim about a line is best settled by
   showing the line. So: a diff. The version that cost something, then the
   version that survives, with a read-head crossing each in turn and a note on
   what the agent did with it.

   Deliberately built out of material the player already recognises - the mono
   voice of the file in Act II, the accent read-head of the terminal in Act III,
   the same warn / ok semantics as every other mark in the game. An icon set or
   a stock illustration would have been faster and would have belonged to some
   other product.

   All motion is one CSS loop keyed off --th-d-loop, so there is no timer to
   leak when the route unmounts, and the percentages inside each keyframe are
   the timeline: read the bad line, mark it, reveal the good one, read that. */

.th-pr-demo {
    --th-d-loop: 7.2s;
    position: relative;
    margin: 0 0 1.5rem;
    padding: .7rem .85rem .85rem;
    background: var(--th-fill);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-sm);
    box-shadow: var(--th-inset);
    overflow: hidden;
}
.th-pr-demo[hidden] { display: none; }

/* File chrome, one line of it. The stage has to read as a fragment of an
   agents.md and not as a generic before/after panel, and the filename is the
   cheapest way to say so. */
.th-d-file {
    margin: 0 0 .6rem;
    padding-bottom: .55rem;
    border-bottom: 1px dashed var(--th-line-2);
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-dim);
}

.th-d-row {
    position: relative;
    display: grid;
    grid-template-columns: 1.15rem 1fr;
    align-items: baseline;
    gap: 0 .5rem;
    padding: .45rem .5rem;
    margin: 0 -.5rem;
    border-radius: var(--th-r-xs);
}
.th-d-row + .th-d-row { margin-top: .35rem; }

.th-d-row i {
    font-family: var(--th-mono);
    font-size: var(--th-t-sm);
    font-style: normal;
    font-weight: 700;
    text-align: center;
}
.th-d-row code {
    font-family: var(--th-mono);
    font-size: var(--th-t-sm);
    line-height: 1.55;
    /* the two-line entries carry a real \A in the copy: a heading and the rule
       under it is the whole point of the `section` lesson */
    white-space: pre-wrap;
}
/* The consequence, not a caption on it. Small, in the mark's own colour, and
   it arrives only after the head has crossed the line above it. */
.th-d-row em {
    grid-column: 2;
    font-family: var(--th-ui);
    font-size: var(--th-t-xs);
    font-style: normal;
    letter-spacing: var(--th-k-none);
    margin-top: .3rem;
}

.th-d-was i, .th-d-was em { color: var(--th-bad); }
.th-d-was code { color: var(--th-muted); }
.th-d-now i, .th-d-now em { color: var(--th-ok); }
.th-d-now code { color: var(--th-text); }

/* The read-head. One per row, anchored to the row rather than travelling the
   whole stage, because the rows are different heights - the heading entries
   are two lines - and a head that lands between them teaches nothing. */
.th-d-row::after {
    content: "";
    position: absolute;
    inset: 0;
    border-radius: inherit;
    background: linear-gradient(90deg,
        transparent 0%,
        var(--th-accent-soft) 38%,
        var(--th-accent-line) 50%,
        var(--th-accent-soft) 62%,
        transparent 100%);
    pointer-events: none;
    opacity: 0;
    transform: translateX(-110%);
}

.th-d-was          { animation: th-d-mark-bad var(--th-d-loop) linear infinite; }
.th-d-was::after   { animation: th-d-read-a   var(--th-d-loop) linear infinite; }
.th-d-was em       { animation: th-d-say-a    var(--th-d-loop) ease   infinite; }
.th-d-now          { animation: th-d-rise     var(--th-d-loop) ease   infinite; }
.th-d-now::after   { animation: th-d-read-b   var(--th-d-loop) linear infinite; }
.th-d-now em       { animation: th-d-say-b    var(--th-d-loop) ease   infinite; }

@keyframes th-d-read-a {
    0%, 5%    { transform: translateX(-110%); opacity: 0; }
    8%        { opacity: 1; }
    26%       { opacity: 1; }
    30%, 100% { transform: translateX(110%);  opacity: 0; }
}
@keyframes th-d-read-b {
    0%, 55%   { transform: translateX(-110%); opacity: 0; }
    58%       { opacity: 1; }
    76%       { opacity: 1; }
    80%, 100% { transform: translateX(110%);  opacity: 0; }
}
@keyframes th-d-mark-bad {
    0%, 22%   { background: transparent; }
    32%, 92%  { background: var(--th-bad-soft); }
    97%, 100% { background: transparent; }
}
@keyframes th-d-say-a {
    0%, 26%   { opacity: 0; transform: translateY(.3em); }
    36%, 92%  { opacity: 1; transform: none; }
    97%, 100% { opacity: 0; transform: translateY(.3em); }
}
/* The answer waits. Showing both lines from frame one turns the demo into a
   pair of examples; withholding the second one makes it a lesson. */
@keyframes th-d-rise {
    0%, 42%   { opacity: 0; transform: translateY(.45em); background: transparent; }
    52%, 92%  { opacity: 1; transform: none;              background: var(--th-ok-soft); }
    97%, 100% { opacity: 0; transform: translateY(.45em); background: transparent; }
}
@keyframes th-d-say-b {
    0%, 74%   { opacity: 0; transform: translateY(.3em); }
    82%, 92%  { opacity: 1; transform: none; }
    97%, 100% { opacity: 0; transform: translateY(.3em); }
}

/* The button stops being a wall. Full width on a 40rem plate is a slab that
   outweighs the lesson above it; at content width it is the one thing left to
   press and reads as a step forward rather than as the subject of the screen. */
@media (min-width: 34rem) {
    .th-pr-plate .th-btn-primary {
        width: auto;
        align-self: flex-start;
        padding: .85rem 1.5rem;
    }
    .th-pr-plate { display: flex; flex-direction: column; align-items: stretch; }
}

/* ---- Act III: the truncation --------------------------------------------
   The read-head stops partway down and everything below it greys. Watching
   your own file go dark under a line is the argument for keeping it short,
   and it is the only place in the game the token meter has consequences. */

.th-r-line.unread,
.th-r-head.unread {
    opacity: .32;
    text-decoration: line-through;
    text-decoration-color: var(--th-line-2);
}

.th-r-cut {
    margin: .5rem 0;
    padding: .18rem 0;
    border-top: 1px dashed var(--th-bad);
    color: var(--th-bad);
    font-family: var(--th-display);
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-none);
    font-weight: 700;
}

/* ---- the debrief readout ------------------------------------------------
   Every card in the deck carries authored copy - why the note has the lifetime
   it has, what the agent did with it under the wrong heading, what it invented
   when the note never arrived - and until now the scorer was the only thing
   that ever read most of it. Folded away by default: the line and whether it
   landed is the summary, and the argument is one press away for anyone who
   wants it. Nothing here is new writing. */

.th-ro-head {
    margin: .9rem 0 .4rem;
    font-family: var(--th-display);
    font-size: var(--th-t-xs);
    letter-spacing: var(--th-k-none);
    font-weight: 700;
    color: var(--th-accent);
}
.th-ro-head:first-child { margin-top: 0; }
.th-ro-head.th-r-loose { color: var(--th-bad); }

.th-ro {
    margin: 0 0 .4rem;
    padding: .55rem .7rem;
    background: var(--th-panel-2);
    border-left: 3px solid var(--th-ok);
    border-radius: 0 var(--th-r-sm) var(--th-r-sm) 0;
}
.th-ro.off { border-left-color: var(--th-warn); }

.th-ro > summary {
    cursor: pointer;
    list-style: none;
    display: flex;
    align-items: baseline;
    gap: .5rem;
}
.th-ro > summary::-webkit-details-marker { display: none; }
.th-ro > summary::after {
    content: "why";
    margin-left: auto;
    flex: none;
    font-family: var(--th-mono);
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-dim);
}
.th-ro[open] > summary::after { content: "close"; }
.th-ro > summary b {
    font-family: var(--th-mono);
    font-size: var(--th-t-sm);
    font-weight: 400;
    line-height: 1.5;
    color: var(--th-text);
    overflow-wrap: anywhere;
}
.th-ro > summary:focus-visible { outline: 2px solid var(--th-accent); outline-offset: 2px; }

.th-ro > span {
    display: block;
    margin-top: .5rem;
    font-size: var(--th-t-sm);
    line-height: 1.55;
    color: var(--th-muted);
}
/* each extra field gets its own label, because "what it did under the wrong
   heading" and "what it invented instead" are different failures and running
   them together as one paragraph loses which is which */
.th-ro .th-ro-more {
    margin-top: .55rem;
    padding-top: .5rem;
    border-top: 1px solid var(--th-line);
}
.th-ro .th-ro-more i {
    display: block;
    margin-bottom: .15rem;
    font-family: var(--th-mono);
    font-style: normal;
    font-size: var(--th-t-2xs);
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
    color: var(--th-dim);
}

/* ====================================================== responsive ======== */

@media (min-width: 560px) {
    .th-t-conf { grid-template-columns: 1fr 1fr; align-items: stretch; }
    .th-t-conf .th-t-file:last-child { margin-left: 0; }
}

@media (min-width: 900px) {
    .th-root { max-width: 980px; }

    /* The start screen stays one column and stops growing - a 980px line of
       body copy is not more readable, only wider. The three acts go across
       instead, which is the one place extra width buys something. */
    .th-start-grid { max-width: 720px; margin: 0 auto; }
    .th-how-track { grid-template-columns: repeat(4, 1fr); }

    /* Act II wants the document permanently open next to the question. You
       should never have to remember what you already wrote.

       The act used to sit in the top two thirds of a desk screen with a third
       of the canvas empty under it, both columns hugging their content. It now
       takes the height it is given: the file is a full-height panel that scrolls
       inside itself with the cost meter pinned to its foot, and the question is
       centred in the column beside it. That is also the right image - the file
       is the tall permanent thing, the question is one passing step.

       .th-f-doc needs min-height:0 or the flex item refuses to shrink below its
       content and the overflow never engages. */
    #th-file.th-screen.on { min-height: calc(100dvh - 56px - 2rem); }
    .th-f-stage { grid-template-columns: 21rem 1fr; gap: 1.1rem; flex: 1; }

    .th-f-side {
        position: sticky;
        top: 1rem;
        max-height: calc(100dvh - 9rem);
        display: flex;
        flex-direction: column;
    }
    .th-f-doc {
        flex: 1 1 auto;
        min-height: 0;
        overflow-y: auto;
        /* a rail, not a page. Set at reading size it wrapped every line into
           three and pushed the four headings apart until the shape of the file
           was no longer visible in one look, which is the only job it has. */
        font-size: var(--th-t-xs);
        line-height: 1.4;
    }
    /* The instruction text is set a step below the headings that file it. The
       headings are the shape of the file and have to survive a glance; the
       lines under them are being counted more than read, and at the same size
       they turned the rail into a wall of prose. */
    .th-f-line { font-size: var(--th-t-2xs); line-height: 1.4; }

    .th-f-main { display: flex; flex-direction: column; }
    /* The question sits at the top of its column, directly under the step
       count, and the ledger follows it. It used to be centred with auto
       margins, which bought a third of a desk screen of empty canvas above
       every question and walked the card up and down as the question changed
       height - the same fault the deck in Act I already had a floor for. */
    #th-f-task { margin-block: 0; }
    #th-pick { margin-block: 1rem 0; }

    /* Act III is the same idea with the stakes reversed: the file is the thing
       being read TO you, so it stays put while the log scrolls. */
    .th-r-stage { grid-template-columns: 19rem 1fr; align-items: start; }
    .th-r-rail { position: sticky; top: 1rem; max-height: calc(100dvh - 8rem); }
    .th-r-term { min-height: 18rem; max-height: calc(100dvh - 14rem); }
}

/* ---- Act IV: the team ---------------------------------------------------- */
.th-team-plate { max-width: 780px; }
.th-team-title {
    margin: .2rem 0 .5rem;
    font-family: var(--th-display);
    font-size: clamp(2rem, 6vw, 4rem);
    line-height: .98;
    letter-spacing: -.04em;
}
.th-team-title span { color: var(--th-accent); }
.th-team-lede {
    max-width: 42rem;
    margin: 0 0 1.25rem;
    color: var(--th-muted);
    line-height: 1.55;
}
.th-team-agents {
    display: grid;
    gap: .65rem;
    margin: 1rem 0;
}
.th-team-agent {
    min-width: 0;
    padding: .9rem;
    border: 1px solid var(--th-line);
    border-top: 3px solid var(--th-warn);
    border-radius: .7rem;
    background: var(--th-panel);
}
.th-team-agent.ok { border-top-color: var(--th-accent); }
/* Three fresh starts land as three, then the verdict. #th-team-go is never
   held back behind them. */
.th-team-agent { animation: th-in .35s var(--th-ease-out) both; }
.th-team-agent:nth-child(1) { animation-delay: .1s; }
.th-team-agent:nth-child(2) { animation-delay: .18s; }
.th-team-agent:nth-child(3) { animation-delay: .26s; }
.th-team-readout { animation: th-in .35s var(--th-ease-out) .38s both; }
.th-team-agent-name {
    margin: 0;
    font-family: var(--th-mono);
    font-size: var(--th-t-xs);
    font-weight: 700;
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
}
.th-team-agent-note { margin: .25rem 0 .7rem; font-size: var(--th-t-xs); color: var(--th-dim); }
.th-team-output { margin: 0; font-weight: 650; line-height: 1.35; }
.th-team-readout {
    margin: 1.2rem 0;
    padding: 1rem;
    border-left: 3px solid var(--th-warn);
    background: var(--th-accent-soft);
}
.th-team-readout p { margin: 0 0 .45rem; line-height: 1.45; }
.th-team-readout p:last-child { margin-bottom: 0; }
.th-team-status {
    font-family: var(--th-mono);
    font-size: var(--th-t-xs);
    font-weight: 700;
    letter-spacing: var(--th-k-label);
    text-transform: uppercase;
}
.th-team-status.ok { color: var(--th-accent); }
.th-team-status.off { color: var(--th-warn); }
.th-team-strong { color: var(--th-muted); font-size: var(--th-t-xs); }

@media (min-width: 700px) {
    .th-team-agents { grid-template-columns: repeat(3, 1fr); }
}

/* A desk monitor was being spent on 980px of content and 500px of canvas either
   side. Only the two acts that are genuinely two columns take the extra width:
   the file gets room to stop wrapping and the log gets room to stop truncating.
   The one-column screens are capped where they already read best, because a
   980px line of body copy is not more readable, only wider. */
@media (min-width: 1240px) {
    .th-root { max-width: 1140px; }
    .th-f-stage { grid-template-columns: 26rem 1fr; gap: 1.5rem; }
    .th-r-stage { grid-template-columns: 22rem 1fr; gap: 1.5rem; }
    #th-start .th-plate,
    #th-end .th-plate { max-width: 780px; margin-inline: auto; }
}

/* ---- the start screen fits the window -----------------------------------
   Four acts, a premise, an offer and a byline ran 234px past the fold of a
   1440x800 desk window: the map you are meant to read before pressing Start
   was the half you could not see, and the seed field was a scroll away.

   Nothing is cut. The map was always an aside, so on a
   desk screen it takes a column of its own beside the premise and the four
   acts pair up 2x2 inside it. Reading order still runs straight down the left
   rail - premise, Start, how long, the two links - which is the one thing the
   single column existed to protect. Below 1024px the screen stays that single
   column, top to bottom, exactly as a phone gets it. */
@media (min-width: 1024px) {
    /* The 900px cap on the wrapper is what the single column needed; the
       plate sets its own width here. */
    #th-start .th-start-grid { max-width: none; }

    #th-start .th-plate {
        display: grid;
        grid-template-columns: minmax(0, 31rem) minmax(0, 1fr);
        column-gap: 1.75rem;
        align-content: start;
        max-width: 1060px;
        padding: 1.5rem 1.5rem 1.35rem;
    }
    /* The rail is every child but the map, in markup order: auto-placement
       fills column 1 row by row because column 2 is already spoken for. */
    #th-start .th-plate > * { grid-column: 1; }
    #th-start .th-how-track {
        grid-column: 2;
        grid-row: 1 / 12;
        align-self: start;
        grid-template-columns: 1fr 1fr;
        margin-top: 0;
    }
    /* The scenes are the one part of the map that can give height back: at
       full column width they are 145px of line art each, and two rows of that
       is the whole overflow. Capped, they are still well above the size their
       own 9px mono labels need. */
    #th-start .th-art { max-width: 150px; margin-inline: auto; }
    #th-start .th-slide { padding: .8rem .95rem .85rem; }
    /* Set at 1.55 the four captions were the tallest thing on the screen. */
    #th-start .th-slide span { line-height: 1.45; }
}


/* Act I stays a single column at every width. It is one question with three
   answers, and widening it only drags the three targets apart. What a desktop
   viewport buys this act is not width but height and reading scale: the column
   used to sit in the top 40% of the window with half a screen of dead canvas
   under the dock, set at phone size and read at desk distance.

   So the stage takes the whole viewport and the column is centred in it, and
   the note is set at the size it earns as the only thing on screen. The deck
   gets a floor height too - a centred column whose card resizes per note would
   walk the dock up and down between notes, which is worse than the empty space
   it fixes. */
@media (min-width: 900px) {
    #th-triage.th-screen.on {
        min-height: calc(100dvh - 56px - 2rem);
        justify-content: center;
        /* safe centring: an overlong note overflows downward only, so the HUD
           is never pushed off the top edge with no way to scroll back to it.
           Browsers that do not know the keyword drop the whole declaration and
           land back on today's top alignment. */
        justify-content: safe center;
    }

    #th-triage .th-main { width: 100%; max-width: 620px; margin: 0 auto; }

    #th-triage .th-deck { min-height: 12rem; }
    #th-triage .th-card { justify-content: center; }
    #th-triage .th-card-kind { font-size: var(--th-t-xs); margin-bottom: .75rem; }
    #th-triage .th-card-text { font-size: var(--th-t-xl); }

    #th-triage .th-ask-head { font-size: var(--th-t-xl); margin-bottom: 1.5rem; }
    #th-triage .th-deck { margin-bottom: 2rem; }

    #th-triage .th-slots { gap: .6rem; }
    #th-triage .th-slot { min-height: 5.25rem; padding: .95rem .6rem; gap: .2rem; }
    #th-triage .th-slot i { font-size: var(--th-t-md); }
    #th-triage .th-slot b { font-size: var(--th-t-sm); }
    #th-triage .th-slot span { font-size: var(--th-t-xs); }
}

/* ================================================== reduced motion ======== */

/* Itemised rather than a blanket kill. Under reduced motion the read-head jumps
   from line to line instead of sweeping, the handoff cut is an instant swap and
   the damage beat flashes once without shaking. Every state stays legible; only
   the tweening goes. */

/* ==================================================== hit targets ======== */

/* Three controls are drawn correctly and are still too small to hit: the sound
   toggle is a 32px disc, the hint pill and the Act III levers about 27px tall.
   The fix is not a bigger button - at 44px the disc stops reading as a quiet
   chrome affordance and starts competing with the note. A centred, unpainted
   pseudo-element carries the touch area instead, so every one of them keeps the
   size the sheet chose and gains the 44px a thumb needs.
   The widest overhang is the sound disc at 6px a side, against a .6rem HUD gap,
   so no overlay ever reaches its neighbour. */
.th-sound, .th-hint-btn, .th-lever { position: relative; }
.th-sound::after,
.th-hint-btn::after,
.th-lever::after {
    content: '';
    position: absolute;
    top: 50%;
    left: 50%;
    width: max(100%, 44px);
    height: max(100%, 44px);
    transform: translate(-50%, -50%);
}

/* ========================================================== focus ======== */

/* One ring, everywhere. The sheet had five :focus-visible rules against about
   forty interactive elements, so a keyboard player got a visible ring on the
   hint button and the patch options and fell off a cliff everywhere else.
   Listed rather than applied to a bare :focus-visible so the shared leaderboard
   and next-game markup mounted inside .th-root keeps its own treatment.

   outline:none paired with a box-shadow ring rather than a plain outline: the
   controls here have radii and colour gutters, and an outline squares off the
   gutter edge where the ring needs to follow it. */
.th-root .th-btn:focus-visible,
.th-root .th-slot:focus-visible,
.th-root .th-pat:focus-visible,
.th-root .th-hint-btn:focus-visible,
.th-root .th-sound:focus-visible,
.th-root .th-hud .th-help:focus-visible,
.th-root .th-hud .th-book:focus-visible,
.th-root .th-skip:focus-visible,
.th-root .th-seed-copy:focus-visible,
.th-root .th-t-opt:focus-visible,
.th-root .th-seed-in:focus-visible {
    outline: none;
    border-color: var(--th-accent);
    box-shadow: 0 0 0 3px var(--th-accent-soft), var(--th-plate);
}

/* An 8%-alpha accent ring is invisible against an accent fill, so the two
   accent-filled buttons take the ground colour instead - the paper the button
   is sitting on, drawn back over its own edge. */
.th-root .th-btn-cert:focus-visible,
.th-root .th-btn-primary:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--th-bg), 0 0 0 4px var(--th-accent-hi);
}

/* Inside the console the accent is nearly invisible on #0B0E14, so the ring
   borrows the colour the run already uses for "look here". */
.th-root.th-dark .th-lever:focus-visible,
.th-root.th-dark .th-p-opt:focus-visible,
.th-root.th-dark .th-btn:focus-visible {
    outline: none;
    border-color: var(--th-c-hot);
    box-shadow: 0 0 0 3px rgba(240, 160, 44, .22);
}



/* ====================================================== how it works ====

   The guide behind the ? in every HUD and behind the first press of Start.
   Three playtesters said the same sentence - "I only understood it once I had
   played" - so this is not more explanation, it is the same explanation with
   one real decision inside it, and it ends by handing the player the note the
   warm-up would have dealt anyway.

   Depth law, unchanged: exactly one raised element. The dialog is raised
   against the act behind it, every beat inside sits flush on it, and the only
   --th-raise within is .th-card - the one thing in here you can touch. A
   scrolling panel of lifted plates would say "act here" four times and mean it
   none of them.

   It lives in the top layer, so the --th-* tokens reach it through its DOM
   parent (#th-root) rather than through paint order. Keep it inside that
   element or every colour in here falls back. */

.th-help {
    flex: none;
    width: 2rem;
    height: 2rem;
    display: grid;
    place-items: center;
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: 999px;
    box-shadow: var(--th-plate);
    color: var(--th-muted);
    font-family: var(--th-display);
    font-size: var(--th-t-sm);
    font-weight: 700;
    line-height: 1;
    cursor: pointer;
}
.th-help:hover { color: var(--th-accent); border-color: var(--th-accent); }
/* On the start screen the same control is an offer rather than a HUD affordance,
   so it takes the shape of the skip link under it. A 2rem disc floating under a
   full-width Start button reads as a fourth piece of chrome on the one screen
   that was cut back to a single decision. */
#th-start .th-help {
    display: block;
    width: auto;
    height: auto;
    margin: 0;
    padding: .35rem .2rem;
    background: none;
    border: 0;
    border-radius: 0;
    box-shadow: none;
    font: inherit;
    font-size: var(--th-t-sm);
    color: var(--th-dim);
    text-decoration: underline;
    text-underline-offset: 3px;
    touch-action: manipulation;
}
#th-start .th-help:hover { color: var(--th-accent); }
/* The book disc sat beside this one in the console as a light chip, because
   only the ? had a dark rule. They are one pair; they get one rule. */
.th-root.th-dark .th-help,
.th-root.th-dark .th-book {
    background: var(--th-c-panel);
    border-color: var(--th-c-line);
    box-shadow: none;
    color: var(--th-c-dim);
}
.th-root.th-dark .th-help:hover,
.th-root.th-dark .th-book:hover { color: var(--th-c-hot); border-color: var(--th-c-hot); }

/* Mobile is the design here too: a bottom sheet, because a scroll narrative
   inside a small centred box on a phone is the worst of both. It stops 12px
   short of the top so the page behind still shows as a sliver - you are over
   the game, not somewhere else - and carries a grabber because it can really
   be dragged (thSheetDrag). The centred card only arrives once there is room
   for it to read as an overlay rather than as a page. */
.th-guide {
    width: 100%;
    max-width: 100%;
    height: calc(100% - 12px - env(safe-area-inset-top, 0px));
    max-height: calc(100% - 12px - env(safe-area-inset-top, 0px));
    margin: auto 0 0;
    padding: 0;
    border: 0;
    border-radius: var(--th-r-plate) var(--th-r-plate) 0 0;
    background: var(--th-bg);
    color: var(--th-text);
    font-family: var(--th-ui);
    display: flex;
    flex-direction: column;
    overflow: hidden;
    /* the head, the tabs and the foot all read this one scroll position */
    timeline-scope: --th-gs;
}
.th-guide:not([open]) { display: none; }
/* Dim to focus, and let the act behind soften a little: this is a modal
   task, so the game recedes rather than competing through the scrim. */
.th-guide::backdrop {
    background: rgba(23, 21, 18, .45);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
}

.th-g-head {
    position: relative;
    flex: none;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: .6rem;
    padding: 1.15rem 1.1rem .7rem;
    background: var(--th-panel);
    /* the handle: a drag here is the sheet's, never the page's */
    touch-action: none;
}
/* The grabber. Decoration that is true: this head really does drag. */
.th-g-head::before {
    content: '';
    position: absolute;
    top: .4rem;
    left: 50%;
    width: 36px;
    height: 5px;
    margin-left: -18px;
    border-radius: 999px;
    background: var(--th-line-2);
}

/* Scroll edge, not a divider. A hairline under the head that is always there
   says "boundary" even when nothing is under it; this one only fades in once
   content has actually scrolled beneath the head, and the foot's only shows
   while there is still more below. Browsers without scroll timelines keep
   plain hairlines, which is what this was. */
@supports (animation-timeline: scroll()) {
    .th-g-scroll { scroll-timeline: --th-gs block; }
    .th-g-head {
        animation: th-edge-in linear both;
        animation-timeline: --th-gs;
        animation-range: 0 28px;
    }
    .th-g-foot {
        animation: th-edge-out linear both;
        animation-timeline: --th-gs;
        animation-range: calc(100% - 28px) 100%;
    }
}
@supports not (animation-timeline: scroll()) {
    .th-g-head { box-shadow: 0 1px 0 var(--th-line); }
    .th-g-foot { box-shadow: 0 -1px 0 var(--th-line); }
}
@keyframes th-edge-in {
    from { box-shadow: 0 1px 0 transparent, 0 6px 14px -12px transparent; }
    to   { box-shadow: 0 1px 0 var(--th-line), 0 6px 14px -12px rgba(40, 34, 24, .35); }
}
@keyframes th-edge-out {
    from { box-shadow: 0 -1px 0 var(--th-line), 0 -6px 14px -12px rgba(40, 34, 24, .35); }
    to   { box-shadow: 0 -1px 0 transparent, 0 -6px 14px -12px transparent; }
}

/* A title, not a label. It was a teal uppercase eyebrow over nothing, which
   is how a section inside a page is headed, not how a sheet is named. Weight
   and ink carry it; tracking tightens as it would at any display size. */
.th-g-eyebrow {
    margin: 0;
    font-family: var(--th-display);
    font-size: var(--th-t-lg);
    letter-spacing: var(--th-k-tight);
    line-height: 1.2;
    color: var(--th-text);
    font-weight: 700;
}

/* Where am I in four pages. Stretched pill for the current one, the way a
   page control marks position without numbers. The width snaps rather than
   tweening: the swipe is already the motion, and width is a layout property. */
.th-g-dots { display: flex; gap: 5px; }
.th-g-dots i {
    width: 6px;
    height: 6px;
    border-radius: 999px;
    background: var(--th-line-2);
    transition: background-color .2s ease;
}
.th-g-dots i.on { width: 16px; background: var(--th-accent); }

/* The close chip: a filled circle, no ring, the cross drawn in the icon
   family. Quieter than the discs in the HUD because it is always there. */
.th-g-x {
    position: relative;
    margin-left: auto;
    flex: none;
    width: 1.875rem;
    height: 1.875rem;
    display: grid;
    place-items: center;
    padding: 0;
    background: var(--th-fill);
    border: 0;
    border-radius: 999px;
    color: var(--th-muted);
    cursor: pointer;
}
.th-g-x .th-ico { width: .8rem; height: .8rem; stroke-width: 2.4; }
.th-g-x:hover { background: var(--th-line); color: var(--th-text); }
.th-g-x::after {
    content: '';
    position: absolute;
    inset: 50% auto auto 50%;
    width: 44px;
    height: 44px;
    transform: translate(-50%, -50%);
}
.th-root .th-g-x:focus-visible {
    outline: none;
    box-shadow: 0 0 0 2px var(--th-panel), 0 0 0 4px var(--th-accent);
}

.th-g-scroll {
    flex: 1 1 auto;
    min-height: 0;
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    padding: 0 1.1rem;
}

/* Flush, always. The hairline is the seam between beats; a border box around
   each one would read as four cards to get through. */
.th-g-beat {
    padding: 1.4rem 0;
    border-bottom: 1px solid var(--th-line);
}
.th-g-beat:last-child { border-bottom: 0; }

.th-g-h {
    margin: 0 0 .7rem;
    font-family: var(--th-display);
    font-size: var(--th-t-lg);
    letter-spacing: var(--th-k-tight);
    line-height: 1.15;
    font-weight: 700;
}

/* .th-slide carries the animation gate and nothing else in here - the start
   screen's panel padding and ground would put a second surface inside a beat
   that is meant to be flush. */
.th-g-scene {
    position: relative;
    padding: 0;
    /* the frame is the surface, so the drawing never shows a seam where its
       own box ends short of the frame's */
    background: var(--th-panel-2);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
    overflow: hidden;
    margin: 0 0 .85rem;
}
/* Capped, because the >=900px .th-art rule lifts its cap for the start
   screen's three-up grid and would otherwise stretch each drawing across the
   whole card - four of them at ~310px tall was most of why this felt huge. */
.th-g-scene .th-art { max-width: 220px; margin: 0 auto; background: none; }

/* Glass over the drawing: the one control in here that floats on content, so
   it is the one place a translucent material means something. */
.th-g-replay {
    position: absolute;
    right: .45rem;
    bottom: .45rem;
    width: 1.875rem;
    height: 1.875rem;
    display: grid;
    place-items: center;
    padding: 0;
    background: rgba(252, 250, 246, .7);
    -webkit-backdrop-filter: blur(10px) saturate(160%);
    backdrop-filter: blur(10px) saturate(160%);
    border: 1px solid rgba(255, 255, 255, .6);
    border-radius: 999px;
    box-shadow: 0 1px 2px rgba(40, 34, 24, .12);
    color: var(--th-muted);
    cursor: pointer;
}
.th-g-replay .th-ico { width: .85rem; height: .85rem; stroke-width: 2.1; }
.th-g-replay:hover { color: var(--th-accent); }
/* spun a quarter the way it points, so the press says what it will do */
.th-g-replay:active .th-ico { transform: rotate(-90deg); transition: transform .25s var(--th-ease-out); }

.th-g-l {
    margin: 0 0 .35rem;
    color: var(--th-muted);
    font-size: var(--th-t-md);
    line-height: 1.55;
}
.th-g-l:last-of-type { margin-bottom: 0; }

/* The one interactive beat. The deck keeps .th-last in markup so the pile
   behind the card never draws - there is no deck here, there is one note. */
.th-g-deck { margin: 1rem 0 .2rem; }
/* The real dock is sticky to the bottom of an act. Inside a scrolling dialog
   that would pin it over the beats below it. */
.th-g-slots { position: static; background: none; padding: .6rem 0 0; }

.th-g-out {
    margin: .8rem 0 0;
    padding: .7rem .85rem;
    border-left: 3px solid var(--th-accent);
    border-radius: 0 var(--th-r-sm) var(--th-r-sm) 0;
    background: var(--th-accent-soft);
    color: var(--th-text);
    font-size: var(--th-t-sm);
    line-height: 1.55;
}
/* Same arrival as its twin, .th-hint-out span. Unhiding restarts it. */
.th-g-out { animation: th-cut-in .22s var(--th-ease-out) both; }
.th-g-out b { font-family: var(--th-display); }
/* Warm means "something went wrong" everywhere else in this sheet, and a wrong
   call in an unscored guide is the mildest version of that there is. */
.th-g-out-bad { border-left-color: var(--th-warn); background: var(--th-warn-soft); }
.th-g-out[hidden] { display: none; }

.th-g-foot {
    position: relative;   /* so its scroll edge paints over the text above it */
    flex: none;
    display: grid;
    gap: .15rem;
    padding: .85rem 1.1rem calc(.85rem + env(safe-area-inset-bottom, 0px));
    background: var(--th-panel);
}
.th-g-foot .th-book-link { margin-top: .35rem; }

/* ---- the guide, paged ----
   One beat per page. Plain scroll-snap does the paging, so a swipe, a
   trackpad, Shift+wheel and Next (thGuideNext) all move the same scroller and
   the IntersectionObserver in thGuideWatch hears every one of them. All of it
   is scoped to #th-guide: the glossary shares every .th-g-* class here and is
   a genuine long read, so it stays a vertical scroll.

   The flex row is as tall as its tallest beat, so the card holds one height
   across all four pages instead of jumping as they change. */
#th-guide { height: auto; }
#th-guide .th-g-scroll {
    display: flex;
    padding: 0;
    overflow-x: auto;
    overflow-y: hidden;
    scroll-snap-type: x mandatory;
    overscroll-behavior-x: contain;
    scrollbar-width: none;
}
#th-guide .th-g-scroll::-webkit-scrollbar { display: none; }
#th-guide .th-g-beat {
    flex: 0 0 100%;
    min-width: 0;
    padding: .4rem 1.1rem 1.1rem;
    border-bottom: 0;
    overflow-y: auto;
    scroll-snap-align: start;
    scroll-snap-stop: always;
}
#th-guide .th-g-beat .th-book-link { margin: .9rem 0 0; }
/* The edges tracked a vertical scroll that no longer exists in here. */
#th-guide .th-g-head,
#th-guide .th-g-foot { animation: none; box-shadow: none; }
#th-guide .th-g-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 1rem;
}
#th-guide #th-g-go { width: auto; min-width: 8.5rem; padding: .8rem 1.4rem; }

/* ---- the segmented control ----
   Two tabs that were two folder tabs with an accent box on the live one. It is
   a two-way switch, so it is drawn as one: a recessed track and a single raised
   thumb that slides under whichever side is on. The thumb is a pseudo-element
   on the tablist and data-tab moves it (thSetGlossaryTab), so the labels never
   move and the selection is one object travelling rather than two boxes
   swapping paint. */
.th-gl-tabs {
    position: relative;
    flex: 1 1 100%;
    display: grid;
    grid-template-columns: 1fr 1fr;
    padding: 2px;
    background: var(--th-fill);
    border-radius: var(--th-r-sm);
    box-shadow: var(--th-inset);
}
.th-gl-tabs::before {
    content: '';
    position: absolute;
    top: 2px;
    bottom: 2px;
    left: 2px;
    width: calc(50% - 2px);
    background: var(--th-panel);
    border-radius: max(2px, calc(var(--th-r-sm) - 2px));
    box-shadow: var(--th-plate), 0 0 0 .5px var(--th-line-2);
    transition: translate .32s var(--th-ease-out);
}
.th-gl-tabs[data-tab="resources"]::before { translate: 100% 0; }
.th-gl-tab {
    position: relative;
    padding: .45rem .75rem;
    background: none;
    border: 0;
    color: var(--th-muted);
    font-family: var(--th-ui);
    font-size: var(--th-t-sm);
    font-weight: 600;
    letter-spacing: var(--th-k-none);
    cursor: pointer;
    transition: color .2s ease;
}
.th-gl-tab.on { color: var(--th-text); }
.th-root .th-gl-tab:focus-visible { outline: 2px solid var(--th-accent); outline-offset: 1px; border-radius: var(--th-r-xs); }

/* The panels sit side by side in the mind - glossary left, resources right -
   so each arrives from its own side. */
.th-gl-panel { display: none; }
.th-gl-panel.on { display: block; }
#th-gl-panel.on { animation: th-gl-from-l .28s var(--th-ease-out) both; }
#th-res-panel.on { animation: th-gl-from-r .28s var(--th-ease-out) both; }
@keyframes th-gl-from-l { from { opacity: 0; transform: translateX(-10px); } }
@keyframes th-gl-from-r { from { opacity: 0; transform: translateX(10px); } }

@media (min-width: 720px) {
    .th-guide {
        width: min(560px, calc(100vw - 3rem));
        height: auto;
        max-height: min(720px, calc(100vh - 4rem));
        margin: auto;
        border: 1px solid var(--th-line);
        border-radius: var(--th-r-plate);
        box-shadow: var(--th-raise);
    }
    /* no drag on the card, so no grabber and no reason to reserve its room */
    .th-g-head { padding-top: 1rem; touch-action: auto; }
    .th-g-head::before { display: none; }
    /* One beat on a page needs a card, not a document. 560px left each line
       of guide copy running the full width of the act behind it. */
    #th-guide { width: min(440px, calc(100vw - 3rem)); }
}

/* Arrival. The phone sheet rises from the thumb edge on the iOS drawer curve
   and leaves by it; the centred card scales up from just under full size, in
   the middle of the screen, the way a modal does. It used to grow out of the
   ? that opened it with a lifting blur - from a HUD corner that read as a
   slide rather than a surface, and blurring the whole card over a blurred
   backdrop repainted both every frame. Critically damped throughout: nothing
   overshoots, because nothing was thrown. allow-discrete holds the
   :not([open]) display:none above until the exit has played. Browsers without
   overlay transitions (Firefox) still get the entrance and close instantly. */
.th-guide {
    transition: opacity .2s var(--th-ease-out), transform .32s cubic-bezier(.32, .72, 0, 1),
                overlay .32s allow-discrete, display .32s allow-discrete;
}
.th-guide:not([open]) { opacity: 0; transform: translateY(100%); transition-duration: .2s; }
@starting-style { .th-guide[open] { opacity: 0; transform: translateY(100%); } }
.th-guide::backdrop {
    opacity: 0;
    transition: opacity .2s ease, overlay .2s allow-discrete, display .2s allow-discrete;
}
.th-guide[open]::backdrop { opacity: 1; }
@starting-style { .th-guide[open]::backdrop { opacity: 0; } }
@media (min-width: 720px) {
    .th-guide { transition: opacity .2s var(--th-ease-out), transform .26s var(--th-ease-out),
                            overlay .26s allow-discrete, display .26s allow-discrete; }
    .th-guide:not([open]) { transform: scale(.96); transition-duration: .15s; }
    @starting-style { .th-guide[open] { transform: scale(.96); } }
}
/* the guide still fades, it just does not travel or grow */
@media (prefers-reduced-motion: reduce) {
    .th-guide, .th-guide:not([open]) { transform: none; transition-duration: .15s; }
    @starting-style { .th-guide[open] { transform: none; } }
    .th-gl-tabs::before, .th-g-dots i { transition: none; }
    #th-gl-panel.on, #th-res-panel.on { animation: none; }
}
/* Frosted surfaces go solid for anyone who has asked for less transparency. */
@media (prefers-reduced-transparency: reduce) {
    .th-guide::backdrop { -webkit-backdrop-filter: none; backdrop-filter: none; background: rgba(23, 21, 18, .6); }
    .th-g-replay { -webkit-backdrop-filter: none; backdrop-filter: none; background: var(--th-panel); border-color: var(--th-line); }
}
@media (prefers-contrast: more) {
    .th-guide { border: 2px solid var(--th-text); }
    .th-gl-tabs::before { box-shadow: 0 0 0 1.5px var(--th-text); }
    .th-g-x { border: 1px solid var(--th-text); }
}

/* ---- 0. the premise: yesterday empties into today ----------------------
   The one scene the start screen did not already have. Every other beat in
   the guide reuses .th-a2 / .th-a3 / .th-a4 whole. */
.th-a1 .a1-win { fill: var(--th-panel); stroke: var(--th-line-2); stroke-width: 1.5; }
.th-a1 .a1-name { text-anchor: start; }
.th-a1 .a1-l { fill: var(--th-dim); transform-box: fill-box; transform-origin: left center; }
.th-a1 .a1-cur { fill: var(--th-accent); }
/* Held just long enough to read yesterday as full. On a page of its own the
   old 2.2s of stillness before anything moved read as a dead page. */
.th-slide.on .th-a1 .a1-l { animation: a1-forget 3s ease-in-out both; }
.th-slide.on .th-a1 .a1-l2 { animation-delay: .2s; }
.th-slide.on .th-a1 .a1-l3 { animation-delay: .4s; }
@keyframes a1-forget {
    0%, 25% { opacity: 1; transform: none; }
    60%, 100% { opacity: 0; transform: translateY(-7px); }
}

@media (prefers-reduced-motion: reduce) {
    /* the start screen arrives assembled rather than assembling */
    #th-start .th-kicker,
    #th-start .th-title,
    #th-start .th-lede,
    #th-start .th-def,
    #th-start .th-btn-primary,
    #th-start .th-lengths,
    #th-start .th-start-links,
    #th-start .th-slide,
    .th-screen.on,
    .th-card.th-deal,
    .th-f-line,
    .th-f-task.th-task-in,
    .th-pick,
    .th-hint,
    .th-r-log,
    .th-r-patch.th-patch-up,
    .th-t-note,
    .th-cutting .th-cut-old,
    .th-cutting .th-cut-new,
    .th-cutting .th-cut-note,
    .th-team-agent,
    .th-team-readout,
    .th-g-out,
    .th-hint-out,
    .th-hint-out span,
    .th-hint-btn i.th-tick {
        animation: none !important;
    }

    /* the start-screen scenes land on their final frame instantly rather than
       running the one-shot pass */
    .th-slide.on .th-a2 .a2-card,
    .th-slide.on .th-a2 .a2-t,
    .th-slide.on .th-a3 .a3-clip,
    .th-slide.on .th-a3 .a3-meter,
    .th-slide.on .th-a3 .a3-note,
    .th-slide.on .th-a3 .a3-drop,
    .th-slide.on .th-a3 .a3-key,
    .th-slide.on .th-a4 .a4-l,
    .th-slide.on .th-a1 .a1-l,
    .th-slide.on .th-team-mini i {
        animation: none !important;
    }

    /* The premise scene is the one whose final frame is the empty half, so it
       cannot simply be frozen: without this the lines that are meant to have
       gone stay on the page and the beat says the opposite of its headline. */
    .th-slide.on .th-a1 .a1-l { opacity: 0; }

    /* the note arrives complete rather than at reading pace. thReveal() checks
       the same query and never writes the spans, so this only catches a class
       that outlived a setting change mid-session. */
    .th-tw > span { animation: none !important; opacity: 1; transform: none; }

    /* the meter still arrives on the first trim, it just does not drop in */
    .th-f-meter.th-m-in { animation: none !important; }

    /* The progress rail and the file stub are both content: how much is left,
       and how big the file has got. They keep their values and lose the slide,
       so they still say the same thing and nothing on the page moves. */
    .th-spine li::after,
    .th-stub { transition: none !important; }

    /* The demo is content, not decoration, so it does not get switched off -
       it lands on the frame the loop was building towards. Both lines, both
       marks, both consequences, all of it legible, none of it moving. */
    .th-d-was, .th-d-now,
    .th-d-was em, .th-d-now em {
        animation: none !important;
        opacity: 1;
        transform: none;
    }
    .th-d-was { background: var(--th-bad-soft); }
    .th-d-now { background: var(--th-ok-soft); }
    .th-d-row::after { animation: none !important; opacity: 0; }

    /* the rail still says which line is being read, it just does not slide */
    .th-r-line.read { animation: none !important; }

    /* the meter still counts and the bar still fills, neither one animates -
       the figure is content, so it must not be switched off, only stilled */
    .th-b-tick { animation: none !important; }
    .th-r-b-bar i { transition: none !important; }

    /* the flash still lands, it just holds instead of pulsing - it is the only
       feedback a right answer gets now, so it must not vanish under this query */
    .th-deck.th-flash-ok::after,
    .th-deck.th-flash-part::after,
    .th-deck.th-flash-bad::after { animation: none !important; opacity: .16; }

    /* the principle card still arrives, it just does not slide in */
    .th-pr-plate { animation: none !important; }

    /* the cut still shows the original struck through, it just does not collapse */
    .th-cutting .th-cut-old { opacity: .55; max-height: none; }

    /* the room still goes dark, it just does not drain */
    .th-root.th-handing { filter: none !important; transition: none !important; }
    .th-root.th-dark { transition: none !important; }

    /* the damage still reads as damage, it just does not shake */
    #th-run.th-damage .th-r-console { animation: none !important; box-shadow: 0 0 0 2px var(--th-c-bad); }
    .th-r-log.th-flash { animation: none !important; }

    /* the score lands on its number rather than counting to it - thCountUp
       checks the same query and skips the ramp */
    /* The bar still shows its fill and the card still shows its content - the
       fill just does not grow and the card does not rise. Both carry a stagger
       delay, and a delay left on a killed animation is how a "disabled" reveal
       ends up holding content invisible for a third of a second. */
    .th-tier, .th-burst i, .th-a-bar u, .th-sec {
        animation: none !important;
        animation-delay: 0ms !important;
        opacity: 1;
        transform: none;
    }

    .th-f-bar i { transition: none !important; }
}

/* a lever that has nothing left to spend refuses out loud, on the dark surface */
.th-lever.th-reject { animation: th-shake .38s ease; }

/* "read that again" on the light surface. The dark console has its own. */
.th-flash { animation: th-flash-light .7s ease both; }
@keyframes th-flash-light {
    0%, 100% { opacity: 1; }
    35%      { opacity: .35; }
}

/* ---- grid overflow guards ----
   A grid child defaults to min-width:auto, so a long unbroken line of mono text
   in the document, the rail or the log pushes the whole column wider than the
   viewport instead of wrapping. Both acts are grid + mono, so both need this.
   Caught at 320px, where a staging URL is wider than the screen. */

.th-f-side, .th-f-main, .th-r-rail, .th-r-console { min-width: 0; }

.th-f-line, .th-f-empty, .th-r-line, .th-t-line, .th-t-file b, .th-p-opt {
    overflow-wrap: anywhere;
}

/* a note costs n lines, so it is drawn as the n lines the meter charges for */
.th-f-line, .th-t-line { white-space: pre-wrap; }

/* Act II's answer note is dismissed, not timed out - same button as Act I's
   verdict card. .th-f-task is block flow, so the sheet's align-self is inert
   here and the margin does the right-align instead. */
.th-f-task .th-v-dismiss { display: block; margin: .9rem 0 0 auto; }

/* ---- glossary & resources ----------------------------------------------
   #th-glossary carries .th-guide, so the shell - the sheet on mobile, the
   centred card at 720px, the head, the scroll body, the foot, the backdrop -
   is inherited whole and nothing below re-declares it. Only the two list
   shapes are new.

   The openers are .th-book. In a HUD it is a disc matching .th-help beside it;
   on the end screen and in the guide's foot it is a line of text, because
   there it is an offer rather than a rescue. */

.th-book {
    flex: none;
    width: 2rem;
    height: 2rem;
    display: grid;
    place-items: center;
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: 999px;
    box-shadow: var(--th-plate);
    color: var(--th-muted);
    cursor: pointer;
}
.th-book:hover { color: var(--th-accent); border-color: var(--th-accent); }
.th-help .th-ico, .th-book .th-ico, .th-sound .th-ico { width: 1.05rem; height: 1.05rem; }
.th-help, .th-book, .th-sound { padding: 0; }

/* Same control, sentence shape. Matches .th-help on the start screen, which
   made the same move for the same reason. */
.th-book-link {
    width: fit-content;
    height: auto;
    display: block;
    margin: .6rem auto 0;
    padding: .25rem .1rem;
    background: none;
    border: 0;
    border-bottom: 1px solid var(--th-accent-line);
    border-radius: 0;
    box-shadow: none;
    color: var(--th-accent);
    font-family: var(--th-ui);
    font-size: var(--th-t-sm);
}
.th-book-link:hover { border-bottom-color: var(--th-accent); }

/* ---- the grouped lists ----
   Both tabs are the same shape: a named group, then one flush plate holding its
   rows, split by hairlines inset from the leading edge. It is a reference list,
   so it is drawn as one - a stack of loose paragraphs under uppercase teal
   labels read as an article. The plate is --th-plate, flush: the dialog is the
   raised thing and nothing inside it competes.

   A <dl> for the terms because that is what it is. The term sits on its own
   line above the definition rather than beside it: at 320px a two-column list
   gives the definition about 20 characters and every entry wraps to four. */
.th-gl-panel { padding: 1.1rem 0 1.4rem; }
.th-gl-cat + .th-gl-cat { margin-top: 1.4rem; }

.th-gl-h {
    display: flex;
    align-items: center;
    gap: .55rem;
    margin: 0 0 .55rem .15rem;
    font-family: var(--th-ui);
    font-size: var(--th-t-sm);
    font-weight: 600;
    letter-spacing: var(--th-k-none);
    color: var(--th-muted);
}
/* the category glyph on a tinted tile, the way a settings row carries its
   icon - in the accent ink, never a warm one */
.th-gl-ico {
    display: grid;
    place-items: center;
    width: 1.5rem;
    height: 1.5rem;
    flex: none;
    border-radius: 7px;
    background: var(--th-accent-soft);
    color: var(--th-accent);
}
.th-gl-ico .th-ico { width: .9rem; height: .9rem; }

.th-gl-list {
    margin: 0;
    padding: 0;
    list-style: none;
    background: var(--th-panel);
    border: 1px solid var(--th-line);
    border-radius: var(--th-r-ctl);
    box-shadow: var(--th-plate);
    overflow: hidden;
}
.th-gl-row { padding: .75rem .95rem .8rem; }
/* The separator starts where the text does, so the rows read as one list
   rather than as boxes stacked on each other. */
.th-gl-row + .th-gl-row { margin-left: .95rem; padding-left: 0; border-top: 1px solid var(--th-line); }

.th-gl-term {
    font-size: var(--th-t-md);
    font-weight: 600;
    letter-spacing: var(--th-k-none);
    color: var(--th-text);
}
.th-gl-def {
    margin: .2rem 0 0;
    font-size: var(--th-t-sm);
    line-height: 1.5;
    color: var(--th-muted);
}

/* ---- the links ----
   Each row is the whole link, with a trailing arrow saying it leaves the
   game. It answers the press itself - a fill on :active - because a thumb on
   a list row cannot see a hover. */
.th-gl-links li + li { margin-left: .95rem; border-top: 1px solid var(--th-line); }
.th-gl-links li + li .th-gl-link { padding-left: 0; }

.th-gl-link {
    display: flex;
    align-items: center;
    gap: .75rem;
    padding: .75rem .95rem;
    color: var(--th-text);
    /* app.css makes every <a> bold and underlines it on hover; the row sets
       its own weights and says "hover" with the title's ink instead */
    font-weight: 400;
    text-decoration: none;
    transition: background-color .12s ease;
    -webkit-tap-highlight-color: transparent;
}
.th-gl-link span { flex: 1; min-width: 0; }
.th-gl-link b {
    display: block;
    font-size: var(--th-t-sm);
    font-weight: 600;
    line-height: 1.4;
}
.th-gl-link em {
    display: block;
    margin-top: .15rem;
    font-size: var(--th-t-xs);
    font-style: normal;
    line-height: 1.45;
    color: var(--th-dim);
}
.th-gl-out { width: .9rem; height: .9rem; color: var(--th-accent); stroke-width: 2; }
@media (hover: hover) {
    .th-gl-link:hover b { color: var(--th-accent); }
    .th-gl-link:hover .th-gl-out { transform: translate(1px, -1px); }
}
.th-gl-out { transition: transform .16s var(--th-ease-out); }
.th-gl-link:hover { text-decoration: none; }
.th-gl-link:active { background: var(--th-panel-2); }
.th-root .th-gl-link:focus-visible { outline: 2px solid var(--th-accent); outline-offset: -2px; }


/* ---- supporter badge ----------------------------------------------------
   app.css paints .tt-supporter-item on a white chip with a hardcoded
   periwinkle edge, which is the shell's colour, not this game's. Re-point the
   two visible values at tokens so the badge follows whatever skin is mounted -
   including none, where they resolve to the paper accent.

   The white chip is load-bearing, not decoration: ShiftSync ship one colourway
   (navy wordmark, green orbit) and the header badge is still on screen when
   Act III drains the desk to near-black. The chip is what keeps the mark
   readable there without inventing a knockout of somebody else's logo. */

.th-root .tt-supporter-item { border-color: var(--th-accent); }
.th-root .tt-supporter-tagline { color: var(--th-accent); }
.th-root.th-dark .tt-supporter-badge { opacity: .85; }
