/* HSCZ palette, solved rather than sampled.
   The previous palette came off a Stitch redesign by eye, and when it was
   finally measured it failed: every subject colour landed between 3.96:1 and
   4.36:1 on white, under the 4.5:1 floor for text, and the two maths shades -
   deliberately two tones of one hue - came out 2.2 apart in Lab under
   simulated protanopia, which is to say identical. Two shades of one hue is a
   pleasant idea and an accessibility bug.

   What replaced it. Five hues, separated by LIGHTNESS as much as by hue,
   because lightness is the one channel every kind of colour blindness keeps.
   Worst pair is now 20.4 apart under protanopia, deuteranopia or tritanopia,
   in both themes. Every pairing below was checked with a script, not an eye:
   >=4.5:1 anywhere a colour carries text, >=3:1 for a colour used as a shape.

   The page ground is #ebe9f4 and not something paler. At #f7f6fa it was
   1.08:1 against a white card, which is a difference you can measure and not
   one you can see - the card had no edge. #ebe9f4 is 1.20:1, and it puts the
   three surfaces in the order they should be: page darkest, inset (--panel-2)
   between, card lightest. Muted text still clears 4.5:1 on it (4.71).

   Two rules this file holds to:
     1. Colour is never the only signal. The subject name always sits beside
        its colour; nothing is encoded in hue alone.
     2. Semantic colour comes from a token, never a literal. A literal is how
        a surface stays light while the text on it follows the theme - see
        .badge and .btn.danger below, which both did exactly that. */
:root{
  --bg:#ebe9f4; --panel:#fff; --panel-2:#f1f0f7; --chrome:#fdfdff;
  --ink:#1e1f28; --muted:#63667a;
  --line:#e2e0ec; --accent:#3f4a85; --accent-ink:#fff; --accent-soft:#e6e9f7;
  --good:#346e4b; --good-soft:#e2efe7; --bad:#9a4040; --bad-soft:#f9e7e7;
  --warn:#8a5e22; --warn-soft:#f7eddc;
  /* Text that sits ON a semantic colour. Was #fff in two places, which is
     white on a pale rose in dark mode. */
  --on-solid:#fff;
  /* The highlight that sweeps a skeleton row. A token because a white at 55%
     is a highlight on a light surface and a glare on a dark one. */
  --shimmer:#ffffff8c;
  --mono:ui-monospace,SFMono-Regular,Menlo,monospace;
  --display:"Fraunces","Iowan Old Style",Georgia,serif;
  --ui:"Plus Jakarta Sans",ui-sans-serif,system-ui,-apple-system,"Segoe UI",sans-serif;
  --radius:16px;
}
/* Dark tokens twice, deliberately.
   The media query covers "System"; the attribute rule covers someone who
   picked Dark in Settings on a phone set to light. With only the media query
   the Dark button silently did nothing, which is worse than not offering it. */
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
  --bg:#13141a; --panel:#1b1d25; --panel-2:#232631; --chrome:#171921;
  --ink:#e8e9f0; --muted:#a0a6b4;
  --line:#2f323e; --accent:#a9b8ee; --accent-ink:#141726; --accent-soft:#272d41;
  --good:#86c79f; --good-soft:#1b2b23; --bad:#e0a0a0; --bad-soft:#2e1e1e;
  --warn:#dbab72; --warn-soft:#2e2519;
  --on-solid:#141726; --shimmer:#ffffff12;
}}
:root[data-theme="dark"]{
  --bg:#13141a; --panel:#1b1d25; --panel-2:#232631; --chrome:#171921;
  --ink:#e8e9f0; --muted:#a0a6b4;
  --line:#2f323e; --accent:#a9b8ee; --accent-ink:#141726; --accent-soft:#272d41;
  --good:#86c79f; --good-soft:#1b2b23; --bad:#e0a0a0; --bad-soft:#2e1e1e;
  --warn:#dbab72; --warn-soft:#2e2519;
  --on-solid:#141726; --shimmer:#ffffff12;
}
*{box-sizing:border-box}
/* A class that sets `display` beats the hidden attribute's UA rule, so
   .topbar{display:flex} and .backbtn{display:grid} kept showing while hidden -
   the back arrow appeared on Home, which is a tab root with nowhere to go
   back to. One rule rather than a per-element fix. */
[hidden]{display:none!important}
html,body{height:100%}
/* Vendored, like KaTeX, and for the same reason: web/ is served byte for byte
   and the app has to work offline. Two variable latin-subset files, 64 KB the
   pair, both SIL Open Font License (LICENSE files sit beside them).
   font-display:swap so a slow font never blanks the question you are reading. */
@font-face{font-family:"Fraunces";font-style:normal;font-weight:400 700;
  font-display:swap;src:url("./vendor/fonts/fraunces-latin-var.woff2") format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,
  U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}
@font-face{font-family:"Plus Jakarta Sans";font-style:normal;font-weight:400 700;
  font-display:swap;src:url("./vendor/fonts/plus-jakarta-sans-latin-var.woff2") format("woff2");
  unicode-range:U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,
  U+2000-206F,U+2074,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD}

body{margin:0;background:var(--bg);color:var(--ink);
  font:16px/1.55 var(--ui);
  -webkit-font-smoothing:antialiased}
/* The serif carries the greeting, the headings and every figure worth reading.
   Numbers are tabular so a streak of 9 and a streak of 12 do not shuffle the
   layout under them. */
h1,h2,h3,h4,.title,.lvlname,.bigscore,.score,.stat b,.num,.hello{font-family:var(--display)}
.stat b,.bigscore,.score,.num,.lvlxp,.timer,.xp,.rank{font-variant-numeric:tabular-nums}
button{font:inherit}
/* Safe areas, both ends.
   TOP: on a notched phone the web view starts behind the status bar, so
   without this the screen title renders underneath the clock. The inset is
   ADDED to the normal padding rather than max()'d with it, or the title ends
   up flush against the clock instead of below it.
   BOTTOM: clears the tab bar plus the home indicator. */
.app{max-width:720px;margin:0 auto;min-height:100%;
  padding-left:16px;padding-right:16px;
  padding-top:calc(16px + env(safe-area-inset-top));
  padding-bottom:calc(84px + env(safe-area-inset-bottom))}

/* --- chrome --------------------------------------------------------------- */
/* Centred title with the back arrow floated over it, which is what the
   redesign does and what every native app does. Centring by flex would push
   the title off-centre whenever the arrow is present, so the arrow is taken
   out of flow instead and the title stays put whether or not it is there. */
.topbar{position:relative;display:flex;align-items:center;justify-content:center;
  margin-bottom:18px;min-height:40px;text-align:center}
.titles{display:flex;flex-direction:column;min-width:0;
  padding:0 44px;align-items:center}
.title{font-weight:700;letter-spacing:-.012em;font-size:1.1rem;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
.subtitle{color:var(--muted);font-size:.8rem;
  overflow:hidden;text-overflow:ellipsis;white-space:nowrap;max-width:100%}
/* Drawn, not typed. This was the character U+2192 at 1.4rem, and a text arrow
   is only ever as heavy as the font's own stroke - thin in the old system
   stack and thinner still in Plus Jakarta Sans, which is what made it look
   like a hairline next to a 600-weight title. An SVG sets its own weight, and
   2.1 matches the tab icons at this size. */
.backbtn{position:absolute;left:0;top:50%;transform:translateY(-50%);
  background:none;border:none;color:var(--accent);
  width:38px;height:38px;cursor:pointer;
  display:grid;place-items:center}
.backbtn svg{width:22px;height:22px;fill:none;stroke:currentColor;
  stroke-width:2.1;stroke-linecap:round;stroke-linejoin:round}
.backbtn:hover{opacity:.7}
.brand{font-weight:700;letter-spacing:-.01em}
.spacer{flex:1}
.iconbtn{background:none;border:1px solid var(--line);color:var(--muted);
  border-radius:8px;padding:5px 10px;font-size:.8rem;cursor:pointer}
.iconbtn:hover{color:var(--ink);border-color:var(--muted)}

/* --- bottom tabs ---------------------------------------------------------- */
/* Fixed to the bottom because that is where a thumb is. The same four
   destinations a native tab bar would carry, so the model ports unchanged. */
/* The bar reaches the physical bottom edge; the safe-area padding sits INSIDE
   it, holding the icons clear of the home indicator. That inset used to read
   as a gap because --panel (#fff) and --bg (#fbfbfa) are nearly the same
   colour, so the bar looked like it was floating a centimetre off the bottom.
   A tinted surface and a shadow make it obvious the bar runs to the edge.
   No backdrop-filter: the background is opaque, so it bought nothing and
   creates a compositing layer some mobile browsers render badly. */
.tabs{position:fixed;left:0;right:0;bottom:0;z-index:20;display:flex;
  background:var(--chrome);border-top:1px solid var(--line);
  box-shadow:0 -6px 20px rgb(0 0 0 / 6%);
  padding-bottom:env(safe-area-inset-bottom)}
/* Paint the bar's own colour BELOW the bar as well.
   `bottom:0` is the bottom of the visual viewport, which on a phone browser is
   not always the bottom of the screen - a collapsing address bar, a home
   indicator, or a keyboard inset can leave the bar hovering above the edge
   with the page showing through underneath. Rather than chase the right
   viewport unit per browser, extend the fill downwards so that whatever ends
   up in that strip is the same colour as the bar. There is nothing to see in
   it, so there is no gap to notice. */
.tabs::after{content:"";position:absolute;top:100%;left:0;right:0;height:140px;
  background:var(--chrome);pointer-events:none}
.tabs a{flex:1;display:flex;flex-direction:column;align-items:center;gap:3px;
  padding:9px 4px 7px;color:var(--muted);text-decoration:none;font-size:.68rem;
  letter-spacing:.01em;-webkit-tap-highlight-color:transparent}
/* Outline rather than solid. Five filled shapes across the bottom of the
   screen is a lot of ink for navigation that should sit behind the content. */
.tabs svg{width:23px;height:23px;fill:none;stroke:currentColor;
  stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round}
.tabs a[aria-current="true"] svg{stroke-width:2.1}
.tabs a[aria-current="true"]{color:var(--accent);font-weight:600}
.tabs a:hover{color:var(--ink)}

/* --- home ----------------------------------------------------------------- */
.hero .hello{margin:0 0 14px;font-size:1.05rem;font-weight:600}
.stats{display:flex;gap:10px}
.stat{flex:1;background:var(--panel-2);border-radius:13px;padding:12px 13px;
  display:flex;flex-direction:column;gap:2px}
.stat b{font-size:1.42rem;line-height:1.05;letter-spacing:-.02em;font-weight:600}
.stat span{color:var(--muted);font-size:.72rem}
button.continue{display:flex;flex-direction:column;gap:3px;text-align:left;
  width:100%;cursor:pointer;border:1px solid var(--accent);
  background:var(--accent-soft);color:var(--ink);margin-top:12px}
button.continue .ctop{font-size:.72rem;text-transform:uppercase;
  letter-spacing:.06em;color:var(--accent);font-weight:700}
button.continue .cmain{font-weight:600}
button.continue .cmeta{color:var(--muted);font-size:.8rem}
.card.weak{margin-top:12px}

/* --- content point lists -------------------------------------------------- */
.cplist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;
  gap:2px}
.cplist li{display:flex;gap:11px;align-items:flex-start;padding:10px 12px;
  border-radius:9px;background:var(--panel);border:1px solid var(--line);
  font-size:.88rem}
.cplist li.done{background:var(--good-soft);border-color:transparent}
.cpmark{flex:0 0 auto;min-width:22px;color:var(--good);font-weight:700;
  font-size:.85rem}
.cpmark.no{color:var(--bad)}
.cpmark.ok{color:var(--good)}
.cpbody{flex:1;min-width:0}
.cpbody p{margin:0}
.cpflag{display:block;color:var(--muted);font-size:.76rem;margin-top:3px}
.cpacc{color:var(--muted);font-size:.78rem}

/* --- history -------------------------------------------------------------- */
.hist{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;
  gap:6px}
.hist li{display:flex;gap:10px;align-items:center;font-size:.85rem}
.hstem{color:var(--muted);overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap}
.pill.part{background:var(--warn-soft);color:var(--warn)}
.ok-note{background:var(--good-soft);color:var(--good);border-radius:8px;
  padding:10px 13px;font-size:.86rem;margin-top:12px}

/* --- generic -------------------------------------------------------------- */
.card{background:var(--panel);border:1px solid var(--line);border-radius:var(--radius);
  padding:18px}
