/* ═══════════════════════════════════════════════════════════════════════════
   PATTERNS — what this application is made of, as opposed to what any
   application is made of.

   The order is the order a screen discloses itself in: a figure you can read
   at a glance, a row you can scan, an object you can open, and the reader,
   which is the whole point of the system.
   ═══════════════════════════════════════════════════════════════════════════ */

/* ── 1 · the figure ─────────────────────────────────────────────────────────
   A heavy rule, the label under it in caps, and the number under that, set
   as large as the column allows. No box, no fill, no tint.

   The label comes first on the screen and second in the markup, which is
   deliberate: a screen reader should hear the number as the answer to the
   label, and an eye should meet the label before it meets the number.

   The accent the caller passes is ignored here, and that is the design. Six
   figures in six colours is a rainbow: the colour would be standing for the
   position of the tile in the row, which is not a fact about anything. On a
   course or a credential the accent names a subject and it is kept. */

.stat {
  display: flex;
  flex-direction: column;
  padding-top: var(--sp-3);
  border-top: var(--rule-heavy) solid var(--rule-ink);
}
.stat b {
  order: 2;
  font: 700 clamp(var(--t-32), 6vw, var(--t-44))/.95 var(--font-display);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-tighter);
}
/* The label reserves two lines whether it needs them or not. Six figures in
   a row of four wrap onto two rows, and a one-line label beside a two-line
   one puts the two numbers on different baselines — which in a row of
   figures reads as a misprint. */
.stat span {
  order: 1;
  display: block;
  min-height: 2.6em;
  margin-bottom: var(--sp-2);
  font: 700 var(--t-11)/1.3 var(--font-ui);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-2);
}
.stat small {
  order: 3;
  display: block;
  margin-top: var(--sp-2);
  font-size: var(--t-12);
  color: var(--ink-3);
}

/* The meter. A square track and a square fill, always anchored at the left —
   a progress bar that floats is a bar chart with one value and no axis. */
.bar {
  height: 8px;
  background: var(--paper-2);
  overflow: hidden;
}
.bar > i {
  display: block;
  height: 100%;
  background: var(--accent, var(--ink));
  transition: width .4s var(--ease);
}
.bar.thin { height: 4px; }

/* ── 2 · the row ────────────────────────────────────────────────────────────
   A list is rows separated by rules, not a stack of cards separated by air.
   Nothing floats, the left edges align down the whole column, and the eye
   can run the list without stepping over a border every 60px.

   The whole row is the hit target — on a tablet, a 38px avatar is not a
   button. */

.list { display: flex; flex-direction: column; border-bottom: var(--rule-hair) solid var(--hairline); }

.item {
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  width: 100%;
  text-align: left;
  padding: var(--sp-3) var(--sp-2);
  min-height: 60px;
  border-top: var(--rule-hair) solid var(--hairline);
  background: transparent;
  color: inherit;
  transition: background var(--fast) var(--ease);
}
.item:hover { background: var(--paper-2); }
.item b { display: block; font-size: var(--t-15); font-weight: 700; }
.item small { display: block; font-size: var(--t-12); color: var(--ink-3); }
.item > svg:last-child { color: var(--ink-3); }

/* The list header rule. A list that opens a section sits under the section's
   own heavy rule, so its first hairline would double up. */
.section-head + .list { border-top: 0; }
.section-head + .list .item:first-child { border-top: 0; }

/* ── 3 · the course ─────────────────────────────────────────────────────────
   A plate, a rule, and the words. The plate is the only place a photograph
   or a saturated field appears, so everything under it stays black on white
   and lets the image carry.

   A course with no cover gets its code reversed out of a field of its own
   accent — a plate rather than a placeholder. It is the one piece of the
   interface that looks like a poster, and it is the piece a learner is
   choosing between four of. */

.course {
  display: flex;
  flex-direction: column;
  overflow: hidden;
  border: var(--rule-hair) solid var(--hairline);
  background: var(--paper);
  color: inherit;
  transition: border-color var(--fast) var(--ease);
}
.course:hover { border-color: var(--ink); }

