/* Ryska — "instrument panel for one word at a time."
 *
 * One rule governs colour: COLOUR MEANS COMFORT. The five-step ramp is the only
 * chromatic thing on the page; ground, card, rules and type are all warm greys.
 * There is deliberately no red/green grade pair — the pair I tried measured
 * ΔE 3.8 under deuteranopia, and rather than mitigate it with labels the whole
 * idea was dropped. Direction is carried by position, wording, and by the gauge
 * previewing where the word will land.
 */

@font-face {
  font-family: "Plex Serif";
  src: url("/fonts/IBMPlexSerif-Bold.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Plex Serif";
  src: url("/fonts/IBMPlexSerif-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Plex Mono";
  src: url("/fonts/IBMPlexMono-Regular.woff2") format("woff2");
  font-weight: 400; font-style: normal; font-display: swap;
}
@font-face {
  font-family: "Plex Mono";
  src: url("/fonts/IBMPlexMono-Bold.woff2") format("woff2");
  font-weight: 700; font-style: normal; font-display: swap;
}

/* Greys are measured, not picked. Everything below is a contrast ratio against
 * --card (#1f1c16), which is the surface most of this text sits on:
 *
 *   ink   14.3:1    dim 7.2:1    faint 4.6:1    rule 1.9:1    lift 1.4:1
 *
 * The first pass had faint at 2.34:1 and rule at 1.24:1 — roughly half the text
 * on the card (part of speech, hints, gauge labels, the example translation) sat
 * under 3:1, and the card border was invisible enough that the card did not read
 * as a card. --card itself is frozen: the comfort ramp is validated against it.
 * The ground was darkened instead, which is the only lever left for separating
 * two near-black surfaces, and the edge is now carried by the border. */
/* Two grounds, one design. The dark theme is the original and its greys are
   the measured ones; the light theme was built to the same ratios rather than
   by eye — ink 15.4:1 against its card where dark is 14.8, dim 7.7 against
   7.2, faint 5.0 against 4.7. Anything reading a colour reads it from here,
   including the comfort ramp in app.js and dash.js, so a theme is one block.

   The ramp reverses between themes on purpose. On dark, comfort runs deep blue
   to pale; on light, pale to deep. Stated as a rule it is the same rule both
   times: MORE CONTRAST AGAINST THE GROUND MEANS SAFER. Reusing the dark ramp
   on paper would put the best-known words in the palest, least visible ink. */
:root {
  --ground: #0f0d09;
  --card:   #1f1c16;
  --card-flip: #332c20;      /* the whole card changes when it is turned over */
  --lift:   #3a3325;
  --ink:    #f4efe4;
  --dim:    #b3a894;
  --faint:  #8f8675;
  --rule:   #514936;

  --c1: #256abf;
  --c2: #3987e5;
  --c3: #6da7ec;
  --c4: #9ec5f4;
  --c5: #cde2fb;

  /* The one non-comfort use of the hue, and it gets its own name so it stops
     borrowing a ramp step whose value flips with the theme. It marks the line
     that is *sounding right now* — the same blue, but chosen per ground for
     legibility as running text: 6.8:1 on the card here, 5.5:1 on the flipped
     card. Straight --c3 on paper would have been 2.6:1. */
  --live: #6da7ec;

  --serif: "Plex Serif", Georgia, "Times New Roman", serif;
  --mono: "Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;

  /* One type scale, so the card stops inventing a size per element. The
     headword and the example sentences are measured per string (fitSize,
     fitLine) and are the only two things allowed off it. */
  --t-label: 12px;   /* mono: part of speech, hints, gauge labels */
  --t-small: 14px;   /* mono, one step under the body size */
  --t-body:  16px;   /* inflected forms, sentence translations */
  --t-lead:  29px;   /* the gloss — the answer, so it answers at a size that
                        belongs to the headword rather than to the labels */

  --safe-t: env(safe-area-inset-top, 0px);
  --safe-b: env(safe-area-inset-bottom, 0px);
}

:root[data-theme="light"] {
  --ground: #e9e5db;
  --card:   #fbf9f4;
  --card-flip: #e7dfc9;
  --lift:   #ded8c9;
  --ink:    #23201a;
  --dim:    #55503f;
  --faint:  #726b58;
  --rule:   #cdc6b4;

  --c1: #cfe0f7;
  --c2: #9dc0ea;
  --c3: #6d9edb;
  --c4: #3f79c4;
  --c5: #1d549e;

  --live: #1d549e;   /* 7.1:1 on the card, 5.6:1 on the flipped card */
}

* { box-sizing: border-box; -webkit-tap-highlight-color: transparent; }

html, body {
  margin: 0;
  height: 100%;
  background: var(--ground);
  color: var(--ink);
  font-family: var(--mono);
  font-size: 15px;
  line-height: 1.45;
  -webkit-font-smoothing: antialiased;
}

body.study {
  height: 100dvh;
  display: grid;
  grid-template-rows: auto 1fr auto;
  overflow: hidden;
  overscroll-behavior: none;
}

.hidden { display: none !important; }

/* ============ top bar ============ */

.bar {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: calc(12px + var(--safe-t)) 18px 8px;
}

/* Two registers of mono, and the split is the whole anti-clutter rule:
 * UPPERCASE TRACKED is reserved for things you operate — the gauge and the three
 * buttons. Everything incidental (how many cards are left, the part of speech,
 * the tap hint) is lowercase. Before this, all twelve text zones on the study
 * screen were set in tracked caps, so nothing was subordinate to anything. */
.bar-left {
  display: flex;
  align-items: baseline;
  gap: 6px;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .01em;
  color: var(--faint);
}
.bar-left b { font-size: 15px; font-weight: 700; color: var(--dim); }

.bar-right { display: flex; gap: 10px; }

/* 44px, which is the smallest target iOS considers reliable — these were 34px
   and sit in the top corner where the thumb is least accurate. The icons are
   drawn (see index.html), so the box can grow without the glyph growing with it:
   20px of art inside 44px of target is the ratio that stops the bar looking like
   two big empty boxes. */
.icon-btn {
  display: grid;
  place-items: center;
  width: 44px; height: 44px;
  border: 1px solid var(--rule);
  border-radius: 3px;
  background: transparent;
  color: var(--dim);
  text-decoration: none;
  cursor: pointer;
  font-family: var(--mono);
  padding: 0;
}
.icon-btn svg { width: 20px; height: 20px; display: block; }
.icon-btn:active { background: var(--lift); color: var(--ink); }

/* ============ card ============ */

/* The card is capped rather than filling every pixel: at full height a bare
   headword floated in ~450px of dead space. The cap was 500px, from before the
   card carried two sentences and a three-line answer — with that much on it, a
   500px card left ~85px of empty ground between its foot and the gauge and put
   the answer two thirds of the way up the screen. The content now earns the
   height, so the cap is only a ceiling for tall screens. */
/* #stage must stay stretched — it is the 1fr row of body.study, which is what
   gives `height: min(100%, …)` below something definite to resolve against.
   Centring via align-content here collapses the row to content height and the
   absolutely-positioned cards flatten to a sliver. Centre the deck, not the row. */
#stage { position: relative; display: grid; padding: 4px 18px 6px; min-height: 0; }
#deck {
  position: relative;
  width: 100%;
  height: min(100%, 620px);
  align-self: center;
}

.card {
  position: absolute;
  inset: 0;
  background: var(--card);
  border: 1px solid var(--rule);
  border-radius: 4px;
  display: grid;
  grid-template-rows: auto 1fr;   /* top row + body; the foot row is gone */
  padding: 16px 18px 22px;
  touch-action: none;
  user-select: none;
  will-change: transform;
  overflow: hidden;
}

/* The stack reads as a deck: each card behind peeks below by a hairline slab. */
.card[data-depth="1"] { transform: translateY(7px) scale(.985); }
.card[data-depth="2"] { transform: translateY(14px) scale(.97); }
.card[data-depth="3"] { transform: translateY(21px) scale(.955); }
.card[data-depth="1"] .card-inner,
.card[data-depth="2"] .card-inner,
.card[data-depth="3"] .card-inner { opacity: 0; }
.card.settling { transition: transform .26s cubic-bezier(.2,.8,.3,1); }
.card.gone { transition: transform .3s ease-out, opacity .3s ease-out; opacity: 0; }

.card-inner { display: contents; }

/* One metadata row, not two. The card used to carry four labels — part of
 * speech, rank, "nytt ord"/"trygghet N", and "N× sedd" — of which the rank and
 * the seen-count are dashboard facts and the comfort is already the big number
 * on the gauge two inches below. What is left is what the word itself needs. */
.card-top {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .01em;
  color: var(--faint);
}
.card-top .tag { color: var(--dim); }

.card-body {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0;
  min-height: 0;
  text-align: center;
}

/* Size is measured and set per word in app.js (fitSize). Never break inside a
   word: chopping "промышленность" into "промы/шленно/сть" is exactly the thing a
   vocabulary card must not do. Only a real space may wrap. */
.ru {
  font-family: var(--serif);
  font-weight: 700;
  font-size: 72px;
  line-height: 1.06;
  letter-spacing: -.015em;
  overflow-wrap: normal;
  word-break: normal;
  hyphens: none;
  max-width: 100%;
}

/* A turned card changes ground. Not a border and not an inset panel — the whole
   surface, which is the one signal you cannot miss at arm's length or in the
   corner of your eye mid-swipe. Said with value rather than hue, because colour
   on this page means comfort and no step of that ramp is borrowed. */
.card.flipped {
  background: var(--card-flip);
  border-color: var(--rule);
}

/* The reveal is a downward disclosure under a rule, not a fade in place —
   and the slot is always reserved, so nothing jumps when it appears. */
/* The answer sits BELOW the sentences now, not between them and the word. Its
   height is still reserved while veiled so nothing jumps on reveal — but at the
   bottom of the card that reserved space reads as margin, whereas in the middle
   it read as a hole with a rule drawn over it. */
/* margin-top:auto, and the tap hint below has one too — so the card's free
   space is split in two rather than piling up under the last line. The answer
   lands in the lower third with real margin above and below it instead of
   sitting tight under the sentences with 150px of nothing beneath. */
.answer {
  margin-top: auto;
  padding-top: 0;
  border-top: 0;                /* no rule: empty while veiled, a well once not */
  width: 100%;
  display: grid;
  gap: 6px;
  min-height: 80px;
  align-content: start;
}
.answer.veiled > * { opacity: 0; }
.answer > * { transition: opacity .22s ease-out; }

.gloss { font-family: var(--serif); font-weight: 400; font-size: var(--t-lead); line-height: 1.3; }

/* The inflected forms, between the gloss and the example. A wrapped row, not a
   stacked table: with an example sentence below it, three stacked rows push the
   card past its own height and .card is overflow:hidden. The label is mono
   lowercase (incidental, like the part of speech) and sits at --faint; the form
   is serif at --dim, one step below the gloss it belongs to. */
.forms {
  margin-top: 10px;
  padding-top: 9px;
  border-top: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  gap: 2px 14px;
  text-align: left;
  font-family: var(--serif);
  font-size: var(--t-body);
  line-height: 1.3;
  color: var(--dim);
}
.forms .lbl {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .04em;
  color: var(--faint);
  margin-right: 5px;
}

/* The context sentences, on the front of the card and below the answer slot.
   Russian is visible from the start — it is the question, not the answer — and
   only the translation under each line is withheld. It keeps its space while
   hidden (visibility, not display) so revealing does not shunt the card.
   The target word is marked by weight and full ink, not by hue, which on this
   page is reserved for comfort. Same no-mid-word-break rule as the headword. */
.ctx {
  margin-top: 18px;
  padding-top: 0;               /* no rule under the headword: the size step
                                   between 58px and 18px already separates them */
  display: grid;
  gap: 6px;
  text-align: left;
  width: 100%;                  /* the 300px cap forced a wrap fitLine cannot beat */
}
.ctx-item { display: grid; gap: 3px; }

/* The whole sentence is the play button, not the speaker glyph beside it.
   A 20px glyph inside a line of running text is a target you miss about as
   often as you hit — and a miss lands on the card, which reveals the answer
   you were still trying to work out. Bordered so it reads as pressable before
   you press it. */
.say.say-row {
  display: flex;
  align-items: center;
  gap: 8px;
  width: 100%;
  text-align: left;
  border: 1px solid var(--rule);
  border-radius: 3px;
  background: transparent;
  color: inherit;
  padding: 9px 10px;
  margin: 0;
  cursor: pointer;
  font: inherit;
}
.say-row.plain {                 /* no clip for this sentence: same box, not a button */
  display: flex; align-items: center; width: 100%; text-align: left;
  border: 1px solid var(--rule); border-radius: 3px; padding: 9px 10px;
  cursor: default;
}
.say.say-row:active { background: var(--lift); }
.say.say-row svg { width: 17px; height: 17px; flex: 0 0 auto; display: block; color: var(--faint); }

/* Sounding now = the words go blue, not the box around them. A lit border draws
   a rectangle you have to interpret; lit text is the thing you are following
   while you listen, and it sits where your eye already is. */
.say.say-row.playing .ctx-ru,
.say.say-row.playing .ctx-ru b,
.say.say-row.playing svg { color: var(--live); }

.ctx-ru {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--serif);
  font-size: 17px;              /* measured per sentence by fitLine in app.js */
  line-height: 1.3;
  color: var(--dim);
  overflow-wrap: normal;
  word-break: normal;
  hyphens: none;
}
.ctx-ru b { font-weight: 700; color: var(--ink); }
.ctx-tr {
  padding-left: 11px;           /* row border + padding, so the two lines align */
  font-family: var(--serif);
  font-size: var(--t-body);
  line-height: 1.4;
  color: var(--faint);
  transition: opacity .22s ease-out;
}
/* Reserved, not removed: display:none would let the card resize on reveal and
   the whole point of a fixed slot is that nothing moves under your thumb. */