.card + .card{margin-top:12px}
h1{font-size:1.35rem;margin:0 0 4px;letter-spacing:-.01em}
h2{font-size:1.05rem;margin:26px 0 10px}
.sub{color:var(--muted);font-size:.88rem;margin:0 0 18px}
label{display:block;font-size:.82rem;color:var(--muted);margin-bottom:5px}
input[type=text],input[type=password],input[type=number]{
  width:100%;padding:12px 14px;border-radius:10px;border:1px solid var(--line);
  background:var(--panel);color:var(--ink);font:inherit}
/* A soft ring rather than a hard outline. The redesign made the focused field
   unmistakable, which matters on a multipart question where three inputs are
   stacked and you need to know which one you are typing into. */
input:focus{outline:none;border-color:var(--accent);
  box-shadow:0 0 0 3px var(--accent-soft)}
.btn{display:inline-flex;align-items:center;justify-content:center;gap:7px;
  background:var(--accent);color:var(--accent-ink);border:none;border-radius:12px;
  padding:12px 18px;font-weight:600;cursor:pointer;width:100%}
.btn:disabled{opacity:.45;cursor:not-allowed}
.btn.ghost{background:none;color:var(--muted);border:1px solid var(--line);font-weight:500}
.btn.ghost:hover{color:var(--ink)}
.row{display:flex;gap:10px}
/* nowrap: "Next question" broke onto two lines beside "Back", which made two
   buttons of the same importance different heights. */
.row .btn{width:auto;flex:1;white-space:nowrap;padding-left:12px;padding-right:12px}
.err{background:var(--bad-soft);color:var(--bad);border-radius:8px;padding:10px 13px;
  font-size:.86rem;margin-top:12px}
.muted{color:var(--muted);font-size:.85rem}
/* The confirmation and the refusal after sending a report. Both existed in the
   markup with no rule behind them, so "Sent. Thank you" and "Could not send
   that" rendered as identical plain paragraphs - the one message that has to
   look like good news and the one that has to look like bad news were
   indistinguishable. Found by listing every class used in markup and diffing
   it against the classes this file defines. */
.good{color:var(--good);font-weight:600}
.bad{color:var(--bad);font-weight:600}
.center{text-align:center}

/* --- module list ---------------------------------------------------------- */
.modules{display:flex;flex-direction:column;gap:8px}
/* Area-of-study heading inside the module list. In English these are the TEXT
   names, with the lenses - themes, quotations, techniques - listed under each,
   which is the whole navigation. The first one loses its top margin so the
   list does not start with a gap. */
.mgroup{display:flex;align-items:baseline;justify-content:space-between;
  gap:12px;margin:18px 0 2px;font-size:.82rem;font-weight:600;
  letter-spacing:.04em;text-transform:uppercase;color:var(--muted)}
.mgroup:first-child{margin-top:0}
.mgroup .sub{margin:0;font-size:.72rem;font-weight:400;letter-spacing:0;
  text-transform:none;white-space:nowrap}
.module.static{cursor:default}
.module{display:flex;align-items:center;gap:12px;background:var(--panel);
  border:1px solid var(--line);border-radius:12px;padding:12px 14px;cursor:pointer;
  text-align:left;width:100%;color:inherit}
/* THE SUBJECT TILE, which replaced a 7px stripe down the left of every row.
   The stripe was a shape carrying a hue and nothing else, repeated once per
   row - eleven identical bars down the module list of one course, where the
   course is already named at the top of the screen. It also took the left
   edge, which is the one place a list actually wants an identifying mark.

   The tile carries the same hue AND a short code read off the course name, so
   it satisfies rule 1 at the top of this file on its own rather than by
   leaning on the name beside it. Colours come from the audited --sf/--s-text
   pair, not from new literals: --sf is the soft ground, --s-text is the tone
   that clears 4.5:1 on it.

   Only COURSE rows get one. A module row inside a course screen does not,
   because the course is the screen it is on. */
.mtile{flex:0 0 auto;width:32px;height:32px;border-radius:9px;display:grid;
  place-items:center;background:var(--sf,var(--panel-2));
  color:var(--s-text,var(--muted));
  font:700 .58rem/1 var(--ui);letter-spacing:.03em}
.module:hover{border-color:var(--s,var(--accent))}
.module:disabled{opacity:.5;cursor:not-allowed}
.module:disabled:hover{border-color:var(--line)}
.mname{flex:1;font-weight:500}
.mmeta{font-size:.76rem;color:var(--muted);white-space:nowrap}
/* display:block IS LOAD BEARING.
   .bar is a <span>, and a span is inline by default - width and height are
   simply ignored on an inline box, so the bar collapses to nothing.
   It survived for months because every .bar in the app lived inside a flex
   row (.module), and a flex item is blockified whether it asked to be or not.
   The moment one was put straight into a plain div - the level bar, the weekly
   goal bar, the group goal bar - it rendered as an empty gap and nobody could
   see it was missing, because a missing bar looks like a design choice.
   Reproduced against the real group-board markup: 346x0 and visible=false
   before, 346x9 and visible=true after. Playwright said so first - "63 x
   locator resolved to hidden" - which is the whole argument for the e2e
   asserting on a rendered element rather than on the DOM. */
.bar{display:block;width:56px;height:6px;border-radius:99px;
  background:var(--panel-2);overflow:hidden}
.bar i{display:block;height:100%;background:var(--accent)}

/* --- question ------------------------------------------------------------- */
/* Leaving a question. Sits above the metadata rather than beside it: the meta
   row is already crowded, and a way out should not be something you hunt for. */
/* .btn sets width:100%, so two of them in here grew to fill the row and met
   in the middle with no seam - one wide grey slab that read as a single
   control. They are secondary navigation, not the point of the screen, so they
   get a gap, their own width, and a smaller size. */
.qnav{display:flex;justify-content:space-between;align-items:center;gap:10px;
  margin:-4px 0 14px}
/* nowrap, for the reason .row .btn already carries: "Skip question" broke onto
   two lines beside "Modules" on a 320px phone, which made two buttons of the
   same importance different heights. They fit on one line at every width the
   app supports. */
.qnav .btn{width:auto;flex:0 1 auto;padding:8px 13px;font-size:.84rem;
  font-weight:500;border-radius:9px;white-space:nowrap}
/* The quiet one, used by the question nav. It lost to a second .linkbtn rule
   800 lines below that made every one of them accent, bold and underlined -
   so the way out of a question has been shouting since that rule landed.
   Renamed rather than merged: the two want different things. */
.quietlink{background:none;border:none;padding:4px 0;cursor:pointer;
  font-size:.83rem;color:var(--muted)}
.quietlink:hover{color:var(--accent);text-decoration:underline}
.qmeta{display:flex;flex-wrap:wrap;align-items:center;gap:7px;margin-bottom:16px}
.tag{font-size:.7rem;text-transform:uppercase;letter-spacing:.03em;color:var(--muted);
  border:1px solid var(--line);border-radius:5px;padding:2px 7px}
.tag.marks{background:var(--accent-soft);border-color:transparent;color:var(--accent);
  font-weight:700}
/* The year beside a course name. Year 11 and Year 12 are separate courses and
   two of them share a name outright, so this tag is the only thing telling
   those two apart in a list - it never shrinks away on a narrow screen, and
   flex:0 0 auto keeps it off .mname's flex:1 growth. */
.tag.year{flex:0 0 auto;font-weight:700;letter-spacing:.06em;
  background:var(--panel);color:var(--muted)}
.timer{margin-left:auto;font-family:var(--mono);font-size:.82rem;color:var(--muted)}
/* Formulas and Calculator sit together at the right of the meta row, with the
   clock directly beneath them rather than inline between the tags. The clock
   is the one thing on that row that CHANGES, and inline it dragged the eye
   back to it every second while reading the question. Both drawers open below
   the whole row, which is what puts them under these buttons. */
/* The tags wrap among themselves on the LEFT and the tools hold their own
   column on the RIGHT. Without this grouping the two interleaved: .qmeta wraps,
   so at 420px the third tag dropped to a second line while Formulas and
   Calculator stayed on the first, and the row read as a jumble.
   .qtags only exists on the two question screens, so the flag card - which
   also uses .qmeta, with bare tag children - is untouched. */
.qtags{flex:1 1 auto;min-width:0;display:flex;flex-wrap:wrap;align-items:center;
  gap:7px}
.qtools{flex:0 0 auto;margin-left:auto;display:flex;flex-direction:column;
  align-items:flex-end;gap:5px}
.qtoolrow{display:flex;gap:6px}
.qtools .timer{margin-left:0}
.timer.over{color:var(--warn);font-weight:600}

/* --- reference sheet ------------------------------------------------------ */
/* Opens in place above the question rather than as an overlay: in the exam the
   sheet sits on the desk next to the paper, not on top of it. */
.reftoggle{background:none;border:1px solid var(--line);color:var(--muted);
  border-radius:5px;padding:2px 9px;font-size:.7rem;text-transform:uppercase;
  letter-spacing:.03em;cursor:pointer}
.reftoggle:hover{color:var(--ink);border-color:var(--muted)}
.reftoggle[aria-expanded="true"]{background:var(--accent-soft);color:var(--accent);
  border-color:transparent;font-weight:700}
.refdrawer{margin:0 0 20px;border:1px solid var(--line);border-radius:10px;
  background:var(--panel-2);padding:4px 14px 14px;max-height:60vh;overflow-y:auto}
.refsec{margin-top:14px}
.refsec h3{margin:0 0 4px;font-size:.9rem;letter-spacing:-.01em}
.refnote{margin:0 0 10px;font-size:.78rem;color:var(--muted);line-height:1.45}
.refgroup{margin-top:12px}
.refgroup h4{margin:0 0 6px;font-size:.72rem;text-transform:uppercase;
  letter-spacing:.04em;color:var(--muted);font-weight:700}
.refrow{padding:7px 0;border-top:1px solid var(--line)}
.refrow:first-of-type{border-top:none}
.refname{font-size:.79rem;color:var(--muted);margin-bottom:2px}
/* Long formulas scroll inside their own row instead of widening the card. The
   fade is the affordance - without it a clipped formula just looks broken. */
.refmath{position:relative;overflow-x:auto;overflow-y:hidden;font-size:.88em}
.refmath .katex-display{margin:.3em 0;text-align:left}
.refmath::after{content:"";position:sticky;right:0;top:0;float:right;
  width:26px;height:1.6em;margin-top:-1.6em;pointer-events:none;
  background:linear-gradient(to right,transparent,var(--panel-2))}
/* The stem reads first and everything else follows it. Larger and tighter
   than body text, because on a long question the difference between "the
   thing being asked" and "the machinery around it" is the whole hierarchy. */
.stem{font-size:1.06rem;line-height:1.5;letter-spacing:-.006em}
.stem p{margin:.7em 0}
.stem p:first-child{margin-top:0}
.parts{margin-top:22px;display:flex;flex-direction:column;gap:20px}
.part{border-top:1px solid var(--line);padding-top:18px}
.plabel{font-weight:700;margin-right:6px}
/* The mark value sits BESIDE the input, not floated at the top right of the
   part. Floated, it read as decoration attached to the prompt; beside the
   field it reads as "this is what this answer is worth", which is the thing a
   student is weighing up when she decides how much working to show. */
.pmarks{font-size:.8rem;color:var(--muted);white-space:nowrap}
.field{margin-top:12px;display:flex;align-items:center;gap:10px;flex-wrap:wrap}
.field input{max-width:230px;flex:1 1 150px}
.hint{flex-basis:100%;color:var(--muted);font-size:.8rem;margin-top:-2px}
.part .field .pmarks{margin-left:auto;padding-left:4px}
.unit{color:var(--muted);font-size:.9rem;white-space:nowrap}
.opts{display:flex;flex-direction:column;gap:8px;margin-top:16px}
.opt{display:flex;align-items:center;gap:11px;border:1px solid var(--line);
  border-radius:9px;padding:12px 14px;cursor:pointer;background:var(--panel)}
.opt:hover{border-color:var(--accent)}
.opt input{accent-color:var(--accent)}
.opt.sel{border-color:var(--accent);background:var(--accent-soft)}
.opt b{min-width:1.2em}

/* --- confidence ----------------------------------------------------------- */
.conf{margin-top:22px;border-top:1px solid var(--line);padding-top:16px}
.confbtns{display:flex;gap:8px;margin-top:8px}
.confbtns button{flex:1;padding:9px 6px;border-radius:8px;border:1px solid var(--line);
  background:var(--panel);color:var(--muted);cursor:pointer;font-size:.83rem}