.course .plate {
  aspect-ratio: 16 / 9;
  background: var(--paper-2);
  border-bottom: var(--rule) solid var(--rule-ink);
  overflow: hidden;
}
.course .plate img { width: 100%; height: 100%; object-fit: cover; display: block; }
/* A course with no photograph gets a band, not an empty 16:9 field. Holding
   a picture's worth of space for a picture that does not exist is how a list
   of twelve courses becomes twelve screens of scrolling — and a solid colour
   field that size is decoration standing where information should be. */
.course .plate.blank {
  aspect-ratio: auto;
  display: flex;
  align-items: center;
  min-height: 3.5rem;
  padding: var(--sp-2) var(--sp-4);
  background: var(--accent, var(--ink));
  border-bottom-color: var(--accent, var(--ink));
}
.course .plate.blank span {
  font: 600 var(--t-20)/1 var(--font-mono);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: #fff;
}

.course .body { flex: 1; display: flex; flex-direction: column; gap: var(--sp-2); padding: var(--sp-4); }
.course .body h3 { font-size: var(--t-17); }
.course .meta {
  display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-3);
  font: 700 var(--t-11)/1.4 var(--font-ui);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-3);
}
.course .meta .mono { letter-spacing: .06em; }
.course .foot { margin-top: auto; padding-top: var(--sp-3); display: flex; flex-direction: column; gap: var(--sp-2); }
.course .pct {
  display: flex; justify-content: space-between; align-items: center; gap: var(--sp-2);
  font-size: var(--t-12); color: var(--ink-2);
  font-variant-numeric: tabular-nums;
}

/* ── 4 · the credential ─────────────────────────────────────────────────────
   The object the whole system exists to issue. A document: white, ruled,
   with the issuing mark stamped square in the corner and the folio set in
   the code face. It prints as what it looks like on screen, which is the
   entire reason this design was chosen for this system.

   Earned and locked are the same object. Only the rule and the stamp change
   — a locked credential is a promise, not an absence. */

.cred {
  display: flex;
  flex-direction: column;
  padding: var(--sp-5);
  border: var(--rule-hair) solid var(--hairline);
  border-top: var(--rule-heavy) solid var(--hairline);
  background: var(--paper);
}
.cred.earned { border-color: var(--rule-ink); border-top-color: var(--accent, var(--brand)); }
.cred.locked { color: var(--ink-2); }
.cred.locked h3 { color: var(--ink-2); }
.cred h3 { font-size: var(--t-20); margin-bottom: var(--sp-2); }
.cred .rule { height: var(--rule-hair); background: var(--hairline); margin: var(--sp-4) 0 var(--sp-3); }

/* The stamp. A square of the credential's own accent with its subject set
   into it, which is why `sealToken` takes the last non-numeric segment of
   the code — otherwise every seal on the wall reads "01". */
.cred .stamp {
  width: 48px; height: 48px;
  margin-bottom: var(--sp-4);
  display: grid; place-items: center;
  background: var(--accent, var(--brand));
  color: #fff;
  font: 600 var(--t-13)/1 var(--font-mono);
  letter-spacing: .06em;
}
.cred.locked .stamp {
  background: var(--paper-2);
  color: var(--ink-3);
  box-shadow: inset 0 0 0 var(--rule-hair) var(--hairline);
}

.cred .folio {
  font: 600 var(--t-12)/1 var(--font-mono);
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--ink);
}
.cred .parts {
  display: flex; flex-direction: column; gap: var(--sp-2);
  margin-top: var(--sp-3);
  font-size: var(--t-13); color: var(--ink-2);
}
.cred .parts div { display: flex; gap: var(--sp-2); align-items: flex-start; }
.cred .parts svg { width: 15px; height: 15px; margin-top: 3px; color: var(--ink-3); }

/* ── 5 · the badge ──────────────────────────────────────────────────────────
   What a hotel earns, as opposed to what a person earns. A ruled row rather
   than a card, because a hotel has a dozen of them and a wall of cards is a
   wall. */