.veiled-line { visibility: hidden; opacity: 0; }

/* Speaker buttons. Quiet by default and lit while playing — audio is an aid
   here, not the subject, so it must not compete with the word it belongs to.
   36px of target inside a 20px footprint via the padding, because it sits close
   to the headword and a bigger box would push the card's type around. */
.say {
  border: 0;
  background: transparent;
  color: var(--faint);
  padding: 8px;
  margin: 0;
  cursor: pointer;
  line-height: 0;
  border-radius: 3px;
  touch-action: manipulation;
  transition: color .15s;
}
.say svg { width: 20px; height: 20px; display: block; }
.say:active { background: var(--lift); }
.say.playing { color: var(--live); }

/* The headword's button sits under the word, centred with it. */
/* Word and speaker in one target. They were two things to aim at, and the small
   one was the one that mattered — so the word is the button now, glyph included. */
.say-word, .say.say-word {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 12px;
  width: 100%;
  padding: 4px 6px;
  margin: 0;
  border: 0;
  background: transparent;
  color: inherit;
  font: inherit;
  cursor: pointer;
  border-radius: 4px;
}
.say-word.plain { cursor: default; }
.say.say-word:active { background: var(--lift); }
.say.say-word svg { width: 22px; height: 22px; flex: 0 0 auto; display: block; color: var(--faint); }
.say.say-word.playing .ru,
.say.say-word.playing svg { color: var(--live); }