.confbtns button[aria-pressed=true]{background:var(--accent);border-color:var(--accent);
  color:var(--accent-ink);font-weight:600}
/* What the timer control actually does. "Exam conditions" submits your paper
   for you; a control that does that should say so before it is pressed. */
.segnote{margin:8px 0 0;font-size:.78rem;color:var(--muted);line-height:1.45}

/* --- result --------------------------------------------------------------- */
.verdict{display:flex;align-items:baseline;gap:12px;border-radius:10px;padding:15px 18px;
  margin-bottom:16px}
.verdict.full{background:var(--good-soft);color:var(--good)}
.verdict.part{background:var(--warn-soft);color:var(--warn)}
.verdict.none{background:var(--bad-soft);color:var(--bad)}
.score{font-size:1.5rem;font-weight:700;font-variant-numeric:tabular-nums}
.pending{background:var(--panel-2);color:var(--muted)}
.partlist{list-style:none;padding:0;margin:0 0 16px}
.partlist li{display:flex;gap:10px;align-items:center;padding:7px 0;
  border-bottom:1px solid var(--line);font-size:.9rem}
.partlist li:last-child{border-bottom:none}
.pill{font-size:.72rem;border-radius:99px;padding:2px 9px;font-weight:600}
.pill.ok{background:var(--good-soft);color:var(--good)}
.pill.no{background:var(--bad-soft);color:var(--bad)}
h4{font-size:.75rem;text-transform:uppercase;letter-spacing:.05em;color:var(--muted);
  margin:20px 0 6px}
.sol p{margin:.6em 0}
ol.cp,ul.rb{font-size:.89rem;padding-left:20px;margin:6px 0}
ol.cp li,ul.rb li{margin:5px 0}
/* Wide formulas must scroll inside their own box, never widen the page or get
   clipped at the card edge. The gradient hints that there is more to the right. */
.katex-display{
  overflow-x:auto;overflow-y:hidden;max-width:100%;padding:6px 0;margin:.9em 0;
  -webkit-overflow-scrolling:touch;
  scrollbar-width:thin;
}
.stem,.sol{max-width:100%;overflow-wrap:break-word}
.tablewrap{overflow-x:auto;-webkit-overflow-scrolling:touch;margin:.9em 0;
  border:1px solid var(--line);border-radius:9px}
.tablewrap table{border-collapse:collapse;width:100%;font-size:.87rem}
.tablewrap th,.tablewrap td{padding:8px 11px;text-align:left;
  border-bottom:1px solid var(--line);vertical-align:top}
.tablewrap thead th{background:var(--panel-2);font-weight:600;white-space:nowrap}
.tablewrap tbody tr:last-child td{border-bottom:none}
ul.mdlist{margin:.7em 0;padding-left:20px}
ul.mdlist li{margin:.3em 0}

/* Figures inherit the text colour, so they adapt to light and dark without a
   second palette. The panel tint separates the diagram from the prose. */
.figure{margin:16px 0;padding:12px 8px;border:1px solid var(--line);
  border-radius:10px;background:var(--panel-2);color:var(--ink);
  overflow-x:auto;-webkit-overflow-scrolling:touch}
.figure svg{max-width:100%;height:auto;display:block;margin:0 auto}
/* Display maths that is wider than the card scrolls sideways rather than
   being clipped, and the fade on the right says so. Worked solutions are the
   worst case - a full substitution line is long - so they get a touch less
   type as well, which buys about four characters and is the difference
   between a line that fits and one that does not. */
.mathwrap{position:relative;max-width:100%;overflow-x:auto;
  -webkit-overflow-scrolling:touch;scrollbar-width:none}
.mathwrap::-webkit-scrollbar{display:none}
.sol .katex-display{font-size:.95em}
.mathwrap[data-overflow="1"]::after{
  content:"";position:absolute;top:0;right:0;bottom:0;width:34px;pointer-events:none;
  background:linear-gradient(to right,transparent,var(--panel));
}
.katex{max-width:100%}

/* --- misc ----------------------------------------------------------------- */
.choice{display:flex;align-items:center;gap:11px;border:1px solid var(--line);
  border-radius:9px;padding:13px 15px;margin-bottom:8px;cursor:pointer}
.choice.sel{border-color:var(--accent);background:var(--accent-soft)}
/* WRAPS. `display:flex` with no `flex-wrap` is one line forever, and a flex
   item will not shrink below its own min-content width - so four buttons
   labelled "Something is broken", "A question looks wrong", "An idea" and
   "Something else" simply ran off the right of a phone screen with the last
   one unreachable.
   `flex:1 1 140px` is the fix that works at every width rather than at one:
   grow to fill the row, shrink when there is room, and start a new line below
   about 140px each. On a phone that is two per row, on a desktop four.
   Measured on the real four buttons: 266px past the right edge at 390px wide
   with the whole page scrolling sideways, against 0px and two rows after. */
.seg{display:flex;flex-wrap:wrap;gap:6px;margin-top:6px}
.seg button{flex:1 1 140px;padding:7px;border-radius:7px;
  border:1px solid var(--line);background:var(--panel);color:var(--muted);
  cursor:pointer;font-size:.8rem}
.seg button[aria-pressed=true]{background:var(--accent);border-color:var(--accent);
  color:var(--accent-ink);font-weight:600}
.demo{background:var(--warn-soft);color:var(--warn);border-radius:8px;padding:9px 13px;
  font-size:.8rem;margin-bottom:16px}

/* --- flags ---------------------------------------------------------------- */
/* Deliberately quiet. This button sits under every question and competes with
   Submit for attention; a student who taps it by accident, or who reads it as
   "I give up", is worse off than one who never notices it. Small, muted, and
   phrased as a question. */
.flagwrap{margin:18px 0 0;padding-top:14px;border-top:1px solid var(--line)}
.flagbtn{background:none;border:none;padding:6px 0;cursor:pointer;
  font-size:.8rem;color:var(--muted);width:100%;text-align:left}
.flagbtn:hover{color:var(--warn)}
.flagpanel{display:flex;flex-direction:column;gap:10px}
.flagreasons{display:flex;flex-wrap:wrap;gap:6px}
.flagreason{padding:7px 11px;border-radius:8px;border:1px solid var(--line);
  background:var(--panel);color:var(--muted);cursor:pointer;font-size:.82rem}
.flagreason:hover{color:var(--ink);border-color:var(--warn)}
.flagreason[aria-pressed=true]{background:var(--warn-soft);color:var(--warn);
  border-color:var(--warn);font-weight:600}
.flagpanel textarea{width:100%;border:1px solid var(--line);border-radius:9px;
  padding:10px 12px;font:inherit;font-size:.9rem;background:var(--panel);
  color:var(--ink);resize:vertical}
.flagdone{margin:0;font-size:.85rem;color:var(--good);line-height:1.5}

/* --- admin ---------------------------------------------------------------- */
/* Five tabs need a little less room each; the label is the first thing to go,
   not the icon, which stays the same size at every count. */
body.is-admin .tabs a{font-size:.64rem;padding-left:2px;padding-right:2px}
.tabs a{position:relative}
.badge{position:absolute;top:4px;left:calc(50% + 6px);min-width:17px;height:17px;
  padding:0 4px;border-radius:99px;background:var(--bad);color:var(--on-solid);
  font-size:.62rem;font-weight:700;line-height:17px;text-align:center}

.flagcard .qmeta{margin-bottom:10px}
.tag.reason{background:var(--warn-soft);border-color:transparent;color:var(--warn);
  font-weight:700}
.tag.warn{background:var(--bad-soft);border-color:transparent;color:var(--bad);
  font-weight:700}
/* What the student wrote, set apart from the question text so the two are not
   read as one paragraph. */
.flagnote{margin:0 0 12px;padding:10px 13px;background:var(--panel-2);
  border-radius:9px;font-size:.9rem;line-height:1.5}
.stem.small{font-size:.9rem;max-height:15em;overflow:auto}
.tid{font-family:var(--mono);font-size:.72rem;margin:10px 0 0}
.flagactions{margin-top:14px;display:flex;flex-direction:column;gap:8px}
.flagactions .btn{font-size:.88rem;padding:10px 12px}
.flagactions .btn.danger{width:100%}
.btn.danger{background:var(--bad);color:var(--on-solid)}

/* --- folds ---------------------------------------------------------------- */
/* <details> rather than a click handler and a class: it is the element for
   this, it works before the JavaScript has run, and find-in-page can still
   reach the text inside a closed one. */
/* 1rem, not 1.05: at 390px "Mathematics Standard" plus its summary numbers
   wrapped onto two lines, which makes a closed row look like an open one. */
.fold > summary{display:flex;align-items:center;gap:10px;cursor:pointer;
  list-style:none;padding:2px 0;font-weight:600;font-size:1rem}
.fold > summary::-webkit-details-marker{display:none}
/* The chevron is drawn rather than a character, so it can rotate. */
.fold > summary::before{content:"";flex:0 0 8px;width:8px;height:8px;
  border-right:2px solid var(--muted);border-bottom:2px solid var(--muted);
  transform:rotate(-45deg);transition:transform .15s ease;margin-right:2px}
.fold[open] > summary::before{transform:rotate(45deg)}
.foldname{flex:1;min-width:0}
.foldmeta{color:var(--muted);font-size:.8rem;font-weight:400;white-space:nowrap}
.foldbody{padding-top:14px}
.fold.inner{margin-top:16px;border-top:1px solid var(--line);padding-top:14px}
.fold.inner > summary{font-size:.95rem}
.fold.inner .cplist{margin-top:12px}

/* The flag list is a second section, not a continuation of the header card.
   #flaglist is a plain div, so `.card + .card` never matched across it. */
#flaglist{margin-top:12px}
#flaglist > .card + .card{margin-top:12px}

/* --- locked tabs and the toast -------------------------------------------- */
/* Dimmed and unlabelled-as-current until a course exists. The tap is still
   received - it has to be, or there is nothing to explain itself with. */
body.nocourses .tabs a[aria-disabled="true"]{opacity:.38}
body.nocourses .tabs a[aria-disabled="true"]:hover{color:var(--muted)}

.toast{position:fixed;left:50%;transform:translate(-50%,14px);
  bottom:calc(80px + env(safe-area-inset-bottom));z-index:40;
  max-width:min(92vw,420px);padding:11px 16px;border-radius:10px;
  background:var(--ink);color:var(--bg);font-size:.88rem;text-align:center;
  box-shadow:0 6px 24px rgb(0 0 0 / 22%);
  opacity:0;pointer-events:none;transition:opacity .18s ease,transform .18s ease}
.toast.on{opacity:1;transform:translate(-50%,0)}
@media (prefers-reduced-motion:reduce){.toast{transition:none}}

/* --- written responses ----------------------------------------------------- */
.written{margin-top:14px}
.written textarea{width:100%;box-sizing:border-box;border:1px solid var(--line);
  border-radius:9px;padding:12px 13px;background:var(--panel);color:var(--ink);
  font:inherit;line-height:1.55;resize:vertical;min-height:150px}
.written textarea:focus{outline:none;border-color:var(--accent)}
.wrow{display:flex;justify-content:space-between;align-items:baseline;gap:12px;
  margin-top:6px}
.wrow .hint{flex-basis:auto;margin:0}
.wc{color:var(--muted);font-size:.78rem;font-variant-numeric:tabular-nums;
  white-space:nowrap}

/* Marking your own answer. The disclaimer is not a footnote - it is the most
   important sentence on the screen, because everything below it is a word
   count wearing the clothes of a mark. */
.selfmark{margin-top:6px}
/* Same story as .linkbtn: a second .yours rule below replaced this one
   wholesale, so the quoted answer lost its rule and its pre-wrap. This is the
   one that should win, so it is the one that survives - the later rule is now
   .yoursbox. */
.yours{margin:0 0 4px;padding:12px 14px;border-left:3px solid var(--accent);
  background:var(--panel-2);border-radius:0 8px 8px 0;white-space:pre-wrap;
  font-size:.92rem;line-height:1.6}
.disclaimer{background:var(--warn-soft);color:var(--warn);border-radius:8px;
  padding:10px 13px;font-size:.82rem;line-height:1.55;margin:0 0 12px}
.wcl{list-style:none;padding:0;margin:8px 0 0}
.wcl li{display:flex;gap:9px;align-items:baseline;padding:6px 0;font-size:.88rem;
  border-bottom:1px solid var(--line)}