.badge {
  display: flex;
  align-items: flex-start;
  gap: var(--sp-4);
  padding: var(--sp-4) 0;
  border-top: var(--rule-hair) solid var(--hairline);
  background: transparent;
}
.badge.locked { color: var(--ink-2); }
.badge .disc {
  width: 40px; height: 40px; flex: none;
  display: grid; place-items: center;
  background: var(--paper-2);
  color: var(--ink-3);
}
.badge.earned .disc { background: var(--accent, var(--green)); color: #fff; }
.badge .disc svg { width: 21px; height: 21px; }
.badge b { display: block; font-size: var(--t-15); font-weight: 700; margin-bottom: 2px; }
.badge p { font-size: var(--t-13); color: var(--ink-3); margin: 0; }
.badge .gauge { margin-top: var(--sp-3); max-width: 22rem; }

/* ── 6 · the table ──────────────────────────────────────────────────────────
   Reports. The one pattern that was already Swiss before anything else was:
   a ruled grid of aligned figures. Column heads in caps under a heavy rule,
   rows on hairlines, numerals tabular and flush right.

   On a phone every row becomes its own block with the column name beside
   each cell, because a table scrolled sideways on a phone is a table nobody
   reads. */

.table-wrap {
  overflow-x: auto;
  -webkit-overflow-scrolling: touch;
  border-bottom: var(--rule-hair) solid var(--hairline);
}

/* `width: auto` with a `min-width` floor: columns take the width their
   content needs and the wrapper scrolls when the sum exceeds the page. The
   alternative — forcing the table to the column width — squeezes a hotel's
   name into four stacked lines and turns a twelve-row report into a page of
   ragged blocks. A report is a grid of figures or it is not a report. */
table { width: auto; min-width: 100%; border-collapse: collapse; font-size: var(--t-13); }
th, td { padding: var(--sp-3) var(--sp-5) var(--sp-3) 0; text-align: left; white-space: nowrap; }
th:last-child, td:last-child { padding-right: 0; }
th {
  position: sticky; top: 0;
  font: 700 var(--t-11)/1.3 var(--font-ui);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-2);
  background: var(--paper);
  border-bottom: var(--rule) solid var(--rule-ink);
  vertical-align: bottom;
}
td { border-top: var(--rule-hair) solid var(--hairline-2); }
tbody tr:first-child td { border-top: 0; }
th.n, td.n { text-align: right; font-variant-numeric: tabular-nums; }
tbody tr:hover { background: var(--paper-2); }
tr.total td { font-weight: 700; border-top: var(--rule) solid var(--rule-ink); background: transparent; }

@media (max-width: 52rem) {
  table.responsive thead { display: none; }
  table.responsive tr { display: block; border-top: var(--rule-hair) solid var(--hairline); padding: var(--sp-3) 0; }
  table.responsive tr:first-child { border-top: 0; }
  table.responsive td {
    display: flex; justify-content: space-between; gap: var(--sp-4);
    border: 0; padding: 2px 0; text-align: right;
    white-space: normal;
  }
  table.responsive td::before {
    content: attr(data-label);
    font: 700 var(--t-11)/1.6 var(--font-ui);
    letter-spacing: var(--track-caps);
    text-transform: uppercase;
    color: var(--ink-3);
    text-align: left;
  }
  table.responsive tr.total { border-top: var(--rule) solid var(--rule-ink); }
}

/* ── 7 · charts ─────────────────────────────────────────────────────────────
   Each figure is a ruled block, drawn straight onto the paper. The grid and
   the axes are furniture: they are set in the lightest ink the page has and
   must never compete with the data. */

.viz-grid { display: grid; gap: clamp(var(--sp-6), 3vw, var(--sp-8)); grid-template-columns: 1fr; }
@media (min-width: 62rem) { .viz-grid { grid-template-columns: 1fr 1fr; } }
.viz.wide { grid-column: 1 / -1; }