/* The sentence's button rides at the end of the sentence, so it reads as
   belonging to that line rather than to the card. */
.ctx-ru .say { padding: 2px 5px; vertical-align: -5px; }
.ctx-ru .say svg { width: 16px; height: 16px; }

/* Rides directly under the answer, which is itself pushed to the foot of the
   card by its own auto margin — so the hint still ends up at the bottom, but
   the free space above it is one gap between question and answer rather than
   two thin ones that leave the answer floating in the middle. */
.tap-hint {
  font-family: var(--mono);
  font-size: var(--t-label);
  letter-spacing: .01em;
  color: var(--faint);
  padding-top: 18px;
  transition: opacity .2s;
}

/* Drag verdict: the number the gauge is about to land on. Lives in #stage, not
   in the card, so it holds still while the card slides out from under it. */
.stamp {
  position: absolute;
  z-index: 5;
  top: 50%;
  margin-top: -46px;
  font-family: var(--mono);
  font-weight: 700;
  font-size: 15px;
  letter-spacing: .12em;
  text-transform: uppercase;
  opacity: 0;
  pointer-events: none;
  display: grid;
  justify-items: center;
  gap: 2px;
}
.stamp .n { font-size: 40px; letter-spacing: 0; line-height: 1; }
.stamp-yes { right: 26px; }
.stamp-no  { left: 26px; }