.wcl li:last-child{border-bottom:none}
.wcl li span{width:1.1em;font-weight:700}
.wcl li.hit span{color:var(--good)}
.wcl li.miss{color:var(--muted)}
.wcl li.miss span{color:var(--warn)}
.wcl li em{margin-left:auto;font-size:.76rem;font-style:normal;color:var(--muted)}
.nudge{margin:12px 0 0;padding:10px 13px;border-radius:8px;
  background:var(--accent-soft);font-size:.85rem;line-height:1.55}
.bands{display:flex;flex-direction:column;gap:8px;margin:10px 0 18px}
.bandrow{display:flex;gap:12px;align-items:flex-start;border:1px solid var(--line);
  border-radius:9px;padding:12px 14px;cursor:pointer;background:var(--panel);
  font-size:.88rem;line-height:1.5}
.bandrow:hover{border-color:var(--accent)}
.bandrow.sel{border-color:var(--accent);background:var(--accent-soft)}
.bandrow input{accent-color:var(--accent);margin-top:3px}
.bandrow b{min-width:2.2em;font-variant-numeric:tabular-nums}

/* --- feedback ------------------------------------------------------------ */
/* The report form on Settings, and the report list on the admin tab. Kept
   deliberately plain: this ships before the visual overhaul, and anything
   fussy here is work that gets thrown away. */
textarea{width:100%;box-sizing:border-box;font:inherit;line-height:1.45;
  padding:10px 12px;border:1px solid var(--line);border-radius:10px;
  background:var(--panel);color:inherit;resize:vertical;min-height:88px}
textarea:focus{outline:2px solid var(--accent);outline-offset:-1px}
.fbrow{display:flex;align-items:center;gap:12px;margin-top:10px}
.fbmine{margin-top:18px;border-top:1px solid var(--line);padding-top:14px}
.fbmine h4{margin:0 0 8px;font-size:.82rem;letter-spacing:.04em;
  text-transform:uppercase;color:var(--muted)}
.fblist{list-style:none;margin:0;padding:0;display:flex;
  flex-direction:column;gap:8px}
.fblist li{display:flex;align-items:flex-start;gap:10px;font-size:.86rem}
.fblist .fbbody{flex:1;min-width:0;overflow-wrap:anywhere}
.fblist em{display:block;margin-top:2px}
.buildline{text-align:center;margin:18px 0 0;font-size:.72rem}
.buildline code{font-size:.72rem}
/* The user agent is long, uninteresting until it matters, and must never push
   the card wide enough to scroll the page sideways. */
.tid.agent{overflow-wrap:anywhere;opacity:.65}

/* --- tests and practice papers ------------------------------------------- */
/* Plain, like the feedback form above and for the same reason: this ships
   before the visual overhaul. What is NOT cosmetic and must survive it is the
   number strip - it is the whole point of sitting a paper on a screen rather
   than one question at a time, and its three states have to be legible at a
   glance and distinguishable without colour. */

.lengths{display:grid;grid-template-columns:repeat(auto-fit,minmax(9rem,1fr));
  gap:8px}
.length{display:flex;flex-direction:column;gap:2px;text-align:left;
  padding:12px 14px;border:1px solid var(--line);border-radius:10px;
  background:var(--panel);color:inherit;cursor:pointer;font:inherit}
.length:hover{border-color:var(--accent)}
.length[aria-pressed="true"]{border-color:var(--accent);
  background:var(--accent-soft)}
.length .lname{font-weight:600;font-size:.9rem}
.length .lmarks{font-variant-numeric:tabular-nums;font-size:.82rem;
  color:var(--accent);font-weight:600}
.length .lwhy{color:var(--muted);font-size:.76rem;line-height:1.35}

.modpicks{display:flex;flex-direction:column;gap:2px;margin-top:6px}
.modpick{display:flex;align-items:center;gap:10px;padding:8px 4px;
  cursor:pointer;font-size:.88rem;border-bottom:1px solid var(--line)}
.modpick:last-child{border-bottom:none}
.modpick input{accent-color:var(--accent);flex:none}
.modpick span:nth-of-type(1){flex:1;min-width:0}
.modpick .sub{flex:none;font-size:.76rem}
label.check{display:flex;align-items:center;gap:9px;margin-top:10px;
  font-size:.86rem;cursor:pointer}
label.check input{accent-color:var(--accent)}
.linkbtn.wide{display:block;width:100%;text-align:center;margin-top:12px}
/* The one card on the course screen that is already running, and it does need
   to look different from the two around it. It used to say so with an accent
   border, which under the new palette came out heavier than any other line on
   the page - the darkest thing on screen, for the least important reason.
   The tint says the same thing in the language the rest of the app uses:
   subject colour means "this belongs to this course". No border at all, so it
   stops competing with the cards it sits between. */
.card.resume{background:var(--sf,var(--accent-soft));border-color:transparent}

.bptable{width:100%;border-collapse:collapse;font-size:.86rem}
/* The header that says what the numbers are. Without it the table read as
   "24  of 24.1" with no unit anywhere, which is what Charlie reported. */
.bptable th{padding:0 0 5px;text-align:left;font-size:.74rem;font-weight:600;
  letter-spacing:.04em;text-transform:uppercase;color:var(--muted);
  border-bottom:1px solid var(--line)}
.bptable th.num{text-align:right}
.bptable td{padding:5px 0;border-bottom:1px solid var(--line)}
/* The printed answer key. It used to carry only a module name and a mark
   count; it now carries the answer and the worked solution, so it needs to
   read as prose on paper rather than as a list of labels. */
.pans{margin:2px 0 0;font-size:.95rem}
.psol{margin:2px 0 8px;font-size:.86rem;line-height:1.45}
.psol p{margin:.3em 0}
.rubric{margin:.2em 0 0;padding-left:1.1em;font-size:.88rem}
.rubric li{margin:.25em 0}
.bptable tr:last-child td{border-bottom:none}
.bptable .num{text-align:right;font-variant-numeric:tabular-nums;
  white-space:nowrap;padding-left:10px}
/* A topic two or more marks off its share. Said out loud rather than hidden:
   the pool cannot always hit the blueprint, and pretending it did would make
   the number meaningless. */
.bptable .off{color:var(--warn)}

/* --- sitting a paper ------------------------------------------------------ */

.sit{display:flex;gap:12px;align-items:flex-start}
.sitmain{flex:1;min-width:0}
.strip{display:flex;flex-direction:column;gap:5px;flex:none;
  position:sticky;top:8px;max-height:calc(100vh - 140px);overflow-y:auto;
  padding-right:2px;scrollbar-width:thin}
.qnum{width:2.1rem;height:2.1rem;flex:none;border-radius:8px;cursor:pointer;
  border:1px solid var(--line);background:var(--panel);color:var(--muted);
  font:600 .8rem/1 inherit;font-variant-numeric:tabular-nums;
  display:flex;align-items:center;justify-content:center;position:relative}
.qnum:hover{border-color:var(--accent);color:var(--ink)}
.qnum.done{background:var(--accent-soft);border-color:var(--accent-soft);
  color:var(--ink)}
/* The flag has to read without colour, so it is a corner wedge as well as a
   tint - a printed screenshot and a colour-blind reader both need to see it. */
.qnum.flag{border-color:var(--warn)}
.qnum.flag::after{content:"";position:absolute;top:0;right:0;
  border:5px solid transparent;border-top-color:var(--warn);
  border-right-color:var(--warn);border-top-right-radius:7px}
/* An INSET ring, not an outline.
   An outline is painted outside the border box, and the number strip is a
   sticky column with `overflow-y:auto` - so the scroll container clipped it and
   the ring around the current question came out sliced down one side. An inset
   box-shadow is drawn inside the box and cannot be clipped by an ancestor. */
.qnum.here{box-shadow:inset 0 0 0 2px var(--accent);color:var(--ink)}

.sitbar{display:flex;align-items:center;gap:12px;margin-bottom:8px;
  flex-wrap:wrap}
.sitbar .timer{font-variant-numeric:tabular-nums;font-size:.82rem;
  color:var(--muted)}
.sitbar .timer.over{color:var(--warn);font-weight:600}
.sitbar .sub{flex:1;min-width:0;font-size:.78rem}
.sitnav{display:flex;align-items:center;justify-content:space-between;gap:10px;
  margin-top:16px;flex-wrap:wrap}
.flagtoggle{background:none;border:1px solid var(--line);border-radius:99px;
  padding:5px 12px;cursor:pointer;color:var(--muted);font:inherit;
  font-size:.78rem}
.flagtoggle[aria-pressed="true"]{border-color:var(--warn);color:var(--warn);
  background:var(--warn-soft)}

@media (max-width:34rem){
  /* On a phone the strip goes across the top and scrolls sideways. Down the
     side it would be either too narrow to tap or wide enough to squeeze the
     question into a column. */
  .sit{flex-direction:column}
  .strip{flex-direction:row;position:static;max-height:none;width:100%;
    overflow-x:auto;padding-bottom:4px}
}

/* --- the marked paper ----------------------------------------------------- */

.card.score{text-align:center}
.bigscore{font-size:2.4rem;font-weight:700;line-height:1;
  font-variant-numeric:tabular-nums}
.bigscore .sub{font-size:1.1rem;font-weight:500}
.bar.big{width:100%;height:9px;margin-top:12px}

.qresult{border:1px solid var(--line);border-radius:10px;margin-bottom:8px;
  background:var(--panel);overflow:hidden}
.qresult summary{display:flex;align-items:center;gap:10px;padding:11px 13px;
  cursor:pointer;font-size:.87rem;list-style:none}
.qresult summary::-webkit-details-marker{display:none}
.qresult .qn{flex:none;width:1.9rem;height:1.9rem;border-radius:7px;
  display:flex;align-items:center;justify-content:center;font-weight:600;
  font-size:.78rem;background:var(--panel-2);font-variant-numeric:tabular-nums}
.qresult .qsum{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;
  white-space:nowrap;color:var(--muted)}
.qresult .qbadge{flex:none;font-size:.76rem;font-weight:600;white-space:nowrap;
  font-variant-numeric:tabular-nums}
.qresult .qflag{flex:none;color:var(--warn)}
.qresult.right .qn{background:var(--good-soft);color:var(--good)}
.qresult.right .qbadge{color:var(--good)}
.qresult.part .qn,.qresult.part .qbadge{color:var(--warn)}
.qresult.part .qn{background:var(--warn-soft)}
.qresult.wrong .qn{background:var(--bad-soft);color:var(--bad)}
.qresult.wrong .qbadge{color:var(--bad)}
.qresult.topmark .qbadge,.qresult.blank .qbadge{color:var(--muted);
  font-weight:500}
.qbody{padding:0 13px 14px;border-top:1px solid var(--line)}
.yoursbox{margin-top:12px;padding:10px 12px;background:var(--panel-2);
  border-radius:9px}
.yours h4,.yoursbox h4,.sol h4{margin:0 0 4px;font-size:.74rem;letter-spacing:.05em;
  text-transform:uppercase;color:var(--muted)}
.sol{margin-top:12px}
.paperrow .mmeta{white-space:normal}

/* --- the printed paper ---------------------------------------------------- */
/* On screen this is a preview at roughly page width. In print it IS the page:
   everything else on the document is hidden, because a printed paper with a
   tab bar across the bottom of every sheet is not a paper. */

.printsheet{margin-top:14px}
.paper{background:var(--panel);border:1px solid var(--line);border-radius:12px;
  padding:24px 26px;max-width:46rem;margin:0 auto}
.phead h1{margin:0;font-size:1.3rem}
.pmeta{margin:4px 0 0;color:var(--muted);font-size:.82rem}
.psection{margin:26px 0 10px;font-size:.9rem;letter-spacing:.05em;
  text-transform:uppercase;border-bottom:1px solid var(--line);
  padding-bottom:5px}
.pq{margin:0 0 22px;break-inside:avoid}
.pqhead{display:flex;justify-content:space-between;align-items:baseline;
  gap:12px}
.pqn{font-weight:700;font-variant-numeric:tabular-nums}
.pqmarks{color:var(--muted);font-size:.82rem;white-space:nowrap}
.ppart{display:flex;gap:8px;align-items:baseline;margin:10px 0 4px;
  font-size:.9rem}
.ppart .plabel{font-weight:600}
.ppart .pqmarks{margin-left:auto}
.popts{display:flex;flex-direction:column;gap:7px;margin-top:9px}
.popt{display:flex;align-items:baseline;gap:9px;font-size:.9rem}
.pbox{flex:none;width:.95rem;height:.95rem;border:1px solid var(--ink);
  border-radius:3px;transform:translateY(2px)}