.viz {
  margin: 0;
  min-width: 0;
  padding-top: var(--sp-3);
  border-top: var(--rule) solid var(--rule-ink);
}
.viz figcaption { display: flex; flex-direction: column; gap: 2px; margin-bottom: var(--sp-5); }
.viz figcaption b {
  font: 700 var(--t-11)/1.4 var(--font-ui);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink);
}
.viz figcaption span { font-size: var(--t-12); color: var(--ink-3); }

.viz .plot { width: 100%; height: auto; display: block; overflow: visible; }
.viz .plot.flat { height: 26px; }

.viz .grid line { stroke: var(--hairline-2); stroke-width: 1; }
.viz .grid text, .viz .xlab text {
  font: 500 10px/1 var(--font-ui);
  fill: var(--ink-3);
  font-variant-numeric: tabular-nums;
}
.viz .track { fill: var(--paper-2); }
.viz text.cat { font: 500 12px/1 var(--font-ui); fill: var(--ink-2); }
.viz text.val { font: 700 12px/1 var(--font-ui); fill: var(--ink); font-variant-numeric: tabular-nums; }
.viz text.val .drop { fill: var(--ink-3); font-weight: 500; }
/* A value that had to move inside its bar sits on the series colour, so it
   takes the ink that reads against that rather than against the paper. */
.viz text.val.inside { fill: #fff; }
.viz .hit rect { fill: transparent; }
.viz [data-tip] { cursor: default; }
.viz g[data-tip]:hover rect:not(.track) { opacity: .8; }

.viz .legend { display: flex; flex-wrap: wrap; gap: var(--sp-1) var(--sp-4); margin-top: var(--sp-4); }
.viz .legend span {
  display: inline-flex; align-items: center; gap: var(--sp-2);
  font: 700 var(--t-11)/1.4 var(--font-ui);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-2);
}
.viz .legend i { width: 10px; height: 10px; flex: none; }
.viz .legend b { font-variant-numeric: tabular-nums; letter-spacing: 0; }

/* The data is always available as a table, not only as a picture. */
.viz-table { margin-top: var(--sp-4); }
.viz-table summary,
.candidates summary {
  font: 700 var(--t-11)/1.4 var(--font-ui);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-3);
  cursor: pointer;
  padding: var(--sp-2) 0;
  list-style: none;
}
.viz-table summary::-webkit-details-marker,
.candidates summary::-webkit-details-marker { display: none; }
.viz-table summary::before,
.candidates summary::before { content: '+  '; }
.viz-table[open] summary::before,
.candidates[open] summary::before { content: '–  '; }
.viz-table summary:hover,
.candidates summary:hover { color: var(--ink); }
.viz-table .table-wrap { max-height: 16rem; overflow-y: auto; margin-top: var(--sp-2); }

.viz-tip {
  position: fixed;
  top: 0; left: 0;
  z-index: 400;
  pointer-events: none;
  max-width: 18rem;
  padding: var(--sp-2) var(--sp-3);
  background: var(--ink);
  color: var(--paper);
  font: 500 var(--t-12)/1.4 var(--font-ui);
  opacity: 0;
  transition: opacity var(--fast) var(--ease);
}
.viz-tip.on { opacity: 1; }

.candidates { margin-top: var(--sp-5); padding-top: var(--sp-3); border-top: var(--rule-hair) solid var(--hairline); }

/* ── 8 · the door ───────────────────────────────────────────────────────────
   One column, flush left, on plain paper. Whoever is here has exactly one
   thing to do, so there is nothing else on the screen — and the column is
   not centred inside a box, because a box would be a second thing. */

.gate {
  display: grid;
  place-items: center;
  min-height: 100dvh;
  padding: var(--sp-8) var(--sp-5) calc(var(--sp-10) + env(safe-area-inset-bottom));
}
.gate .card {
  width: 100%;
  max-width: 26rem;
  text-align: left;
}
.gate .seal-big {
  width: 56px; height: 56px;
  margin: 0 0 var(--sp-5);
  display: grid; place-items: center;
  background: var(--brand);
  color: #fff;
  font: 700 var(--t-32)/1 var(--font-display);
  letter-spacing: var(--track-tighter);
}
.gate h1 { margin-bottom: var(--sp-3); font-size: clamp(var(--t-24), 6vw, var(--t-32)); }
.gate .lead {
  color: var(--ink-2);
  font-size: var(--t-15);
  margin-bottom: var(--sp-6);
  padding-bottom: var(--sp-6);
  border-bottom: var(--rule-heavy) solid var(--rule-ink);
}
.gate form { text-align: left; }