/* ============ the gauge ============ */

#controls {
  padding: 10px 18px calc(12px + var(--safe-b));
  display: grid;
  gap: 9px;
}

.gauge-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .09em;
  text-transform: uppercase;
  color: var(--faint);
}
.gauge-read { display: flex; align-items: baseline; gap: 4px; }
.gauge-read .val {
  font-size: 21px;
  font-weight: 700;
  letter-spacing: 0;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
}
/* "/ 10" states the range in the one place the reading already is, which is why
   the separate 1 · 5 · 10 scale row underneath could go. */
.gauge-read .of { color: var(--faint); letter-spacing: .04em; }

/* A word you have never graded has no comfort, and the gauge used to say so
   with a bare "–". That reads as a broken readout rather than an empty one —
   the honest version names the state, at label weight because it is a sentence
   about the gauge and not a value on it. */
.gauge-read .val.unset {
  font-size: 12px;
  font-weight: 400;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--faint);
}

.gauge {
  display: grid;
  grid-template-columns: repeat(10, 1fr);
  gap: 3px;
  height: 34px;
}

.seg {
  position: relative;
  border: 0;
  padding: 0;
  background: var(--lift);
  border-radius: 2px;
  cursor: pointer;
  transition: background .18s, transform .1s, box-shadow .18s;
}
.seg::after {                      /* engraved tick above each step */
  content: "";
  position: absolute;
  top: -6px; left: 50%;
  width: 1px; height: 3px;
  background: var(--rule);
}
.seg:nth-child(5n+1)::after { height: 5px; top: -8px; background: var(--faint); }
.seg:active { transform: scaleY(1.14); }
.seg.preview { box-shadow: inset 0 0 0 1px var(--ink); }