.prules{margin-top:8px;display:flex;flex-direction:column;gap:0}
/* Ruled lines rather than an empty box: a box invites a two-word answer to a
   four-mark question, and the number of lines is the clearest signal there is
   about how much writing is wanted. */
.prule{display:block;height:1.65rem;border-bottom:1px solid var(--line)}
/* Both spellings on purpose. `break-before` is the current property and is
   what Chromium honours - verified by printing a 14 question paper to PDF and
   reading back which page each landmark fell on (the key lands alone, on its
   own page). `page-break-before` is the legacy alias older engines want, and
   costs one declaration. See B7 in docs/09-backlog.md: the bug was reported
   against a browser that was never identified. */
.ppage{break-before:page;page-break-before:always;margin-top:30px}
/* The reference sheet is the FIRST thing after the paper header, so it pushes
   the questions onto the next page rather than pushing itself onto one. Both
   spellings, same as .ppage. */
.refpage{break-after:page;page-break-after:always;margin-bottom:20px}
.pkey{font-size:.86rem;line-height:1.7}

/* --- gamification --------------------------------------------------------
   Plain and undecorated on purpose. Charlie is doing a visual overhaul, and
   structure survives one while styling does not, so nothing here does more
   than lay the elements out and lean on the tokens already at the top of this
   file. No new colours. */

/* The home hero and the quest rows became targets. Without a cursor and a
   hover the only way to find out they were tappable was to tap them. */
.tappable{cursor:pointer}
.tappable:hover{border-color:var(--accent)}
.herostreak{margin:14px 0 0;font-size:.86rem;color:var(--muted)}

.lvl{margin-top:14px}
.lvltop{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  margin-bottom:6px}
.lvlname{font-weight:600;font-size:.9rem}
.lvlxp{color:var(--muted);font-size:.76rem}

/* Seven buttons on one row, because a weekly goal is a choice between seven
   things and a dropdown hides six of them behind a tap. */
.goalpick{display:flex;gap:6px}
.goalbtn{flex:1;padding:9px 0;border-radius:9px;border:1px solid var(--line);
  background:var(--panel);color:var(--ink);font-weight:600;cursor:pointer}
.goalbtn:hover{border-color:var(--accent)}
.goalbtn.on{background:var(--accent-soft);border-color:var(--accent);
  color:var(--accent)}

.calscore{color:var(--accent);font-size:1.25rem;margin-left:6px}

/* Two columns on a phone, three once there is room. Every badge is shown,
   earned or not: a locked badge you can see is a goal, and one you cannot see
   is nothing at all. */
.badges{display:grid;grid-template-columns:repeat(2,1fr);gap:8px}
@media (min-width:520px){.badges{grid-template-columns:repeat(3,1fr)}}
.badge2{border:1px solid var(--line);border-radius:10px;padding:11px 12px;
  background:var(--panel-2);opacity:.5}
.badge2.got{opacity:1;border-color:var(--accent);background:var(--accent-soft)}
.bicon{display:block;font-weight:700;font-size:1rem;color:var(--accent);
  letter-spacing:.05em}
.bname{display:block;font-weight:600;font-size:.84rem;margin-top:3px}
.bdesc{display:block;color:var(--muted);font-size:.72rem;margin-top:3px;
  line-height:1.35}
/* The "hidden" marker. Charlie asked for "a little hidden text on them to
   show they are hidden ones" - so it sits on the EARNED ones too, because
   part of the reward is knowing you found a secret one. */
.badge2{position:relative}
.btag{position:absolute;top:7px;right:8px;font-size:.6rem;font-weight:700;
  letter-spacing:.07em;text-transform:uppercase;color:var(--muted);
  border:1px solid var(--line);border-radius:4px;padding:1px 4px;
  background:var(--panel)}
.badge2.got .btag{color:var(--accent);border-color:transparent;
  background:var(--panel)}
/* A secret one is dimmer still, and its dashed edge says there is something
   there rather than something missing. */
.badge2.secret{border-style:dashed;opacity:.42}
.badge2.secret .bicon{color:var(--muted)}
/* "Copy answer" on the calculator bar. Pushed to the right so it is nowhere
   near MC, which clears memory - two destructive-looking taps side by side is
   how somebody loses a running total.

   It used to set only margin and weight, which meant it had NO background, no
   border, no padding and no radius - so it fell through to the browser's own
   button chrome and rendered as a beige bevelled box next to three styled
   ones. The contrast sweep in the e2e never caught it because a default
   button is perfectly readable; it is just from a different app.

   Accent-filled rather than outlined like MR and MC, because it is the one
   button on this bar that does something to the question rather than to the
   calculator. */
.calcuse{margin-left:auto;font-weight:600;white-space:nowrap;
  background:var(--accent);color:var(--accent-ink);border:1px solid var(--accent);
  border-radius:9px;padding:6px 11px;font-size:.78rem;cursor:pointer}
.calcuse:hover{opacity:.9}

/* --- what an overseer gives, and what the student sees -------------------
   Praise is styled DIFFERENTLY from XP on purpose. They are different
   currencies and the app must never let one read as the other - see the
   header of migration 017 for why the leaderboard depends on it. */
.rewardrow{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;
  margin-bottom:14px}
.rewardbtn{display:flex;flex-direction:column;align-items:center;gap:4px;
  padding:10px 4px;border:1px solid var(--line);border-radius:10px;
  background:var(--panel);cursor:pointer;font-size:.68rem;color:var(--muted);
  text-align:center;line-height:1.2}
.rewardbtn:hover{border-color:var(--accent);color:var(--accent)}
.rewardbtn:disabled{opacity:.5;cursor:not-allowed}
.ricon{font-size:1.1rem;color:var(--accent)}

/* Warm, not the accent: the accent is XP everywhere else in the app. */
.praisecount{display:flex;align-items:baseline;gap:8px;padding:10px 12px;
  border-radius:10px;background:var(--warn-soft);margin-bottom:12px}
.praisecount b{font-size:1.3rem;color:var(--warn)}
.praisecount span{font-size:.78rem;color:var(--muted)}

.feed{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;
  gap:8px}
.feeditem{display:grid;grid-template-columns:auto 1fr auto;gap:10px;
  align-items:baseline;padding:9px 11px;border-radius:10px;
  background:var(--panel-2);font-size:.86rem}
.feeditem.unread{background:var(--warn-soft)}
.fico{color:var(--warn);font-weight:700}
.fwhen{color:var(--muted);font-size:.7rem;white-space:nowrap}

/* A follow-only account: Home is hidden and Insights leads.
   `order` rather than reordering the markup, because the moment they pick a
   course they are an ordinary student again and the bar has to go back to
   normal without anything being rebuilt. */
body.followonly #tabs a[data-tab="home"]{display:none}
body.followonly #tabs a[data-tab="progress"]{order:-1}

/* The boot splash. Sized and centred so nothing jumps when it is replaced by
   the first screen, and the bar has a real width from the start rather than
   growing from zero - a bar that begins empty reads as "nothing is
   happening", which is the impression this exists to remove. */
/* FIXED, not in the flow.
   #view sits directly under the topbar, and the topbar is `hidden` until the
   first setChrome() - so when chrome appears, #view drops by its height and
   everything in it jumps. Measured at a layout shift of 0.058, which is the
   boot half of what Charlie described as the screen refreshing.
   Taking the splash out of the flow means the thing on screen during boot is
   not the thing that moves. */
.splash{position:fixed;inset:0;z-index:50;background:var(--bg);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:12px;padding:24px}
.splashlogo{opacity:.92}
.splashname{font-weight:700;font-size:1.25rem;letter-spacing:.02em}
.splashbar{width:min(220px,60vw);height:6px;border-radius:99px;
  background:var(--line);overflow:hidden}
.splashbar i{display:block;height:100%;background:var(--accent);
  /* Eased, so a step that lands instantly still reads as movement rather
     than a jump. Width only - never a keyframe animation, because the bar
     must never advance on its own. */
  transition:width .28s ease}
.splashstep{font-size:.8rem;color:var(--muted)}
/* A slow boot is the only time the hint is worth the words. */
.splashslow{font-size:.76rem;color:var(--muted);max-width:32ch;text-align:center}

/* The board. Four columns that stay in the same place down the list, because
   a leaderboard is read by scanning a column rather than a row. */
.board{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;
  gap:6px}
.board li{display:grid;grid-template-columns:1.6rem 1fr auto auto;gap:10px;
  align-items:baseline;padding:10px 12px;border-radius:9px;
  border:1px solid var(--line);background:var(--panel)}