/* Which face of the system to come in through. It changes what signing in
   means, so it belongs above the door rather than buried inside. */
.gate .faces {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  margin-bottom: var(--sp-3);
  border: var(--rule-hair) solid var(--hairline);
}
.gate .faces button {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: var(--sp-1);
  min-height: 56px;
  padding: var(--sp-2) var(--sp-1);
  border-right: var(--rule-hair) solid var(--hairline);
  font: 700 var(--t-11)/1.2 var(--font-ui);
  letter-spacing: .02em;
  color: var(--ink-3);
  transition: background var(--fast) var(--ease), color var(--fast) var(--ease);
}
.gate .faces button:last-child { border-right: 0; }
.gate .faces button:hover { background: var(--paper-2); color: var(--ink); }
.gate .faces button[aria-pressed="true"] { background: var(--ink); color: var(--paper); }
.gate .faces-note { font-size: var(--t-12); color: var(--ink-3); margin-bottom: var(--sp-6); min-height: 2.6em; }

.gate .gsi { display: flex; justify-content: flex-start; min-height: 44px; margin-bottom: var(--sp-4); }
.gate .gsi > div { max-width: 100%; }
.gate .signing { display: flex; align-items: center; gap: var(--sp-2); font-size: var(--t-13); color: var(--ink-3); margin-bottom: var(--sp-3); }

.gate .or {
  display: flex; align-items: center; gap: var(--sp-3);
  margin: var(--sp-5) 0;
  font: 700 var(--t-11)/1 var(--font-ui);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-3);
}
.gate .or::after { content: ''; flex: 1; height: var(--rule-hair); background: var(--hairline); }

.gate .err {
  margin-top: var(--sp-4);
  padding: var(--sp-3);
  border-left: var(--rule-heavy) solid var(--red);
  background: var(--paper-2);
  color: var(--ink);
  font-size: var(--t-13);
  text-align: left;
}
.gate .note { margin-top: var(--sp-6); font-size: var(--t-12); color: var(--ink-3); line-height: var(--lh-body); }
.gate .hint {
  display: flex; flex-direction: column; gap: var(--sp-1);
  margin-top: var(--sp-6); padding-top: var(--sp-4);
  border-top: var(--rule-hair) solid var(--hairline);
  text-align: left;
}
.gate .hint button {
  font: 400 var(--t-12)/1.6 var(--font-mono);
  color: var(--ink-3);
  text-align: left;
  padding: var(--sp-1) 0;
}
.gate .hint button:hover { color: var(--ink); text-decoration: underline; }

/* ── 9 · the reader ─────────────────────────────────────────────────────────
   A spread is one content frame: the plate on the recto, the words on the
   verso, and a single rule down the gutter. The turn is a cut, not a fold —
   a page curling in perspective is an illustration of a book rather than a
   way of reading one, and the frames here are slides.

   `.reader` covers the viewport and paints the paper itself: it replaces the
   application underneath rather than floating over it. The bar above the
   spread and the navigation below it are chrome and take the chrome grey, so
   the sheet between them is the only white on the screen — which is what a
   sheet is. */

.reader {
  position: fixed;
  inset: 0;
  z-index: 100;
  display: flex;
  flex-direction: column;
  background: var(--paper);
}