.actions { display: grid; grid-template-columns: 1fr 84px 1fr; gap: 8px; margin-top: 5px; }

.act {
  border: 1px solid var(--rule);
  border-radius: 3px;
  background: transparent;
  color: var(--ink);
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 14px 6px;
  cursor: pointer;
  transition: background .15s, color .15s, border-color .15s;
}
.act:active { background: var(--lift); }

/* Emphasis moves with the step you are on, so a first-timer is never guessing
   which of three equal buttons starts things. Before the reveal "Visa" is the
   lit one; after it, the two verdicts are. Nothing is hidden and nothing is
   disabled — grading a word you knew on sight, without revealing, still works
   from the buttons, the keys and the swipe. */
#controls.veiled .act-grade { color: var(--faint); }
#controls:not(.veiled) .act-flip { color: var(--faint); border-color: var(--rule); }
.act-flip { border-color: var(--dim); }

/* ============ empty state ============ */

.empty {
  display: grid;
  place-content: center;
  justify-items: center;
  text-align: center;
  gap: 14px;
  padding: 30px 24px;
  height: 100%;
}
.empty h2 { font-family: var(--serif); font-weight: 700; font-size: 27px; margin: 0; }
/* Serif, not mono: a five-line centred monospace paragraph is a wall. Mono
   earns its place on numbers and labels, not on running text. */
.empty p {
  margin: 0;
  max-width: 27ch;
  color: var(--dim);
  font-family: var(--serif);
  font-size: 15px;
  line-height: 1.55;
}
.empty .marks { display: flex; gap: 5px; }
.empty .marks i { width: 22px; height: 3px; background: var(--rule); border-radius: 2px; }
.empty .marks i:nth-child(-n+3) { background: var(--c3); }

.ghost {
  border: 1px solid var(--rule);
  background: transparent;
  color: var(--ink);
  border-radius: 3px;
  padding: 12px 20px;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .08em;
  text-transform: uppercase;
  cursor: pointer;
}

/* ============ toast ============ */

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(150px + var(--safe-b));
  transform: translate(-50%, 10px);
  background: var(--lift);
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 9px 14px;
  font-family: var(--mono);
  font-size: 12px;
  letter-spacing: .02em;
  opacity: 0;
  pointer-events: none;
  transition: opacity .18s, transform .18s;
  z-index: 40;
  white-space: nowrap;
  max-width: calc(100vw - 32px);
  overflow: hidden;
  text-overflow: ellipsis;
}
.toast.show { opacity: 1; transform: translate(-50%, 0); }

/* ============ settings sheet ============ */

.sheet { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: grid; align-items: end; z-index: 50; }
.sheet-inner {
  background: var(--card);
  border-top: 1px solid var(--rule);
  border-radius: 4px 4px 0 0;
  padding: 22px 20px calc(24px + var(--safe-b));
  display: grid;
  gap: 15px;
}
.sheet h3 {
  margin: 0;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--dim);
}
.row { display: flex; align-items: center; justify-content: space-between; gap: 16px; font-size: 14px; }
/* A row whose control is a block, not a single field: the label sits on its
   own line above it rather than fighting a ten-checkbox grid for width. */