.board li.me{border-color:var(--accent);background:var(--accent-soft)}
.board .rank{color:var(--muted);font-size:.8rem;font-weight:600}
.board .who{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.board .xp{font-weight:600}
.board .days{color:var(--muted);font-size:.76rem}

.weeknav{display:flex;gap:10px;margin-top:14px}
.weeknav .btn{width:auto;flex:1}

/* Letter spaced and large, because this is read out loud across a room or
   typed off a screenshot. */
.code{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:1.5rem;
  letter-spacing:.22em;font-weight:700;margin:0 0 14px;color:var(--accent)}


/* Leaving a group is the one destructive button on these screens, and it sits
   next to two that are not.
   .btn.ghost.danger, NOT .btn.danger. Written unscoped, this rule also matched
   the SOLID danger button in the admin queue - "Retire this question" - and
   being 400 lines later it won, setting red text on the red ground that rule
   had already given it. Rendered contrast 1.00:1, in both themes: the button
   has been there and unreadable. Every use of .danger in app.js except the
   retire button carries .ghost, which is what this rule was always for. */
/* background AND border, not just colour.
   `.btn.ghost{background:none}` sits at line ~223 and `.btn.danger{background:
   var(--bad)}` at ~496. Same specificity, so the LATER one won and every ghost
   danger button got a solid red ground it was never meant to have - while this
   rule, at 0,3,0, kept the red TEXT. Red on red, measured at 1.00:1: "Leave
   this group", "Close this group", "Stop sharing with them" and "Stop
   following them" have all been solid red blocks with no readable label.
   Worse, "Close this group" arms on first tap and swaps its own label to
   "Tap again to close it for everyone" - a confirmation nobody could read. */
.btn.ghost.danger{background:none;color:var(--bad);border-color:var(--bad-soft)}
.btn.ghost.danger:hover{color:var(--bad);border-color:var(--bad)}

/* Stacked, because these three sit in one card and a row of three on a phone
   gives each of them four characters of width. */
.card .btn.ghost + .btn.ghost{margin-top:8px}

/* --- 011: group goals, the freeze shop, and the numbers screen ------------
   Still plain, still no new colours. */

.shop{margin-top:14px;padding-top:14px;border-top:1px solid var(--line)}
.shoprow{display:flex;align-items:center;justify-content:space-between;gap:12px;
  flex-wrap:wrap}
.shoprow .btn{width:auto;flex:none}

.ggoal{margin-bottom:14px}

/* Segmented pickers. Buttons in a row rather than a <select>, because a
   dropdown hides every option but one behind a tap, and the whole point of
   this screen is switching between them. */
.segmented{display:flex;gap:6px}
.segmented.wrap{flex-wrap:wrap;margin-top:8px}
/* .segbtn, NOT .seg. There is already a `.seg` further up this file and it is
   a CONTAINER - `<div class="seg">` wrapping a row of buttons, used by the
   report kinds, the timer modes, the theme, the admin queues and the paper
   delivery toggle. Naming the ITEM class `.seg` too meant the container
   silently inherited padding, a border, a background and - the one that
   showed - `white-space:nowrap`, so the report kinds could no longer wrap and
   ran off the side of a phone. Two meanings, one name, and the collision was
   invisible in both files on its own. */
.segbtn{padding:8px 14px;border-radius:9px;border:1px solid var(--line);
  background:var(--panel);color:var(--ink);font-size:.84rem;font-weight:600;
  cursor:pointer;white-space:nowrap}
.segbtn:hover{border-color:var(--accent)}
.segbtn.on{background:var(--accent-soft);border-color:var(--accent);
  color:var(--accent)}

/* One row per bucket. The label column is fixed so the bars line up, which is
   the only reason a row of bars is readable at all. */
.series{display:flex;flex-direction:column;gap:8px}
.srow{display:grid;grid-template-columns:5.2rem 1fr auto;gap:10px;
  align-items:center}
.slabel{color:var(--muted);font-size:.76rem;white-space:nowrap;
  overflow:hidden;text-overflow:ellipsis}
/* Two bars in one track: marks possible behind, marks awarded in front. The
   gap between them IS the marks dropped, which is easier to read at a glance
   than two numbers. */
.sbar{position:relative;height:10px;border-radius:99px;background:var(--panel-2);
  overflow:hidden}
.sbar i{position:absolute;left:0;top:0;height:100%;border-radius:99px}
.sbar i.pos{background:var(--accent);opacity:.25}
.sbar i.got{background:var(--accent);z-index:1}
.svals{text-align:right;white-space:nowrap}
.svals b{display:block;font-size:.82rem}
.svals span{display:block;color:var(--muted);font-size:.7rem}

@media (max-width:380px){
  .srow{grid-template-columns:4.2rem 1fr auto}
  .svals span{display:none}
}

/* A button that reads as a link, for the one inside a sentence in the recap
   card. A full-width .btn there would break the paragraph in half. */
.linkbtn{background:none;border:none;padding:0;font:inherit;color:var(--accent);
  font-weight:600;cursor:pointer;text-decoration:underline}

/* The row above the module checkboxes: select all on the left, the escape hatch
   back to a whole-course paper on the right. */
.pickbar{display:flex;justify-content:space-between;align-items:center;gap:12px;
  margin-bottom:10px;min-height:1.4rem}

/* --- stacking cards across wrapper divs -----------------------------------
   `.card + .card` is an ADJACENT SIBLING selector, so it only puts a gap
   between two cards that sit next to each other in the DOM. The moment a
   screen wraps a section in a plain <div> - which every screen that redraws
   one section on its own has to do - the selector stops matching and the
   cards butt together with no gap at all.

   That is the same fault #flaglist hit above, generalised. `.stack` puts the
   gap on the CONTAINER instead, which does not care how its children are
   grouped, and the unclassed wrapper divs inside it get the same treatment so
   a section holding several cards spaces them too.

   `div:not([class])` is deliberate: it catches `<div id="totals">`-style
   wrappers and leaves anything with a class of its own - .jump, .modules,
   .segmented - laid out the way that class intends. */
.stack{display:flex;flex-direction:column;gap:12px}
.stack > div:not([class]){display:flex;flex-direction:column;gap:12px}
/* The container gap has already done this, and both together is 24px. */
.stack > .card + .card,
.stack > div:not([class]) > .card + .card{margin-top:0}

/* A card inside a wrapper that FOLLOWS a card.
   `#recap > .card` was the one-off for this on home. It is not a home problem:
   any screen that redraws one section on its own has to wrap that section in a
   plain div, and the moment it does, `.card + .card` stops matching and the two
   cards butt together with no gap at all. It happened again on the course
   screen, where "Paper in progress" lives in <div id="openpaper"> directly
   after the "Practise a mix" card and sat flush against it.

   So the one-off becomes the rule. `div:not([class])` still catches id-only
   wrappers like #recap and #openpaper and still leaves .jump, .modules and
   .segmented laid out the way those classes intend, and `:not(.stack) >`
   keeps it off the screens where .stack is already spacing the children -
   both would be 24px. */
:not(.stack) > .card + div:not([class]) > .card{margin-top:12px}

/* --- the overseer portal --------------------------------------------------
   The list has to stay readable at thirty rows, which is the whole constraint.
   One line of identity, one of numbers, one of status - fixed positions down
   the column, because a list like this is read by scanning a column and not by
   reading a row. */
.srow2{display:block;width:100%;text-align:left;cursor:pointer;
  background:var(--panel);border:1px solid var(--line);border-radius:10px;
  padding:12px 14px;color:var(--ink);font:inherit;margin-bottom:8px}
.srow2:last-child{margin-bottom:0}
.srow2:hover{border-color:var(--accent)}
/* Flagged rows are OUTLINED rather than filled. At thirty rows a filled
   highlight on six of them is a stripy mess; a border reads as "look here"
   without shouting, and the sort has already put them at the top. */
.srow2.flagged{border-color:var(--warn);background:var(--warn-soft)}
.sname{display:block;font-weight:600;font-size:.95rem}
.stag{display:inline-block;margin-left:8px;padding:1px 8px;border-radius:99px;
  background:var(--panel-2);color:var(--muted);font-size:.68rem;font-weight:500;
  vertical-align:middle}
.smeta{display:block;color:var(--muted);font-size:.78rem;margin-top:3px}
.sflag{display:block;font-size:.74rem;margin-top:5px;color:var(--muted)}
.srow2.flagged .sflag{color:var(--warn);font-weight:600}
/* The name and the flag on one line, the flag pushed right. It used to be a
   third line in muted text - the same weight as the numbers above it, so the
   one thing on a flagged row worth noticing was the quietest thing on it. */
.stop{display:flex;align-items:baseline;gap:10px}
.stop .sname{flex:1;min-width:0}
.schip{flex:0 0 auto;font-size:.68rem;font-weight:700;letter-spacing:.04em;
  text-transform:uppercase;padding:3px 8px;border-radius:7px;
  background:var(--warn);color:var(--on-solid);white-space:nowrap}
/* A heading over each group, carrying its count. The question this screen
   answers is "does anybody need me?" and the heading answers it before a row
   is read. */
.sgroup{display:flex;align-items:baseline;justify-content:space-between;
  gap:10px;margin:16px 0 8px;font-size:.72rem;font-weight:700;
  letter-spacing:.12em;text-transform:uppercase;color:var(--muted)}
.sgroup:first-child{margin-top:0}
.sgroup.warn{color:var(--warn)}
.sgroup span{font-variant-numeric:tabular-nums}

/* A module nested under its subject in the report. Indented rather than given
   a heading of its own, so eight modules under two subjects still reads as two
   things and not ten. */
.module.static.sub .mname{font-weight:400;font-size:.86rem}

@media print{
  :root{--bg:#fff;--panel:#fff;--panel-2:#fff;--ink:#000;--muted:#555;
        --line:#bbb}
  .topbar,.tabs,.demo,.card,.buildline{display:none !important}
  .printsheet{display:block;margin:0}
  .paper{border:none;border-radius:0;padding:0;max-width:none}
  .app,#view{padding:0;margin:0;max-width:none}
  .prule{border-bottom-color:#999}
  a[href]::after{content:""}
}

/* ===========================================================================
   SUBJECT COLOUR
   Four tokens per subject, because one colour cannot do four jobs safely:
     --s        identity - the stripe, the bar, a button ground. Never text.
     --sf       the soft fill it sits on
     --s-text   a darker tone of the same hue, for when the colour IS text
     --s-ink    the label colour that goes ON --s
   The tone is set by app.js from the course's subject_id (see toneFor), so a
   screen never has to know a colour, only which course it is showing.
   =========================================================================== */
[data-subj="ms"] {--s:#316d99;--sf:#e5f1fe;--s-text:#316d99;--s-ink:#fff}
[data-subj="ma"] {--s:#ac70e6;--sf:#f7ebff;--s-text:#8250af;--s-ink:#14161d}
[data-subj="bio"]{--s:#6e9757;--sf:#e8f3e0;--s-text:#47792f;--s-ink:#14161d}
[data-subj="ees"]{--s:#826417;--sf:#faeeda;--s-text:#826417;--s-ink:#fff}
[data-subj="eng"]{--s:#bd778a;--sf:#ffeaef;--s-text:#a24c65;--s-ink:#14161d}
/* Dark tones twice, for the same reason the palette above is: the media query
   covers "System", the attribute covers somebody who picked Dark in Settings
   on a phone set to light. */
@media (prefers-color-scheme:dark){
  :root:not([data-theme="light"]) [data-subj="ms"] {--s:#78d2f1;--sf:#233a42;--s-text:#78d2f1;--s-ink:#14161d}
  :root:not([data-theme="light"]) [data-subj="ma"] {--s:#919bea;--sf:#34354c;--s-text:#919bea;--s-ink:#14161d}
  :root:not([data-theme="light"]) [data-subj="bio"]{--s:#97d694;--sf:#2a3b29;--s-text:#97d694;--s-ink:#14161d}
  :root:not([data-theme="light"]) [data-subj="ees"]{--s:#c09b50;--sf:#413521;--s-text:#c09b50;--s-ink:#14161d}
  :root:not([data-theme="light"]) [data-subj="eng"]{--s:#e980a2;--sf:#4c2e37;--s-text:#e980a2;--s-ink:#14161d}
}
:root[data-theme="dark"] [data-subj="ms"] {--s:#78d2f1;--sf:#233a42;--s-text:#78d2f1;--s-ink:#14161d}
:root[data-theme="dark"] [data-subj="ma"] {--s:#919bea;--sf:#34354c;--s-text:#919bea;--s-ink:#14161d}
:root[data-theme="dark"] [data-subj="bio"]{--s:#97d694;--sf:#2a3b29;--s-text:#97d694;--s-ink:#14161d}
:root[data-theme="dark"] [data-subj="ees"]{--s:#c09b50;--sf:#413521;--s-text:#c09b50;--s-ink:#14161d}
:root[data-theme="dark"] [data-subj="eng"]{--s:#e980a2;--sf:#4c2e37;--s-text:#e980a2;--s-ink:#14161d}

/* The mastery figure at the end of a module row. Text, so it takes --s-text. */
.mpct{font-family:var(--display);font-weight:600;font-size:1.02rem;
  color:var(--s-text,var(--accent));font-variant-numeric:tabular-nums}

/* ===========================================================================
   THE HOME HERO - the daily ring
   =========================================================================== */
/* The one gradient in the app, and the reason it is here and nowhere else:
   this is the front door. Rewards gets it too, because that is the room the
   hero opens into, and nothing else does. Defined as a token pair rather than
   a literal - a literal is how the old hero stayed light while the text on it
   followed the theme into dark. */
/* --hero-line is the EDGE, and it is its own token for the same reason the
   gradient is: --line is #e2e0ec and the page is #ebe9f4, which measures
   1.09:1 - an edge you cannot see. This one is 1.48:1 against the page and
   1.51:1 against the gradient, so it reads from both sides without becoming a
   hard rule. Measured, not picked. */
:root{--hero-a:#e9ecfb;--hero-b:#f3ebf8;--hero-c:#e6f0f4;--hero-line:#c4c0d6}
@media (prefers-color-scheme:dark){:root:not([data-theme="light"]){
  --hero-a:#232840;--hero-b:#2b2440;--hero-c:#1d2c33;--hero-line:#3d4254;
}}
:root[data-theme="dark"]{--hero-a:#232840;--hero-b:#2b2440;--hero-c:#1d2c33;
  --hero-line:#3d4254}

/* The hero is a gradient on a page background that is itself a pale lilac, and
   with a transparent border the two met with nothing between them - the card
   had no edge, so it read as part of the page rather than as the front door.
   A real border plus a soft lift is enough; it does not need to be darker. */
.card.hero{background:linear-gradient(160deg,var(--hero-a) 0%,var(--hero-b) 55%,var(--hero-c) 100%);
  border:1px solid var(--hero-line);
  box-shadow:0 1px 3px rgb(0 0 0 / .05), 0 6px 18px rgb(0 0 0 / .04)}
.hero .hello{margin:0 0 14px;font-size:1.28rem;font-weight:600;letter-spacing:-.015em}
/* The ring and the sentence beside it. Wraps to one column only if the ring
   and the words cannot both fit, which on a 320px phone they can. */
.goal{display:flex;align-items:center;gap:17px}
.goaltext{flex:1;min-width:0}
.goaltext h3{margin:0 0 3px;font-size:1.16rem;font-weight:600;letter-spacing:-.015em}
.goaltext p{margin:0;font-size:.85rem;color:var(--muted)}
.ringwrap{position:relative;flex:0 0 auto;width:98px;height:98px}
.ringwrap svg{display:block;width:100%;height:100%}
.ringwrap circle{fill:none;stroke-linecap:round}
.ringwrap .trk{stroke:var(--panel-2)}
.ringwrap .val{stroke:var(--accent);transition:stroke-dashoffset .5s ease}
@media (prefers-reduced-motion:reduce){.ringwrap .val{transition:none}}
.ringnum{position:absolute;inset:0;display:flex;flex-direction:column;
  align-items:center;justify-content:center;gap:1px}
.ringnum b{font-family:var(--display);font-size:1.42rem;font-weight:600;line-height:1;
  font-variant-numeric:tabular-nums}
.ringnum span{font-size:.62rem;letter-spacing:.08em;text-transform:uppercase;
  color:var(--muted);font-weight:700}
/* The streak, as a chip rather than a number in a row of numbers. */
.fire{display:inline-flex;align-items:center;gap:6px;background:var(--warn-soft);
  color:var(--warn);border-radius:999px;padding:6px 12px;font-size:.8rem;font-weight:600}
.fire svg{width:15px;height:15px;fill:currentColor}
.herotop{display:flex;align-items:center;gap:12px;margin-bottom:14px}
.herotop .hello{margin:0;flex:1;min-width:0}
.quest{display:inline-flex;align-items:center;gap:6px;background:var(--panel-2);
  color:var(--muted);border-radius:999px;padding:7px 13px;font-size:.79rem;font-weight:600;
  margin-top:12px}
/* Earned badges, on the hero. Wraps - it never scrolls sideways, because a
   horizontal scroller clips the third one mid-word on a phone and that reads
   as a broken layout rather than as "there is more over here". */
.herobadges{display:flex;flex-wrap:wrap;gap:7px;margin-top:11px}
.herobadge{display:inline-flex;align-items:center;gap:7px;background:var(--panel);
  border:1px solid var(--line);border-radius:11px;padding:7px 11px;
  font-size:.76rem;font-weight:600}
.herobadge .bic{font-size:.9rem;line-height:1}

/* ===========================================================================
   MY INFO - the hub
   =========================================================================== */
/* Four small buttons in one row put four labels at 0.84rem on a 320px phone
   and truncated three of them. A hub is a list: full width, one per line, and
   the label has room to say what the screen is for. */
.jump{display:flex;flex-direction:column;gap:9px;margin-bottom:16px}
.jumpbtn{display:flex;align-items:center;gap:12px;width:100%;text-align:left;
  padding:13px 15px;border-radius:14px;border:1px solid var(--line);
  background:var(--panel);color:var(--ink);font-weight:600;font-size:.95rem;
  cursor:pointer}
.jumpbtn:hover{border-color:var(--accent)}
/* One row in Settings is an <a>, because the privacy policy is a plain page
   rather than a route. Without this it arrives underlined and in link colour,
   which makes one row in a list of ten look like a different kind of thing. */
a.jumpbtn{text-decoration:none;color:var(--ink)}
a.jumpbtn .jname{text-decoration:none}
.jumpbtn .jico{flex:0 0 auto;width:34px;height:34px;border-radius:11px;
  background:var(--accent-soft);color:var(--accent);display:grid;place-items:center}
.jumpbtn .jico svg{width:18px;height:18px;fill:none;stroke:currentColor;
  stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.jumpbtn .jmain{flex:1;min-width:0}
.jumpbtn .jname{display:block}
.jumpbtn .jmeta{display:block;color:var(--muted);font-size:.78rem;font-weight:400}
.jumpbtn .jchev{flex:0 0 auto;color:var(--muted)}
/* THE CURRENT VALUE, on the right.
   This one slot is what turned Settings from a scroll into an index. The old
   screen was thirteen open cards, so the only way to find out what the timer
   was set to was to scroll until the timer card came past. A row that says
   "Timer  No timer >" answers the question without being opened, which is the
   entire job of a settings index. */
.jumpbtn .jval{flex:0 0 auto;color:var(--muted);font-size:.85rem;font-weight:500;
  white-space:nowrap;font-variant-numeric:tabular-nums;max-width:45%;
  overflow:hidden;text-overflow:ellipsis}
/* The glyph takes a semantic tint where the row means something - the queue
   badge, the destructive one. Tokens, never literals. */
.jumpbtn .jico.plain{background:var(--panel-2);color:var(--muted)}
.jumpbtn .jico.good{background:var(--good-soft);color:var(--good)}
.jumpbtn .jico.warn{background:var(--warn-soft);color:var(--warn)}
.jumpbtn .jico.bad{background:var(--bad-soft);color:var(--bad)}
.jumpbtn.danger .jname{color:var(--bad)}

/* --- the loading state ---------------------------------------------------- */
/* Three generic rows, shown between a tap and the data arriving. See
   skeleton() in app.js for why it is generic and why it lives in setChrome.
   The shimmer is a transform on a pseudo-element rather than an animated
   background-position, so it runs on the compositor and does not repaint the
   row underneath it. */
.skel{display:flex;flex-direction:column;gap:8px}
.skrow{display:flex;align-items:center;gap:12px;background:var(--panel);
  border:1px solid var(--line);border-radius:12px;padding:12px 14px;min-height:56px}
.skt{flex:1;min-width:0}
.skb{display:block;border-radius:6px;background:var(--panel-2);position:relative;
  overflow:hidden}
.skb.tile{flex:0 0 auto;width:32px;height:32px;border-radius:9px}
.skb.l1{width:62%;height:11px}
.skb.l2{width:38%;height:9px;margin-top:8px}
.skb.l1.w2{width:48%} .skb.l2.w2{width:29%}
.skb.l1.w3{width:55%} .skb.l2.w3{width:34%}
/* --shimmer, not a literal. A white highlight at 55% is right on a light
   surface and a glare on a dark one, which is the same failure the token rule
   exists to prevent - and taking the token means the dark theme needs no
   override here at all. */
.skb::after{content:"";position:absolute;inset:0;
  background:linear-gradient(90deg,transparent,var(--shimmer),transparent);
  animation:skshimmer 1.4s ease-in-out infinite}
@keyframes skshimmer{0%{transform:translateX(-100%)}100%{transform:translateX(100%)}}
/* A student who has asked for less motion gets a plain grey block, which still
   says "something is coming" without anything moving. */
@media (prefers-reduced-motion:reduce){.skb::after{animation:none}}

/* NO SWITCH COMPONENT HERE ON PURPOSE.
   One was written for this redesign and then deleted unused. Nothing in this
   app is genuinely binary: reminders have five states and four of them need
   different words and different remedies, the timer is three, the theme is
   three. A switch would have to lie about at least one of them. */

/* ===========================================================================
   A PICKER, WHERE A SEGMENTED CONTROL WOULD GROW
   =========================================================================== */
/* Day / Week / Month is a closed set of three and stays one row forever, so it
   stays segmented. The course filter is open-ended - five courses today, more
   as subjects are added - and a segmented row that wraps onto a second and
   third line stops reading as one control and starts reading as another
   filter. A <select> is one row at any length, and it is also the native
   control, so it gets the phone's own picker for free. */
.picker{display:flex;align-items:center;gap:12px;width:100%;
  background:var(--panel);border:1px solid var(--line);border-radius:12px;
  padding:10px 14px;margin-top:8px}
.picker > span{flex:0 0 auto;font-size:.68rem;letter-spacing:.09em;
  text-transform:uppercase;font-weight:700;color:var(--muted)}
.picker select{flex:1;min-width:0;border:0;background:none;color:var(--ink);
  font:inherit;font-weight:600;text-align:right;cursor:pointer;
  padding:2px 0;-webkit-appearance:none;appearance:none}
.picker select:focus{outline:none}
.picker:focus-within{border-color:var(--accent);box-shadow:0 0 0 3px var(--accent-soft)}
.picker .pchev{flex:0 0 auto;color:var(--muted);font-size:.8rem}

/* ===========================================================================
   THE WEEKLY RECAP
   Shown on a Monday or Tuesday and never otherwise, which is how it stayed an
   unstyled hook for so long - four days out of seven there is nothing to see.
   Listed as a known gap in docs/07-state.md; this closes it.
   =========================================================================== */
/* .recap is on the CARD - `<div class="card recap">` - not on the wrapper it
   is appended into. Written as `.recap .card` these rules matched nothing at
   all, which is a quieter version of the same bug they were meant to fix. */
.card.recap{background:var(--accent-soft);border-color:transparent}
.card.recap > h2{font-size:1.02rem}
.card.recap .stats{gap:8px}
.card.recap .stat{background:var(--panel)}

/* ===========================================================================
   HOVER AND PRESS
   The module rows already did this - `.module:hover` changes its border - and
   it is the thing that makes that list feel live. Nothing else on the screen
   did, so a solid button was the only control that gave nothing back.

   A border colour is enough for anything that has a border. A solid button has
   none to change, so it lifts instead: brightness plus a shadow, both of which
   work on a dark ground where a darker border would vanish.

   Two rules about HOW, both of which matter here:
     - everything animates a filter, a shadow or a transform, never a layout
       property, so a hover can never reflow the page under the pointer;
     - it is all inside `@media (hover:hover)`. On a phone there is no pointer,
       and a :hover rule without this guard sticks after a tap - the button you
       just pressed stays lit while you read the next screen.
   =========================================================================== */
.btn,.seg button,.segbtn,.module,.jumpbtn,.goalbtn,.length,.qnum,
button.continue,.opt,.bandrow,.srow2{
  transition:border-color .15s ease,color .15s ease,filter .15s ease,
             box-shadow .15s ease,transform .1s ease}

@media (hover:hover){
  .btn:not(:disabled):hover{filter:brightness(1.07);
    box-shadow:0 3px 12px -6px rgb(0 0 0 / 45%)}
  /* The ghost variants have a border, so they use it and drop the lift. */
  .btn.ghost:not(:disabled):hover{filter:none;box-shadow:none;
    border-color:var(--accent);color:var(--ink)}
  .btn.ghost.danger:not(:disabled):hover{border-color:var(--bad);color:var(--bad)}
  .seg button:not([aria-pressed="true"]):hover{border-color:var(--accent);color:var(--ink)}
  .seg button[aria-pressed="true"]:hover{filter:brightness(1.07)}
  /* .continue already has an accent border in its base rule, so changing the
     border colour on hover is a no-op - measured as "NO CHANGE". A tinted card
     needs a shadow, which reads on either ground. */
  button.continue:hover{box-shadow:0 4px 14px -8px rgb(0 0 0 / 45%)}
  .card.resume{transition:box-shadow .15s ease}
  .segbtn.on:hover{filter:brightness(1.05)}
}

/* Press is for everyone, pointer or thumb - it is the feedback that says the
   tap landed, which matters more on a phone than hover ever does. */
.btn:not(:disabled):active,.seg button:active,.segbtn:active,
.module:not(:disabled):active,.jumpbtn:active,button.continue:active{
  transform:translateY(1px)}

@media (prefers-reduced-motion:reduce){
  .btn,.seg button,.segbtn,.module,.jumpbtn,.goalbtn,.length,.qnum,
  button.continue,.opt,.bandrow,.srow2{transition:none}
  .btn:not(:disabled):active,.seg button:active,.segbtn:active,
  .module:not(:disabled):active,.jumpbtn:active,button.continue:active{
    transform:none}
}

/* An armed destructive button. The label has already changed to a question;
   this makes the state visible at a glance so a second tap is a decision
   rather than a reflex. */
.btn.ghost.danger.armed{background:var(--bad-soft);border-color:var(--bad);
  font-weight:600}

/* ===========================================================================
   A QUIET BLOCK BUTTON
   Previous / Next / Submit paper / Modules / Skip question were all .linkbtn -
   accent, bold, underlined - so the controls that move you through a paper
   looked like links in a sentence rather than things to press, and the only
   real target was the text itself. This is a proper button with a proper hit
   area that still does not compete with the question: it takes the inset
   surface rather than the accent, so it sits quietly on the card it is on.
   .linkbtn stays for what it is actually for - a link inside a sentence, like
   "Practise it" in the weakest-module line.
   =========================================================================== */
.btn.quiet{background:var(--panel-2);color:var(--ink);border:1px solid transparent;
  font-weight:600}
.btn.quiet:disabled{opacity:.4}
@media (hover:hover){
  .btn.quiet:not(:disabled):hover{filter:none;box-shadow:none;
    border-color:var(--accent);background:var(--accent-soft);color:var(--accent)}
}
/* The one that ends the paper carries a little more weight than the two that
   move within it, without becoming the loudest thing on the screen. */
.btn.quiet.strong{background:var(--accent-soft);color:var(--accent)}

/* A button and the field above it. Both the create-group form and the
   follow-a-student form put a .btn directly after an <input> with nothing
   between, so the control sat flush against the box you had just typed in.
   One rule rather than a margin on each, because there will be more forms. */
input + .btn,input + .btn.quiet,select + .btn{margin-top:12px}

/* The marked paper: make it obvious a row opens.
   .fold already has a rotating caret and .qresult never did, so a screen made
   entirely of expandable rows gave no sign that any of them were. */
.qresult summary::after{content:"";flex:0 0 auto;width:7px;height:7px;
  margin-left:2px;border-right:2px solid var(--muted);
  border-bottom:2px solid var(--muted);transform:rotate(-45deg);
  transition:transform .15s ease}
.qresult[open] summary::after{transform:rotate(45deg)}
@media (prefers-reduced-motion:reduce){.qresult summary::after{transition:none}}

/* Framing for what is inside an opened row. Your answer and the solution are
   two different kinds of thing and were two undifferentiated blocks of text. */
.qbody .yoursbox,.qbody .sol{border-left:3px solid var(--line);
  border-radius:0 9px 9px 0;padding:10px 12px;margin-top:12px}
.qbody .yoursbox{border-left-color:var(--muted);background:var(--panel-2)}
.qbody .sol{border-left-color:var(--accent);background:var(--accent-soft)}

/* The nested module rows on an overseer report.
   They were `.module.static.sub`, and `.sub` is the muted-PARAGRAPH class -
   `margin:0 0 18px`, a smaller font and a muted colour - so every nested row
   silently picked up an 18px bottom margin inside a flex column that was
   already gapping them. Another two-meanings-one-name, like .seg. Renamed to
   .nested, which means only this.

   width:auto IS THE FIX, AND THE REST OF THIS COMMENT USED TO BE WRONG.
   It claimed the right edge was "flush with the subject rows above". It was
   not: .module sets width:100%, so margin-left:14px SHIFTS the row right
   without narrowing it, and the nested row overhung its parent by exactly
   that 14px - measured at right:399 against the subject row at right:385.
   Charlie reported it as the modules ending too close to the right side.
   width:auto lets the row stretch to the container minus its margins, which
   is what makes it read as a child rather than as a row that missed. */
.module.static.nested{margin-left:14px;margin-right:0;width:auto;
  background:var(--panel-2);border-style:dashed}

/* The password field and its reveal. The button is inside the box, so the
   input keeps the full width of the card on a phone; the right padding on the
   input is what stops the text running under it. */
.pwwrap{position:relative}
.pwwrap input{padding-right:46px}
.pweye{position:absolute;right:6px;top:50%;transform:translateY(-50%);
  width:34px;height:34px;display:grid;place-items:center;cursor:pointer;
  background:none;border:0;color:var(--muted);border-radius:8px}
.pweye svg{width:19px;height:19px;fill:none;stroke:currentColor;stroke-width:1.8;
  stroke-linecap:round;stroke-linejoin:round}
.pweye:hover{color:var(--ink)}
.pweye[aria-pressed="true"]{color:var(--accent)}

/* "I am not the student", on the course picker.
   Two lines, full width, above the list - a parent should be able to leave
   this screen without reading a list of subjects first. Ghost rather than
   solid so it does not outrank Save for a student, who is the common case. */
.btn.notstudent{flex-direction:column;align-items:flex-start;gap:2px;
  text-align:left;padding:13px 15px}
.notstudent .nsmain{font-weight:600;color:var(--ink)}
.notstudent .nsmeta{font-size:.78rem;color:var(--muted);font-weight:400}
@media (hover:hover){
  .btn.notstudent:not(:disabled):hover .nsmain{color:var(--accent)}
}

/* The submit wrapper. `pointer-events:none` is what lets the click through to
   the wrapper so the app can say WHY the button is dead; without it a disabled
   button eats the event and nothing can answer. */
.submitwrap{display:block}
.submitwrap .btn:disabled{pointer-events:none}

/* The fallback box for a copy the browser would not let us make. Monospace
   because what goes in it is uuids and template ids being read back by a
   person, and a proportional font makes those harder to check. */
.copyout{width:100%;margin-top:12px;font-family:var(--mono);font-size:.78rem;
  line-height:1.5;padding:10px 12px;border-radius:10px;border:1px solid var(--line);
  background:var(--panel-2);color:var(--ink);resize:vertical}
.flagactions .btn.quiet{width:100%;margin-top:8px}

/* ===========================================================================
   THE PAPER REVIEW OVERVIEW  (B3)
   Every question, its state, and a way into each. Built out of tokens only -
   no literal colours - so it follows the theme like everything else.
   =========================================================================== */
.revlist{list-style:none;margin:14px 0 16px;padding:0;display:flex;
  flex-direction:column;gap:6px}
.revrow{display:flex;align-items:center;gap:11px;width:100%;text-align:left;
  padding:9px 11px;border:1px solid var(--line);border-radius:9px;
  background:var(--panel);color:var(--ink);cursor:pointer;font:inherit}
.revrow:hover{border-color:var(--accent)}
.revrow .qn{flex:none;width:1.9rem;height:1.9rem;border-radius:7px;
  display:flex;align-items:center;justify-content:center;font-weight:700;
  font-size:.82rem;background:var(--panel-2);color:var(--muted);
  font-variant-numeric:tabular-nums}
.revrow.done .qn{background:var(--accent-soft);color:var(--accent)}
.revwhat{flex:1;min-width:0;display:flex;flex-direction:column;gap:1px;
  overflow:hidden}
.revwhat b{font-size:.88rem;font-weight:600;overflow:hidden;
  text-overflow:ellipsis;white-space:nowrap}
.revwhat .sub{font-size:.76rem;margin:0}
.revrow .qflag{flex:none;color:var(--warn)}
/* The state is the thing being scanned for, so it is the one bit of the row
   that never wraps and never shrinks. */
.revstate{flex:none;font-size:.76rem;font-weight:600;white-space:nowrap;
  color:var(--muted)}
.revrow.done .revstate{color:var(--good)}
.revrow.blank .revstate{color:var(--warn)}

/* The answer is in the field above this, not behind a button - so this says
   only that it is recorded. Quiet on purpose: it is a receipt, not a verdict,
   and a paper shows no verdicts. */
.savednote{margin:8px 0 0;font-size:.8rem;color:var(--good)}
.savednote[hidden]{display:none}

/* ===========================================================================
   THE ADMIN ANSWER TAB  (backlog D1, migration 016)
   Folded shut by default and styled quietly on purpose: this is the one thing
   in the app that shows a stored answer, and it should look like something you
   opened deliberately rather than part of the card.
   =========================================================================== */
.answertab{margin-top:12px;border:1px solid var(--line);border-radius:9px;
  background:var(--panel-2)}
.answertab summary{padding:9px 12px;cursor:pointer;font-size:.85rem;
  font-weight:600;color:var(--muted);list-style:none;display:flex;
  align-items:center;gap:8px}
.answertab summary::-webkit-details-marker{display:none}
/* The same rotating chevron the marked paper rows use, so an expandable thing
   looks expandable in both places. */
.answertab summary::after{content:"";flex:0 0 auto;width:7px;height:7px;
  margin-left:auto;border-right:2px solid currentColor;
  border-bottom:2px solid currentColor;transform:rotate(-45deg);
  transition:transform .15s ease}
.answertab[open] summary::after{transform:rotate(45deg)}
.answertab[open] summary{color:var(--ink);border-bottom:1px solid var(--line)}
.answerbody{padding:11px 12px}
.answerbody h4{margin:12px 0 5px;font-size:.82rem;letter-spacing:.03em;
  text-transform:uppercase;color:var(--muted)}
.answerbody h4:first-child{margin-top:0}
/* Monospace and scrollable in its own right: a stored answer is JSON, it can
   be wider than a phone, and the page must not scroll sideways because of it. */
.answerjson{margin:0;padding:9px 10px;border-radius:7px;background:var(--panel);
  border:1px solid var(--line);font-family:ui-monospace,SFMono-Regular,
  "SF Mono",Menlo,Consolas,monospace;font-size:.78rem;line-height:1.5;
  overflow-x:auto;white-space:pre}

/* ===========================================================================
   THE CALCULATOR  (backlog D2/F5)
   Lives in the same drawer as the formula sheet, so it inherits .refdrawer for
   its frame and only styles what is inside. Tokens only - no literal colours.
   =========================================================================== */
.calc{padding:11px 12px 13px}
.calcbar{display:flex;align-items:center;gap:8px;margin-bottom:9px}
/* The angle mode is on screen permanently and not in a menu. A calculator
   quietly in radians turns every trigonometry question in Standard into a
   wrong answer that looks plausible. */
.calcmode{font-family:var(--display);font-weight:700;font-size:.76rem;
  letter-spacing:.06em;padding:4px 10px;border-radius:999px;cursor:pointer;
  border:1px solid var(--accent);background:var(--accent-soft);
  color:var(--accent)}
.calcmode[aria-pressed="true"]{background:var(--warn-soft);
  border-color:var(--warn);color:var(--warn)}
.calcmem{margin-left:2px;font-size:.7rem;font-weight:700;letter-spacing:.08em;
  color:var(--accent);align-self:center}
.calcmem[hidden]{display:none}
.calcmr,.calcmc{margin-left:auto;font-size:.72rem;font-weight:600;
  padding:4px 9px;border-radius:7px;cursor:pointer;
  border:1px solid var(--line);background:var(--panel);color:var(--muted)}
.calcmc{margin-left:0}
.calcmr:hover,.calcmc:hover{color:var(--ink);border-color:var(--muted)}

.calcscreen{background:var(--panel-2);border:1px solid var(--line);
  border-radius:9px;padding:8px 11px 9px;margin-bottom:10px;text-align:right}
/* The expression can be longer than the box and must scroll rather than push
   the drawer wider than the phone. */
.calcexpr{font-family:ui-monospace,SFMono-Regular,"SF Mono",Menlo,Consolas,monospace;
  font-size:.8rem;color:var(--muted);min-height:1.2em;overflow-x:auto;
  white-space:nowrap}
.calcval{font-family:var(--display);font-size:1.5rem;font-weight:600;
  line-height:1.25;font-variant-numeric:tabular-nums;overflow-x:auto;
  white-space:nowrap;margin-top:2px}
/* An error is a sentence, not a number, so it wraps and shrinks. */
.calcval.bad{font-size:.86rem;font-weight:500;color:var(--bad);
  white-space:normal;font-family:inherit}

.calcpad{display:grid;grid-template-columns:repeat(5,1fr);gap:6px}
.calckey{padding:11px 2px;border-radius:9px;cursor:pointer;font:inherit;
  font-size:.84rem;font-weight:600;border:1px solid var(--line);
  background:var(--panel);color:var(--ink);
  /* 44px is the smallest comfortable touch target, and this is a grid of
     forty of them on a phone. */
  min-height:44px}
.calckey:hover{border-color:var(--accent)}
.calckey:active{background:var(--accent-soft)}
.calckey.num{font-size:1rem;background:var(--panel-2)}
.calckey.eq{background:var(--accent);border-color:var(--accent);
  color:var(--on-solid)}
.calckey.clear{color:var(--bad);border-color:var(--bad-soft)}
.calcnote{margin:9px 0 0;font-size:.72rem;color:var(--muted);text-align:center}

/* Four columns under about 360px: five 44px keys plus gaps do not fit a 320px
   phone, and a key running off the edge is a key that cannot be pressed. */
@media (max-width:359px){
  .calcpad{grid-template-columns:repeat(4,1fr)}
}

/* ===========================================================================
   THE TWO WAYS INTO A COURSE  (backlog F1)
   Two tiles on one row, each folding its own options open underneath. Only one
   is ever open, so the choice stays visible above whatever it opened.
   =========================================================================== */
.ways{display:flex;gap:10px}
.way{flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:2px;
  align-items:flex-start;text-align:left;padding:12px 13px;cursor:pointer;
  border:1px solid var(--line);border-radius:12px;background:var(--panel-2);
  color:var(--ink);font:inherit}
.way:hover{border-color:var(--accent)}
/* The open one takes the accent so the panel below it has an owner. */
.way.open{background:var(--accent-soft);border-color:var(--accent);
  color:var(--accent)}
.wayname{font-weight:600;font-size:.95rem;letter-spacing:-.01em}
/* Wraps rather than truncates: on a 320px phone "One question at a time" does
   not fit on one line, and an ellipsis on the only explanation a control has
   is worse than a second line. */
.waysub{font-size:.74rem;color:var(--muted);line-height:1.3}
.way.open .waysub{color:var(--accent)}

/* The panel one of them opens. Indented by nothing and framed by nothing: it
   belongs to the tile above it, and a second border inside a card is noise. */
.waypanel{margin-top:12px}
.waypanel[hidden]{display:none}
.waypanel .segnote{margin-top:8px}
.waypanel .btn{margin-top:12px}

/* The offline sandbox notice.
   A distinct name rather than a generic .note, because class names in this
   file are one namespace and a generic one has already collided here - .seg
   meant a container in one place and an item in another, and eight pickers
   silently inherited padding and a border from it.

   Accent-soft rather than a warning colour: this is the app being honest
   about what offline practice is, not telling the student off. */
.sandboxnote{background:var(--accent-soft);border:1px solid var(--accent);
  border-radius:12px;padding:12px 13px;margin:0 0 16px;
  font-size:.84rem;line-height:1.45;color:var(--ink)}
.sandboxnote strong{color:var(--accent)}