.reader-bar {
  flex: none;
  display: flex;
  align-items: center;
  gap: var(--sp-4);
  min-height: 56px;
  padding: var(--sp-2) var(--reader-gutter);
  border-bottom: var(--rule-hair) solid var(--hairline);
  background: var(--chrome);
}
.reader-bar b { display: block; font: 700 var(--t-15)/1.25 var(--font-display); letter-spacing: var(--track-tight); }
.reader-bar .where {
  font: 700 var(--t-11)/1.4 var(--font-ui);
  letter-spacing: var(--track-caps);
  text-transform: uppercase;
  color: var(--ink-3);
}
.reader-bar .gauge { flex: none; width: clamp(4rem, 18vw, 9rem); }

/* The narration meter: a rule that fills while the frame speaks. It writes
   to a CSS variable rather than to the store, because a playhead moving
   through Redux would re-render the interface four times a second. */
.meter { flex: none; height: var(--rule); background: var(--paper-2); overflow: hidden; }
.meter > i {
  display: block; height: 100%;
  width: calc(var(--heard, 0) * 100%);
  background: var(--accent, var(--ink));
  transition: width .2s linear;
}

.stage {
  flex: 1;
  min-height: 0;
  display: grid;
  place-items: stretch;
  padding: 0;
}

.book {
  position: relative;
  width: 100%; height: 100%;
  display: grid;
  grid-template-rows: minmax(0, 38%) minmax(0, 1fr);
  overflow: hidden;
  background: var(--paper);
}
@media (min-width: 52rem) {
  .book { grid-template-rows: none; grid-template-columns: 1fr 1fr; }
  /* The gutter: one hairline, because that is what separates two columns of
     a grid. A soft shaded fold would be a picture of a book. */
  .book::after {
    content: '';
    position: absolute;
    left: 50%; top: 0; bottom: 0;
    width: var(--rule-hair);
    transform: translateX(-50%);
    pointer-events: none;
    z-index: 3;
    background: var(--hairline);
  }
}

/* A frame with no plate is one page, not a spread with an empty half — and
   it is a page, so the words start at the top of it on the same left edge
   as the bar above them. Centring them in the height of a 1024px tablet is
   what makes a short frame look like a mistake rather than a slide: the air
   belongs below the text, not wrapped around it. */
.book.solo { grid-template-rows: minmax(0, 1fr); }
@media (min-width: 52rem) {
  .book.solo { grid-template-columns: minmax(0, 1fr); }
  .book.solo::after { display: none; }

  /* A frame with no plate has the whole page and about forty words to fill
     it with. Set at reading size those forty words occupy a fifth of a
     tablet and the rest is a blank sheet — so on the page where the words
     are the only thing, the words are set as the thing. This is the only
     place the type scale is allowed out of its usual range, and it is the
     one place a lámina is genuinely a lámina. */
  .book.solo .leaf.verso > * { max-width: 46rem; }
  .book.solo .leaf.verso h2 { font-size: clamp(var(--t-32), 4.6vw, 3.25rem); }
  .book.solo .leaf.verso .key { font-size: clamp(var(--t-20), 2.4vw, 1.6rem); line-height: 1.32; }
  .book.solo .leaf.verso li { font-size: var(--t-17); }
}

.leaf { position: relative; min-height: 0; overflow: hidden; }
.leaf.recto { background: var(--paper-2); order: -1; border-bottom: var(--rule-hair) solid var(--hairline); }
@media (min-width: 52rem) { .leaf.recto { order: 0; border-bottom: 0; } }
.leaf.recto img, .leaf.recto video { width: 100%; height: 100%; object-fit: cover; display: block; }
.leaf.recto .figure { width: 100%; height: 100%; object-fit: contain; padding: 7%; background: var(--paper); }

.leaf.verso {
  padding: clamp(var(--sp-6), 5vh, var(--sp-12)) var(--reader-gutter);
  display: flex; flex-direction: column; justify-content: flex-start; gap: var(--sp-4);
  overflow-y: auto;
  -webkit-overflow-scrolling: touch;
}
.leaf.verso > * { max-width: var(--measure); }
.leaf.verso .eyebrow { color: var(--accent, var(--ink-3)); }
.leaf.verso h2 { font-size: clamp(var(--t-24), 4.2vw, var(--t-32)); }