.row.col { display: block; }
.row.col > span { display: block; margin-bottom: 8px; }
.row input[type="number"] {
  width: 82px;
  background: var(--ground);
  border: 1px solid var(--rule);
  color: var(--ink);
  border-radius: 3px;
  padding: 9px;
  font-family: var(--mono);
  font-size: 15px;
  text-align: right;
}
/* Two-state control in the settings sheet. Mono and boxed like the three
   buttons under the gauge, because it is a thing you operate. */
.seg { display: flex; gap: 0; border: 1px solid var(--rule); border-radius: 3px; overflow: hidden; }
.seg button {
  border: 0;
  background: transparent;
  color: var(--faint);
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .08em;
  text-transform: uppercase;
  padding: 9px 14px;
  cursor: pointer;
}
.seg button + button { border-left: 1px solid var(--rule); }
.seg button.on { background: var(--lift); color: var(--ink); }

.row input[type="checkbox"] { width: 20px; height: 20px; accent-color: var(--c2); }

/* ============ first-run coach ============ */

/* Shown once, on the first visit, because nothing else on the screen says what
   a swipe means or what the number is for — the app was legible only to the
   person who built it. Dismissal is a localStorage flag rather than a row in
   the settings table: it is cosmetic, and re-showing it after a browser wipe
   costs one tap. "Så funkar det" in the ⚙ sheet brings it back. */
.coach { position: fixed; inset: 0; background: rgba(8,7,5,.86); display: grid; align-items: end; z-index: 60; }
.coach-inner {
  background: var(--card);
  border-top: 1px solid var(--rule);
  border-radius: 4px 4px 0 0;
  padding: 26px 22px calc(26px + var(--safe-b));
  display: grid;
  gap: 20px;
}
.coach h2 { margin: 0; font-family: var(--serif); font-weight: 700; font-size: 25px; }
.coach ol { margin: 0; padding: 0; list-style: none; display: grid; gap: 17px; counter-reset: step; }
.coach li {
  display: grid;
  grid-template-columns: 26px 1fr;
  gap: 12px;
  align-items: baseline;
  font-family: var(--serif);
  font-size: 16px;
  line-height: 1.45;
  color: var(--dim);
}
.coach li::before {
  counter-increment: step;
  content: counter(step);
  font-family: var(--mono);
  font-size: 12px;
  font-weight: 700;
  color: var(--ink);
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 3px 0;
  text-align: center;
}
.coach li b { color: var(--ink); font-weight: 700; }
.coach .go {
  border: 1px solid var(--dim);
  background: transparent;
  color: var(--ink);
  border-radius: 3px;
  padding: 15px;
  font-family: var(--mono);
  font-size: 13px;
  letter-spacing: .1em;
  text-transform: uppercase;
  cursor: pointer;
}

/* ============ dashboard ============ */

body.dash { min-height: 100dvh; padding-bottom: calc(44px + var(--safe-b)); }
.wrap { max-width: 640px; margin: 0 auto; padding: 0 18px; }

.backlink {
  display: inline-block;
  color: var(--dim);
  text-decoration: none;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .1em;
  text-transform: uppercase;
  padding: calc(16px + var(--safe-t)) 0 18px;
}

.panel { border-top: 1px solid var(--rule); padding: 18px 0 20px; }
.panel:first-of-type { border-top: 0; padding-top: 0; }
.panel h3 {
  margin: 0;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .12em;
  text-transform: uppercase;
  color: var(--dim);
}

/* Heading and a one-value readout on the same line. Each panel used to spend a
   second line on a sentence describing its own chart; the space now carries a
   number instead, which is what the panel was for. Lowercase against the tracked
   caps of the heading, by the same two-registers rule as the study screen: the
   heading names the thing, the readout is incidental. */
.panel-head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: 12px;
  margin-bottom: 14px;
}
.panel-head .readout {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .02em;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* --- the 1000-dot field: one dot per word, in frequency order --- */
/* 40 columns, so each row is a round 40 words and the whole 1000 stays inside
   one screen-height block instead of scrolling. */
.field {
  display: grid;
  grid-template-columns: repeat(40, minmax(0, 1fr));
  gap: 2px;
}
/* An unmet word is --lift, not --rule. Rules got brighter so that borders and
   separators actually delineate, but 988 of these dots are unmet at the start
   and at rule brightness the field reads as a slab of brown with the tinted
   dots lost in it. An empty data mark is a surface, not a rule. */
.field i { width: 100%; aspect-ratio: 1; border-radius: 1px; background: var(--lift); }

.field-legend {
  display: flex;
  align-items: center;
  gap: 9px;
  margin-top: 15px;
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--faint);
}
.field-legend .ramp { display: flex; gap: 2px; }
.field-legend .ramp i { width: 13px; height: 7px; border-radius: 1px; }