/* The key line — the one sentence the frame exists to say. Set larger than
   the body and hung on a rule in the frame's own accent. */
.leaf.verso .key {
  font: 400 clamp(var(--t-17), 3vw, 1.25rem)/1.4 var(--font-ui);
  letter-spacing: var(--track-tight);
  color: var(--ink);
  padding-left: var(--sp-4);
  border-left: var(--rule-heavy) solid var(--accent, var(--ink));
}
.leaf.verso ul { margin: 0; padding-left: var(--sp-5); display: flex; flex-direction: column; gap: var(--sp-2); }
.leaf.verso li { font-size: var(--t-15); color: var(--ink-2); }

/* Questions. A quiz explains itself once answered; a test says nothing. */
.q { display: flex; flex-direction: column; gap: var(--sp-2); }
.q .opt {
  display: flex; align-items: flex-start; gap: var(--sp-3);
  width: 100%; text-align: left;
  min-height: 52px;
  padding: var(--sp-3);
  border: var(--rule-hair) solid var(--hairline);
  background: var(--paper);
  font-size: var(--t-15);
  line-height: 1.4;
  transition: border-color var(--fast) var(--ease), background var(--fast) var(--ease);
}
.q .opt:hover:not([disabled]) { border-color: var(--ink); }
.q .opt kbd {
  flex: none;
  width: 24px; height: 24px;
  display: grid; place-items: center;
  background: var(--paper-2);
  font: 600 var(--t-11)/1 var(--font-mono);
  color: var(--ink-2);
}
.q .opt[data-state="right"] { border-color: var(--good); box-shadow: inset 0 0 0 1px var(--good); }
.q .opt[data-state="right"] kbd { background: var(--good); color: #fff; }
.q .opt[data-state="wrong"] { border-color: var(--bad); box-shadow: inset 0 0 0 1px var(--bad); }
.q .opt[data-state="wrong"] kbd { background: var(--bad); color: #fff; }
.q .opt[data-state="muted"] { opacity: .45; }
.q .note {
  padding: var(--sp-3) var(--sp-4);
  font-size: var(--t-15);
  line-height: 1.5;
  background: var(--paper-2);
  border-left: var(--rule-heavy) solid var(--accent, var(--ink));
}

/* The turn. A cut with a short slide — enough to say which direction the
   frame went, and over before anyone has to wait for it. */
.book.turn-next .leaf.verso { animation: leaf-next .2s var(--ease) both; }
.book.turn-prev .leaf.verso { animation: leaf-prev .2s var(--ease) both; }
@keyframes leaf-next {
  from { transform: translateX(0); opacity: 1; }
  to   { transform: translateX(-16px); opacity: 0; }
}
@keyframes leaf-prev {
  from { transform: translateX(0); opacity: 1; }
  to   { transform: translateX(16px); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .book.turn-next .leaf.verso, .book.turn-prev .leaf.verso { animation: appear .15s both; }
}

.reader-nav {
  flex: none;
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  border-top: var(--rule-hair) solid var(--hairline);
  background: var(--chrome);
  padding-bottom: env(safe-area-inset-bottom);
}
.reader-nav button {
  display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 2px;
  min-height: 60px;
  padding: var(--sp-2);
  border-right: var(--rule-hair) solid var(--hairline);
  background: var(--chrome);
  transition: background var(--fast) var(--ease);
}
.reader-nav button:last-child { border-right: 0; }
.reader-nav button:hover:not(:disabled) { background: var(--chrome-2); }
.reader-nav button b { display: flex; align-items: center; gap: var(--sp-1); font: 700 var(--t-13)/1 var(--font-ui); }
.reader-nav button small { font-size: var(--t-11); color: var(--ink-3); text-align: center; padding: 0 var(--sp-1); }
.reader-nav button:disabled { opacity: .35; }
.reader-nav button.go { background: var(--ink); color: var(--paper); border-right-color: var(--ink); }
.reader-nav button.go:hover:not(:disabled) { background: var(--ink-2); }
.reader-nav button.go small { color: color-mix(in oklab, var(--paper) 72%, transparent); }

/* The verdict. */
.finish { text-align: left; max-width: var(--measure); margin: 0 auto; padding: clamp(var(--sp-6), 6vw, var(--sp-12)) var(--sp-5); }
.finish .verdict {
  width: 88px; height: 88px;
  margin: 0 0 var(--sp-5);
  display: grid; place-items: center;
  background: var(--accent, var(--good));
  color: #fff;
}
.finish .verdict svg { width: 44px; height: 44px; }
.finish .score {
  font: 700 var(--t-44)/.95 var(--font-display);
  font-variant-numeric: tabular-nums;
  letter-spacing: var(--track-tighter);
  margin: var(--sp-4) 0 var(--sp-2);
}

/* ── 10 · ephemera ──────────────────────────────────────────────────────── */

.empty {
  text-align: left;
  padding: var(--sp-5) 0 clamp(var(--sp-8), 9vw, var(--sp-16));
  border-top: var(--rule-heavy) solid var(--rule-ink);
  color: var(--ink-3);
}
.empty svg { width: 32px; height: 32px; color: var(--ink-3); margin-bottom: var(--sp-4); }
.empty b { display: block; font: 700 var(--t-20)/1.2 var(--font-display); color: var(--ink); margin-bottom: var(--sp-2); }
.empty p { font-size: var(--t-13); max-width: var(--measure); margin: 0; }

.skeleton {
  background: var(--paper-2);
  animation: pulse 1.6s ease-in-out infinite;
}
@keyframes pulse { 50% { opacity: .45 } }
@media (prefers-reduced-motion: reduce) { .skeleton { animation: none; } }

/* A square, turning. Nothing in this interface is a circle, and a busy
   indicator is not the place to make the one exception. */
.spinner {
  width: 14px; height: 14px; flex: none;
  border: var(--rule) solid currentColor;
  border-right-color: transparent;
  border-bottom-color: transparent;
  animation: spin .8s steps(8, end) infinite;
}
@keyframes spin { to { transform: rotate(360deg) } }
@media (prefers-reduced-motion: reduce) { .spinner { animation-duration: 2.4s; } }

/* A message, reversed out of ink so it cannot be mistaken for content, with
   its tone carried on a rule at the leading edge. */
.toast {
  position: fixed;
  left: 50%;
  bottom: calc(var(--tab-h) + var(--sp-4) + env(safe-area-inset-bottom));
  z-index: 300;
  transform: translateX(-50%);
  display: flex; align-items: center; gap: var(--sp-3);
  max-width: calc(100vw - var(--sp-8));
  padding: var(--sp-3) var(--sp-4);
  border-left: var(--rule-heavy) solid var(--paper);
  background: var(--ink);
  color: var(--paper);
  font: 500 var(--t-13)/1.35 var(--font-ui);
  animation: toast-up .16s var(--ease) both;
}
@media (min-width: 60rem) { .toast { bottom: var(--sp-6); } }
@keyframes toast-up { from { transform: translate(-50%, 10px); opacity: 0 } }
@media (prefers-reduced-motion: reduce) { .toast { animation: none; } }
.toast svg { width: 18px; height: 18px; }
.toast.good { border-left-color: var(--good); }
.toast.bad  { border-left-color: var(--bad); }

/* ── print ──────────────────────────────────────────────────────────────────
   A report and an expediente come off a printer as documents — and for once
   there is almost nothing to undo, because the screen was already ink on
   paper. Only the chrome has to go. */

@media print {
  .netline, .rail, .tabbar, .topbar, .toolbar, .segments, .btn, .viewing, .reader-nav { display: none !important; }
  :root { --paper: #fff; --paper-2: #f2f2f0; --ink: #000; --ink-2: #333; --ink-3: #666;
          --hairline: #999; --hairline-2: #ccc; }
  body { background: #fff; color: #000; }
  main { padding: 0; max-width: none; }
  .pane, .stat, .cred, .badge, .item, .viz, .table-wrap, .course { break-inside: avoid; }
  a[href^="#"]::after { content: ''; }
}