/* --- the known-words tracker --- */
/* Drawn as SVG because the line has to cross the milestone rules and carry on
   as a dashed projection, which the div-and-percentage-height trick the old bar
   charts used cannot do. No library and nothing from a CDN, as everywhere else. */
#tracker { margin-bottom: 4px; }
.chart { display: block; overflow: visible; }
.chart .curve { stroke-width: 2; stroke-linejoin: round; stroke-linecap: round; }
/* The baseline is a border, so it gets --rule. The milestone rules are an axis
   you read positions against, so they get --faint — the rule that keeps the
   1000-cell field quiet is about empty *data* marks and does not apply here. */
.chart .base { stroke: var(--rule); stroke-width: 1; }
.chart .ms { stroke: var(--faint); stroke-width: 1; stroke-dasharray: 2 4; opacity: .8; }
.chart .ms.met { stroke: var(--ink); opacity: .5; stroke-dasharray: none; }
.chart .ms-label,
.chart .ax {
  font-family: var(--mono);
  font-size: 10px;
  letter-spacing: .04em;
  fill: var(--faint);
}
.chart .ms-label.met { fill: var(--ink); }
/* The break between the two x scales. It is drawn, and labelled "today",
   because an unmarked scale break is a lie about time. */
.chart .split { stroke: var(--rule); stroke-width: 1; stroke-dasharray: 3 3; }
.chart .proj { stroke: var(--faint); stroke-width: 1.5; stroke-dasharray: 4 4; }
.chart .proj-dot { stroke: var(--faint); stroke-width: 1.5; }

/* One row per milestone: the count, what it buys, and the date the projection
   puts it on. Tabular figures so the dates form a column. */
.ms-list { list-style: none; margin: 16px 0 0; padding: 0; }
.ms-list li {
  display: grid;
  grid-template-columns: 42px 1fr auto;
  align-items: baseline;
  gap: 10px;
  padding: 7px 0;
  border-top: 1px solid var(--rule);
  font-family: var(--mono);
  font-size: 11px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
}
.ms-list li:first-child { border-top: 0; }
.ms-list .n { text-align: right; }
.ms-list .l { color: var(--dim); }
.ms-list li.met .n, .ms-list li.met .l, .ms-list li.met .w { color: var(--ink); }
.ms-list .w { white-space: nowrap; }

.note {
  margin: 12px 0 0;
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.5;
  color: var(--dim);
  font-variant-numeric: tabular-nums;
  text-wrap: pretty;   /* keeps "ord" off a line of its own */
}

/* --- stat tiles --- */
.tiles { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1px; background: var(--rule); border: 1px solid var(--rule); }
.tile { background: var(--ground); padding: 15px 14px; }
.tile .k { font-family: var(--mono); font-size: 10px; letter-spacing: .1em; text-transform: uppercase; color: var(--faint); }
.tile .v { font-family: var(--mono); font-size: 27px; font-weight: 700; font-variant-numeric: tabular-nums; margin-top: 5px; line-height: 1; }
.tile .s { font-family: var(--mono); font-size: 11px; color: var(--dim); margin-top: 5px; }

table.hard { width: 100%; border-collapse: collapse; }
table.hard td { padding: 11px 0; border-top: 1px solid var(--rule); vertical-align: baseline; }
table.hard tr:first-child td { border-top: 0; }
table.hard .w { font-family: var(--serif); font-weight: 700; font-size: 19px; }
table.hard .g { color: var(--dim); font-size: 12px; margin-top: 2px; }
table.hard .c { text-align: right; white-space: nowrap; font-family: var(--mono); font-size: 11px; color: var(--faint); letter-spacing: .05em; }
table.hard .c b { display: block; font-size: 15px; color: var(--ink); font-variant-numeric: tabular-nums; margin-bottom: 2px; }


/* ============ landing page ============ */

/* Same ground, same two faces, same measured greys as the deck — a stranger's
   first screen and their tenth screen should be recognisably one thing. The
   serif does the talking here because this is the one page that is prose. */

body.start {
  min-height: 100dvh;
  padding-bottom: calc(40px + var(--safe-b));
}
.start-wrap { max-width: 620px; margin: 0 auto; padding: 0 20px; }

.hero { padding: calc(48px + var(--safe-t)) 0 8px; }
.kicker {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 0 0 18px;
}
.hero h1 {
  font-family: var(--serif);
  font-weight: 700;
  font-size: clamp(34px, 9vw, 50px);
  line-height: 1.08;
  letter-spacing: -.015em;
  margin: 0 0 20px;
  text-wrap: balance;
}
.lede {
  font-family: var(--serif);
  font-size: 18px;
  line-height: 1.5;
  color: var(--dim);
  margin: 0 0 28px;
  max-width: 46ch;
}

/* The one lit control on the page. It borrows the top of the comfort ramp,
   which is the only chromatic thing this design owns — and it is the one place
   colour is allowed to mean "go" instead of "comfort", because there is no
   comfort meter on this page for it to be confused with. */
.go {
  display: inline-block;
  font-family: var(--mono);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: .12em;
  text-transform: uppercase;
  text-decoration: none;
  color: var(--ground);
  background: var(--c5);
  padding: 15px 26px;
  border-radius: 3px;
  border: 1px solid var(--c5);
}
.go:active { opacity: .85; }
.go.quiet { background: transparent; color: var(--c5); }

.fineprint {
  font-family: var(--mono);
  font-size: 12px;
  color: var(--faint);
  margin: 16px 0 0;
}

.block { border-top: 1px solid var(--rule); margin-top: 40px; padding-top: 24px; }
.block h2 {
  margin: 0 0 16px;
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .14em;
  text-transform: uppercase;
  color: var(--dim);
}

/* The numbered marker is not decoration: these are ordered steps and the order
   is the information — step 3 makes no sense before step 2. */
.steps { list-style: none; margin: 0; padding: 0; display: grid; gap: 18px; }
.steps.tight { gap: 13px; }
.steps li {
  display: grid;
  grid-template-columns: 26px 1fr;
  gap: 12px;
  align-items: baseline;
  font-family: var(--serif);
  font-size: 16px;
  line-height: 1.5;
  color: var(--dim);
}
.steps .num {
  font-family: var(--mono);
  font-size: 11px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
  text-align: right;
}
.steps b { color: var(--ink); font-weight: 700; }
.steps i { font-style: normal; font-weight: 700; color: var(--ink); }

.tabs { display: flex; gap: 8px; margin: 0 0 18px; }
.tabs button {
  font-family: var(--mono);
  font-size: 11px;
  letter-spacing: .08em;
  text-transform: uppercase;
  color: var(--faint);
  background: transparent;
  border: 1px solid var(--rule);
  border-radius: 3px;
  padding: 9px 13px;
  cursor: pointer;
}
.tabs button.on { color: var(--ink); border-color: var(--faint); background: var(--lift); }

.foot { margin-top: 40px; padding-top: 24px; border-top: 1px solid var(--rule); }

/* ============ word-type filter, in the settings sheet ============ */

/* Ten groups, not the deck's sixteen part-of-speech tags. Nobody asks to drill
   masculine nouns but not neuter ones, and sixteen checkboxes do not fit a
   phone. Two columns, because ten single-file rows push Save off the screen. */
.types {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 2px 14px;
  margin: 4px 0 2px;
}
.types label {
  display: grid;
  grid-template-columns: 18px 1fr auto;
  align-items: center;
  gap: 9px;
  padding: 7px 0;
  font-family: var(--mono);
  font-size: 13px;
  color: var(--dim);
  cursor: pointer;
  min-height: 34px;
}
.types input { width: 17px; height: 17px; accent-color: var(--c2); margin: 0; }
.types .n {
  font-size: 11px;
  color: var(--faint);
  font-variant-numeric: tabular-nums;
}
.types label.off { color: var(--faint); }
.types label.off .n { color: var(--rule); }

/* Says what the filter is costing right now. A filter that silently holds back
   repetitions is a trap; this is the line that stops it being one. */
.types-note {
  font-family: var(--mono);
  font-size: 11px;
  line-height: 1.45;
  color: var(--faint);
  margin: 8px 0 0;
  text-wrap: pretty;
}
.types-note.warn { color: var(--live); }

@media (prefers-reduced-motion: reduce) {
  * { transition-duration: .01ms !important; animation-duration: .01ms !important; }
}

:focus-visible { outline: 2px solid var(--live); outline-offset: 2px; }
