/* ==========================================================================
   Reading practice — design system

   Every colour in the app comes from the tokens below and every measurement
   from the 4px step. If you need a value that isn't here, add a token rather
   than a literal, otherwise this file starts drifting again.

   Palette: aizuri — the indigo of Japanese woodblock printing — on washi
   paper. Celadon means learned or correct, ochre means due soon, clay means
   wrong or destructive. The accent is the indigo itself, not a pop colour.
   ========================================================================== */

:root {
    /* ground */
    --washi: #f4f2ec;
    --surface: #fffdf8;
    --surface-2: #edeae1;
    --surface-3: #e4e0d4;
    --hairline: #d9d5c9;

    /* ink */
    --ink: #191d2b;
    --muted: #5c6274;
    --faint: #7b8194;

    /* accent */
    --indigo: #2b4c9b;
    --indigo-hover: #1f3b7d;
    --indigo-soft: #e2e7f4;
    --indigo-line: #b9c6e6;
    --on-indigo: #fffdf8;

    /* semantic */
    --celadon: #3f7a5e;
    --celadon-soft: #e1ede6;
    --ochre: #8d6318;
    --ochre-soft: #f5ecdb;
    --clay: #a8402f;
    --clay-soft: #f5e3df;

    /* type */
    --ui: ui-sans-serif, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
        "Helvetica Neue", Arial, sans-serif;
    --read: "Iowan Old Style", "Palatino Linotype", Palatino, "Book Antiqua",
        Georgia, serif;
    --cjk: "Hiragino Mincho ProN", "Yu Mincho", YuMincho, "Songti SC",
        "Noto Serif CJK JP", serif;
    --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas,
        "Liberation Mono", monospace;

    /* step scale — everything spatial is a multiple of 4px */
    --s1: 4px;
    --s2: 8px;
    --s3: 12px;
    --s4: 16px;
    --s5: 20px;
    --s6: 24px;
    --s8: 32px;
    --s10: 40px;
    --s12: 48px;

    --radius-sm: 6px;
    --radius: 10px;
    --radius-lg: 14px;
    --radius-pill: 999px;

    --shadow-sm: 0 1px 2px rgba(25, 29, 43, .06);
    --shadow-lg: 0 8px 20px rgba(25, 29, 43, .12), 0 32px 64px -32px rgba(25, 29, 43, .4);

    --rail-w: 68px;
    --tap: 44px;

    color-scheme: light;
}

@media (prefers-color-scheme: dark) {
    :root {
        --washi: #12151f;
        --surface: #1b1f2c;
        --surface-2: #232838;
        --surface-3: #2b3145;
        --hairline: #2e3446;

        --ink: #e9eaef;
        --muted: #98a0b4;
        --faint: #7d8599;

        --indigo: #93aeee;
        --indigo-hover: #aec3f5;
        --indigo-soft: #232c45;
        --indigo-line: #3b4560;
        --on-indigo: #12151f;

        --celadon: #7fb79a;
        --celadon-soft: #1d2e27;
        --ochre: #d8a24f;
        --ochre-soft: #2e2718;
        --clay: #e08a78;
        --clay-soft: #331f1c;

        --shadow-sm: 0 1px 2px rgba(0, 0, 0, .3);
        --shadow-lg: 0 8px 20px rgba(0, 0, 0, .4), 0 32px 64px -32px rgba(0, 0, 0, .9);

        color-scheme: dark;
    }
}

/* The in-app toggle must beat the media query in both directions. */
:root[data-theme="dark"] {
    --washi: #12151f;
    --surface: #1b1f2c;
    --surface-2: #232838;
    --surface-3: #2b3145;
    --hairline: #2e3446;
    --ink: #e9eaef;
    --muted: #98a0b4;
    --faint: #7d8599;
    --indigo: #93aeee;
    --indigo-hover: #aec3f5;
    --indigo-soft: #232c45;
    --indigo-line: #3b4560;
    --on-indigo: #12151f;
    --celadon: #7fb79a;
    --celadon-soft: #1d2e27;
    --ochre: #d8a24f;
    --ochre-soft: #2e2718;
    --clay: #e08a78;
    --clay-soft: #331f1c;
    --shadow-sm: 0 1px 2px rgba(0, 0, 0, .3);
    --shadow-lg: 0 8px 20px rgba(0, 0, 0, .4), 0 32px 64px -32px rgba(0, 0, 0, .9);
    color-scheme: dark;
}

:root[data-theme="light"] {
    --washi: #f4f2ec;
    --surface: #fffdf8;
    --surface-2: #edeae1;
    --surface-3: #e4e0d4;
    --hairline: #d9d5c9;
    --ink: #191d2b;
    --muted: #5c6274;
    --faint: #7b8194;
    --indigo: #2b4c9b;
    --indigo-hover: #1f3b7d;
    --indigo-soft: #e2e7f4;
    --indigo-line: #b9c6e6;
    --on-indigo: #fffdf8;
    --celadon: #3f7a5e;
    --celadon-soft: #e1ede6;
    --ochre: #8d6318;
    --ochre-soft: #f5ecdb;
    --clay: #a8402f;
    --clay-soft: #f5e3df;
    --shadow-sm: 0 1px 2px rgba(25, 29, 43, .06);
    --shadow-lg: 0 8px 20px rgba(25, 29, 43, .12), 0 32px 64px -32px rgba(25, 29, 43, .4);
    color-scheme: light;
}

/* --------------------------------------------------------------- reset -- */

*, *::before, *::after { box-sizing: border-box; }

html { background: var(--washi); }

body {
    margin: 0;
    background: var(--washi);
    color: var(--ink);
    font-family: var(--ui);
    font-size: 15px;
    line-height: 1.55;
    -webkit-font-smoothing: antialiased;
    overscroll-behavior-y: none;
}

h1, h2, h3, h4 { margin: 0; font-weight: 600; text-wrap: balance; }
p { margin: 0; }
ul, ol { margin: 0; padding: 0; list-style: none; }
img, svg { max-width: 100%; }
a { color: var(--indigo); }

button, input, select, textarea { font: inherit; color: inherit; }

:focus-visible {
    outline: 2px solid var(--indigo);
    outline-offset: 2px;
    border-radius: var(--radius-sm);
}

.visually-hidden {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
}

.hidden { display: none !important; }
/* Components set display, which would otherwise beat the hidden attribute. */
[hidden] { display: none !important; }

@media (prefers-reduced-motion: reduce) {
    *, *::before, *::after {
        animation-duration: .01ms !important;
        animation-iteration-count: 1 !important;
        transition-duration: .01ms !important;
        scroll-behavior: auto !important;
    }
}

/* ------------------------------------------------------------ app shell -- */

.app {
    display: grid;
    grid-template-columns: var(--rail-w) minmax(0, 1fr);
    min-height: 100vh;
    min-height: 100dvh;
}

/* ------------------------------------------------------- public shell -- */

/* No rail for a visitor, so the one column is the whole width and the bar
   sits across the top of it. Rows are named because the grid is at least a
   screen tall: left to itself it shares the leftover height between the two
   of them and the bar comes out a third of the page deep. */
.app-public {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: auto minmax(0, 1fr);
}

.public-bar {
    grid-column: 1;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: var(--s2);
    padding: var(--s3) var(--s5);
    border-bottom: 1px solid var(--hairline);
    background: var(--surface);
}
.public-brand {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    font-weight: 600;
    color: var(--ink);
    text-decoration: none;
    margin-right: var(--s2);
}
.public-brand svg {
    width: 20px; height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
    color: var(--indigo);
}
.public-nav { display: flex; flex-wrap: wrap; gap: var(--s1); }
/* Pushed to the far end, and wrapping as a pair rather than one at a time. */
.public-auth { display: flex; align-items: center; gap: var(--s2); margin-left: auto; }
.public-link {
    padding: var(--s1) var(--s3);
    border-radius: var(--radius-pill);
    font-size: 13px;
    color: var(--muted);
    text-decoration: none;
}
.public-link:hover { color: var(--ink); background: var(--surface-2); }
.public-link[aria-current="page"] { color: var(--indigo); background: var(--indigo-soft); }

/* ---------------------------------------------------------- scenarios -- */

.scenario-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: var(--s3);
}
.scenario-card {
    display: flex;
    gap: var(--s4);
    padding: var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    text-decoration: none;
    color: var(--ink);
    transition: border-color .12s, background .12s;
}
.scenario-card:hover { border-color: var(--indigo-line); background: var(--indigo-soft); }
.scenario-card-icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 40px; height: 40px;
    border-radius: var(--radius);
    background: var(--indigo-soft);
    color: var(--indigo);
}
.scenario-card:hover .scenario-card-icon { background: var(--surface); }
.scenario-card-icon svg,
.scenario-brief-scene svg {
    width: 22px; height: 22px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.scenario-card-body { display: grid; gap: var(--s1); min-width: 0; }
.scenario-card-head { display: flex; align-items: center; gap: var(--s2); flex-wrap: wrap; }
.scenario-card-title { font-weight: 600; font-size: 15.5px; }
/* Carried on or done. Both quiet: the card is the thing being read, and the
   state is a note in the corner of it. */
.scenario-state { font-size: 11px; }
.scenario-state.is-open { background: var(--indigo-soft); color: var(--indigo); }
.scenario-state.is-done { background: var(--celadon-soft); color: var(--celadon); }

.scenario-card-place {
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: .04em;
    color: var(--muted);
}
.scenario-card-setting { font-size: 13px; line-height: 1.55; color: var(--muted); }
.scenario-card-goal { font-size: 12.5px; line-height: 1.5; color: var(--muted); margin-top: var(--s1); }
.scenario-card-goal b { color: var(--ink); font-weight: 600; }

/* The room on the left, the conversation on the right. */
/* One multiplier for everything the conversation is written in, so text size
   is a single number rather than a rule per element. The panel beside it does
   not scale: the room does not need to be bigger, the Japanese does. */
.scenario-stage { --scenario-zoom: 1; }
.scenario-zoom { display: flex; gap: var(--s1); }

.scenario-stage {
    flex: 1;
    display: grid;
    grid-template-columns: minmax(240px, 300px) minmax(0, 1fr);
    /* One row, no taller than the space there is. An auto row is as tall as
       the tallest thing in it, and the tallest thing in it is a vocabulary
       list of eighteen entries — which pushed the box you type in a thousand
       pixels below the fold the moment that tab was opened. Capping the row
       and letting the panel scroll inside it keeps the conversation and its
       controls where they were. */
    grid-template-rows: minmax(0, 1fr);
    gap: var(--s5);
    padding: var(--s5);
    min-height: 0;
}

.scenario-brief {
    display: grid;
    /* Whichever of the two is showing — the panel or a word's gloss — takes
       the column and scrolls itself rather than stretching it. */
    grid-template-rows: minmax(0, 1fr);
    gap: var(--s2);
    align-content: start;
    padding: var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    /* A grid item is min-content wide unless it is told otherwise, and the
       vocabulary list inside this one is unbroken Japanese phrases — long
       enough that without this the column refuses to be narrower than its
       longest phrase and takes the whole page sideways on a 320px screen. */
    min-width: 0;
    min-height: 0;
}
/* Tabs on top, the chosen one under them taking what is left. The tab bar
   stays put while the list beneath it scrolls: a list you scroll away from
   its own switch is a list you cannot get out of without scrolling back. */
.scenario-panel {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    gap: var(--s3);
    min-width: 0;
    min-height: 0;
}
.scenario-scene,
.scenario-vocab { overflow-y: auto; min-height: 0; }
/* Held to roughly what the window has room for, rather than to the column —
   the app shell is min-height:100vh and grows with whatever is put in it, so
   there is nothing further up that bounds this. Eighteen vocabulary entries
   are about 1700px, and without a cap here they push the box you type in that
   far down the page the moment the tab is opened. The subtracted 220px is the
   page head, the stage padding, the panel padding and the tab row. */
.scenario-vocab,
.scenario-gloss { max-height: calc(100vh - 220px); }
/* Where the picture goes. Until there is one this is the icon on a wash, so
   the panel still reads as a place rather than as a gap. */
.scenario-brief-scene {
    display: grid;
    place-items: center;
    height: 96px;
    border-radius: var(--radius);
    background: var(--indigo-soft);
    color: var(--indigo);
    margin-bottom: var(--s2);
}
.scenario-brief-scene svg { width: 34px; height: 34px; }
.scenario-brief-setting { font-size: 13px; line-height: 1.6; color: var(--muted); }
.scenario-brief-goal { font-size: 13px; line-height: 1.55; margin-top: var(--s2); }
.scenario-brief-goal b { font-weight: 600; }

.scenario-phrases { display: grid; gap: var(--s2); margin-top: var(--s4); }
.scenario-phrase {
    display: grid;
    gap: 1px;
    padding: var(--s2) var(--s3);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    text-align: left;
    cursor: pointer;
}
.scenario-phrase:hover { border-color: var(--indigo-line); }
.scenario-phrase-ja { font-family: var(--cjk); font-size: 13.5px; line-height: 1.5; }
.scenario-phrase-romaji { font-family: var(--mono); font-size: 10.5px; color: var(--muted); }
.scenario-phrase-meaning { font-size: 11.5px; color: var(--muted); line-height: 1.4; }

/* The words this room needs, in the column beside the conversation. Panel
   furniture rather than conversation, so these are plain pixels: A+ makes the
   Japanese being learned bigger, not the reference list next to it. */
.scenario-vocab { display: grid; gap: var(--s2); min-width: 0; }
.scenario-vocab-list {
    display: grid;
    gap: var(--s1);
    list-style: none;
    margin: 0 0 var(--s3);
    padding: 0;
    min-width: 0;
}
.scenario-vocab-item {
    display: flex;
    align-items: flex-start;
    gap: var(--s2);
    padding: var(--s2);
    border-radius: var(--radius-sm);
    background: var(--surface-2);
    min-width: 0;
}
.scenario-vocab-body { display: grid; gap: 1px; min-width: 0; text-align: left; }
/* Only the phrases are tappable as a whole — a phrase goes into the box. In a
   word row it is the word itself that is the target, and a pointer over the
   translation next to it would be promising something that is not there. */
.scenario-vocab-body[data-phrase] { cursor: pointer; }
.scenario-vocab-body[data-phrase]:hover .scenario-vocab-ja { color: var(--indigo); }
/* The word itself is a button, and a grid item stretches unless stopped —
   which would leave its underline running the width of the panel. */
.scenario-vocab-body > .word { justify-self: start; }
/* Japanese has no spaces to break at, so a long phrase would otherwise sit on
   one line and push the panel wider than its column. */
.scenario-vocab-ja {
    font-family: var(--cjk);
    font-size: 13.5px;
    line-height: 1.55;
    overflow-wrap: anywhere;
}
.scenario-vocab-romaji {
    font-family: var(--mono);
    font-size: 10.5px;
    color: var(--muted);
    overflow-wrap: anywhere;
}
.scenario-vocab-meaning { font-size: 11.5px; color: var(--muted); line-height: 1.4; }
.scenario-vocab-when { font-size: 11px; color: var(--faint); line-height: 1.4; }

.scenario-talk { display: flex; flex-direction: column; min-height: 0; gap: var(--s3); }
.scenario-thread {
    flex: 1;
    overflow-y: auto;
    display: flex;
    flex-direction: column;
    gap: var(--s4);
    padding: var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    min-height: 240px;
}

.scenario-line { display: grid; gap: var(--s1); max-width: 78%; }
.scenario-line.is-mine { margin-left: auto; justify-items: end; }
.scenario-bubble {
    display: grid;
    gap: 2px;
    padding: var(--s3) var(--s4);
    border-radius: var(--radius);
    background: var(--surface-2);
}
.scenario-line.is-mine .scenario-bubble {
    background: var(--indigo-soft);
    text-align: right;
}
.scenario-ja { font-family: var(--cjk); font-size: calc(16px * var(--scenario-zoom)); line-height: 1.6; }
.scenario-romaji { font-family: var(--mono); font-size: calc(11.5px * var(--scenario-zoom)); color: var(--muted); }
.scenario-meaning { font-size: calc(12.5px * var(--scenario-zoom)); color: var(--muted); line-height: 1.45; margin-top: 2px; }

/* The sound and the meaning of what the other side said, out of focus until
   they are asked for. A blur rather than anything that removes them: the words
   stay in the page, so a screen reader still reads them and the shape of them
   still says there is something there to check against. Blurred by the same
   multiplier as the text, or a bigger setting would read straight through it.

   Never on your own lines — you wrote those — and never in the correction
   block, which exists to be read. Neither is matched here: the correction's
   lines are .scenario-fix-romaji and .scenario-fix-meaning, and your own side
   is spelled out rather than left to the script to keep off. */
.scenario-line:not(.is-mine) .scenario-bubble.is-veiled .scenario-romaji,
.scenario-line:not(.is-mine) .scenario-bubble.is-veiled .scenario-meaning {
    filter: blur(calc(4px * var(--scenario-zoom)));
    user-select: none;
    -webkit-user-select: none;
    cursor: pointer;
}
.scenario-line:not(.is-mine) .scenario-bubble.is-veiled .scenario-romaji:hover,
.scenario-line:not(.is-mine) .scenario-bubble.is-veiled .scenario-meaning:hover {
    filter: blur(calc(3px * var(--scenario-zoom)));
}

/* Hearing the line. Small, because it sits inside a bubble that is mostly
   text — but a circle this size is under the minimum anything is meant to be
   tapped at, so on a touch screen it grows to it rather than relying on
   somebody's aim. */
.scenario-play {
    justify-self: start;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px; height: 28px;
    margin-top: var(--s1);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-pill);
    background: var(--surface);
    color: var(--muted);
    cursor: pointer;
    transition: color .12s, border-color .12s, background .12s;
}
.scenario-vocab-play {
    flex: none;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: 28px; height: 28px;
    border: 1px solid var(--hairline);
    border-radius: var(--radius-pill);
    background: var(--surface);
    color: var(--muted);
    cursor: pointer;
    transition: color .12s, border-color .12s, background .12s;
}
.scenario-play svg,
.scenario-vocab-play svg { width: 13px; height: 13px; fill: currentColor; stroke: none; }
.scenario-play:hover,
.scenario-vocab-play:hover { color: var(--indigo); border-color: var(--indigo-line); }
/* Three states, and only one line is ever in the last of them. */
.scenario-play.is-loading,
.scenario-vocab-play.is-loading { color: var(--faint); border-style: dashed; }
.scenario-play.is-playing,
.scenario-vocab-play.is-playing {
    background: var(--indigo);
    border-color: var(--indigo);
    color: var(--on-indigo);
}

@media (pointer: coarse) {
    .scenario-play, .scenario-vocab-play { width: var(--tap); height: var(--tap); }
    .scenario-play svg, .scenario-vocab-play svg { width: 16px; height: 16px; }
}

/* Beside what you said, not instead of it. */
.scenario-fix {
    display: grid;
    gap: 1px;
    padding: var(--s2) var(--s3);
    border-left: 2px solid var(--ochre);
    background: var(--ochre-soft);
    border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
    text-align: left;
    justify-self: end;
    max-width: 100%;
}
.scenario-fix-label {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ochre);
}
.scenario-fix-ja { font-family: var(--cjk); font-size: calc(14px * var(--scenario-zoom)); line-height: 1.55; }
.scenario-fix-romaji { font-family: var(--mono); font-size: calc(11px * var(--scenario-zoom)); color: var(--muted); }
.scenario-fix-meaning { font-size: calc(12.5px * var(--scenario-zoom)); line-height: 1.45; }
.scenario-fix-note {
    font-size: 12px;
    line-height: 1.45;
    color: var(--muted);
    margin-top: var(--s1);
}

/* A word in a line, tappable the way a word in a passage is. The bubbles are
   tight, so this leans on the underline rather than on padding. */
.scenario-ja .word,
.scenario-fix-ja .word {
    padding: 0 1px;
    margin: 0;
    border-bottom: 1.5px solid transparent;
}
.scenario-ja .word:hover,
.scenario-fix-ja .word:hover { background: none; border-bottom-color: var(--indigo-line); }
.scenario-ja .word.is-saved,
.scenario-fix-ja .word.is-saved { border-bottom-color: var(--celadon); }
.scenario-ja .word.is-open,
.scenario-fix-ja .word.is-open { background: var(--indigo); color: var(--on-indigo); }

/* The gloss takes the scene's column while it is open. */
/* Scrolls itself for the same reason the vocabulary list does: a word with a
   long kanji breakdown is taller than the column, and growing the column
   pushes the conversation's controls off the bottom of the page. */
.scenario-gloss { display: grid; gap: var(--s2); align-content: start; overflow-y: auto; min-height: 0; }
.scenario-gloss-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
    margin-bottom: var(--s2);
}

/* The current tab in either of this page's two rows of them — how you answer,
   and what the column beside the conversation is showing. */
.chip-tabs .chip.is-on {
    background: var(--indigo-soft);
    border-color: var(--indigo-line);
    color: var(--indigo);
    font-weight: 600;
}
.chip[disabled] { opacity: .45; cursor: not-allowed; }
.chip[disabled]:hover { border-color: var(--hairline); }

/* The microphone, where the box would be. One target, wide enough to hit
   without looking, because in voice mode it is the only thing there is to
   press. */
.scenario-mic-row { display: flex; justify-content: center; }
.scenario-mic {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s3);
    min-height: var(--tap);
    padding: 0 var(--s6);
    border: 1px solid var(--indigo-line);
    border-radius: var(--radius-pill);
    background: var(--indigo-soft);
    color: var(--indigo);
    font-size: 14px;
    font-weight: 600;
    cursor: pointer;
    transition: background .12s, border-color .12s, color .12s;
}
.scenario-mic svg {
    width: 20px; height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.scenario-mic:hover { border-color: var(--indigo); }
.scenario-mic[disabled] { opacity: .5; cursor: not-allowed; }
/* Listening, and saying so in more than words: the pulse is the only thing on
   the page that moves on its own, and it is the one moment where somebody
   needs to be sure the microphone really is on. It is stilled by the
   reduced-motion rule at the top of this file along with everything else. */
.scenario-mic.is-recording {
    border-color: var(--clay);
    background: var(--clay-soft);
    color: var(--clay);
    animation: scenario-listening 1.6s ease-in-out infinite;
}
@keyframes scenario-listening {
    50% { border-color: var(--clay); box-shadow: 0 0 0 4px var(--clay-soft); }
}

.scenario-compose { display: flex; gap: var(--s2); }
.scenario-input { flex: 1; font-family: var(--cjk); font-size: calc(15px * var(--scenario-zoom)); }
.scenario-send { flex: none; }
.scenario-hint { font-size: 12.5px; color: var(--muted); min-height: 1.2em; }

.scenario-end {
    display: grid;
    gap: var(--s3);
    justify-items: start;
    padding: var(--s5);
    border: 1px solid var(--celadon);
    border-radius: var(--radius);
    background: var(--celadon-soft);
}
.scenario-end-title { font-weight: 600; font-size: 16px; }
.scenario-end-body { font-size: 13.5px; line-height: 1.55; color: var(--muted); }

/* ------------------------------------------------------------ landing -- */

.landing { display: grid; gap: var(--s12); padding: var(--s10) var(--s5) var(--s12); }
.landing > * { width: 100%; max-width: 900px; margin-inline: auto; }

.landing-hero { display: grid; gap: var(--s4); justify-items: start; }
.landing-eyebrow {
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: .12em;
    text-transform: uppercase;
    color: var(--indigo);
}
.landing-title {
    font-size: clamp(30px, 4.6vw, 46px);
    line-height: 1.15;
    letter-spacing: -.02em;
    max-width: 18ch;
}
.landing-lede {
    font-family: var(--read);
    font-size: clamp(16px, 1.6vw, 19px);
    line-height: 1.6;
    color: var(--muted);
    max-width: 58ch;
}
.landing-actions { display: flex; flex-wrap: wrap; gap: var(--s3); margin-top: var(--s2); }
.landing-note { font-size: 12.5px; color: var(--faint); }

/* The reader's own markup, standing still. */
.landing-demo {
    display: grid;
    grid-template-columns: minmax(0, 1.4fr) minmax(260px, 1fr);
    align-items: center;
    gap: var(--s5);
    padding: var(--s6);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-lg);
    background: var(--surface);
}
.landing-demo .word { cursor: default; }
.landing-demo .word:hover { background: none; }
/* Scoped through the parent so it outranks `.word { background: none }`,
   which is declared further down the file and was winning on order. */
.landing-demo .landing-demo-word { background: var(--indigo); color: var(--on-indigo); }
.landing-demo-text { display: grid; gap: var(--s3); align-content: start; }
.landing-demo-label,
.landing-demo-hint { font-size: 12.5px; color: var(--muted); }
.landing-demo-gloss {
    display: grid;
    gap: 2px;
    align-content: start;
    padding: var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface-2);
}
.landing-demo-kanji { margin-top: var(--s4); background: var(--surface); }

.landing-steps {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: var(--s5);
}
.landing-step { display: grid; gap: var(--s2); align-content: start; }
.landing-step-n {
    font-family: var(--mono);
    font-size: 12px;
    color: var(--indigo);
}
.landing-step-title { font-size: 17px; line-height: 1.3; }
.landing-step p { font-size: 14px; line-height: 1.6; color: var(--muted); }

.landing-section-title { font-size: 22px; line-height: 1.25; }
.landing-section-sub {
    font-size: 14.5px;
    line-height: 1.6;
    color: var(--muted);
    max-width: 56ch;
    margin-top: var(--s2);
}
.landing-links {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
    gap: var(--s3);
    margin-top: var(--s5);
}
.landing-link {
    display: grid;
    gap: var(--s1);
    padding: var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    text-decoration: none;
    color: var(--ink);
    transition: border-color .12s, background .12s;
}
.landing-link:hover { border-color: var(--indigo-line); background: var(--indigo-soft); }
.landing-link-title { font-weight: 600; font-size: 15px; }
.landing-link-sub { font-size: 13px; line-height: 1.5; color: var(--muted); }

.landing-close {
    padding: var(--s8) var(--s6);
    border: 1px solid var(--indigo-line);
    border-radius: var(--radius-lg);
    background: var(--indigo-soft);
    display: grid;
    gap: var(--s2);
    justify-items: start;
}

/* The way in, at the foot of the page rather than in front of it. */
.public-cta {
    margin: var(--s10) var(--s5) var(--s8);
    padding: var(--s6);
    border: 1px solid var(--indigo-line);
    border-radius: var(--radius-lg);
    background: var(--indigo-soft);
    display: grid;
    gap: var(--s3);
    max-width: 62ch;
}
.public-cta-title { font-size: 19px; line-height: 1.3; }
.public-cta-body {
    font-family: var(--read);
    font-size: 15px;
    line-height: 1.6;
    color: var(--ink);
}
.public-cta-actions { display: flex; flex-wrap: wrap; gap: var(--s2); }
.public-cta-note { font-size: 12.5px; color: var(--muted); }

.rail {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: var(--s1);
    padding: var(--s3) 0 calc(var(--s3) + env(safe-area-inset-bottom));
    background: var(--surface-2);
    border-right: 1px solid var(--hairline);
    position: sticky;
    top: 0;
    height: 100vh;
    height: 100dvh;
}

.rail-item {
    position: relative;
    width: 52px;
    padding: var(--s2) 0 var(--s1);
    border: 1px solid transparent;
    border-radius: var(--radius);
    background: none;
    color: var(--muted);
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    cursor: pointer;
    text-decoration: none;
    transition: background .12s, color .12s;
}

.rail-item svg {
    width: 20px; height: 20px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.6;
    stroke-linecap: round;
    stroke-linejoin: round;
}

/* Centred because a label that wraps is two lines, and the flex centring only
   places the box — inside it the lines were still flush left, so "Kanji" sat
   off to one side above "library" while the icon above them was centred. */
.rail-label { font-size: 10px; letter-spacing: .02em; text-align: center; }

.rail-item:hover { color: var(--ink); background: var(--surface-3); }

.rail-item[aria-current="page"] {
    background: var(--indigo-soft);
    border-color: var(--indigo-line);
    color: var(--indigo);
}

.rail-spacer { flex: 1; }

.rail-badge {
    position: absolute;
    top: 2px; right: 4px;
    min-width: 17px; height: 17px;
    padding: 0 4px;
    border-radius: var(--radius-pill);
    background: var(--clay);
    color: #fff;
    font-family: var(--mono);
    font-size: 9px;
    line-height: 17px;
    text-align: center;
    font-variant-numeric: tabular-nums;
}

/* One icon per theme; the other is hidden. */
.icon-moon { display: none; }
@media (prefers-color-scheme: dark) {
    :root:not([data-theme="light"]) .icon-sun { display: none; }
    :root:not([data-theme="light"]) .icon-moon { display: block; }
}
:root[data-theme="dark"] .icon-sun { display: none; }
:root[data-theme="dark"] .icon-moon { display: block; }
:root[data-theme="light"] .icon-sun { display: block; }
:root[data-theme="light"] .icon-moon { display: none; }

.main { min-width: 0; display: flex; flex-direction: column; }

/* ----------------------------------------------------------- page head -- */

.page-head {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: var(--s2);
    padding: var(--s3) var(--s5);
    border-bottom: 1px solid var(--hairline);
    background: var(--surface);
}

.page-title { font-size: 15px; margin-right: var(--s2); }

.page-sub {
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
}

.grow { flex: 1; }

/* ---------------------------------------------------------- primitives -- */

.chip {
    display: inline-flex;
    align-items: center;
    gap: var(--s1);
    height: var(--s8);
    padding: 0 var(--s3);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-pill);
    background: var(--surface);
    color: var(--ink);
    font-size: 13px;
    white-space: nowrap;
    cursor: pointer;
    text-decoration: none;
    transition: border-color .12s, background .12s, color .12s;
}

.chip:hover { border-color: var(--indigo-line); }
.chip[aria-pressed="true"], .chip.is-active {
    background: var(--indigo-soft);
    border-color: var(--indigo-line);
    color: var(--indigo);
    font-weight: 600;
}
.chip .caret { color: var(--faint); font-size: 10px; }
.chip-value { color: var(--muted); }
.chip[aria-pressed="true"] .chip-value, .chip.is-active .chip-value { color: inherit; }

/* A chip that holds a number rather than a word: the custom length. Drawn as
   a chip so the row still reads as one set of controls, with the box inside
   it borderless — two outlines a few pixels apart look like a mistake. The
   spinner is dropped because it does not fit a 32px pill; the arrow keys
   still step the value. */
.chip-field { cursor: text; }
.chip-field:focus-within { border-color: var(--indigo-line); }
.chip-input {
    width: 4.5ch;
    border: 0;
    padding: 0;
    background: none;
    color: var(--ink);
    font: inherit;
    font-variant-numeric: tabular-nums;
    text-align: right;
    appearance: textfield;
    -moz-appearance: textfield;
}
.chip-input:focus { outline: none; }
.chip-input::-webkit-outer-spin-button,
.chip-input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }

.btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    height: var(--s8);
    padding: 0 var(--s4);
    border: 1px solid var(--indigo);
    border-radius: var(--radius-pill);
    background: var(--indigo);
    color: var(--on-indigo);
    font-size: 13px;
    font-weight: 600;
    cursor: pointer;
    text-decoration: none;
    transition: background .12s, border-color .12s;
}

.btn:hover { background: var(--indigo-hover); border-color: var(--indigo-hover); }
.btn[disabled] { opacity: .45; cursor: not-allowed; }
.btn svg { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }

.btn-ghost { background: none; color: var(--ink); border-color: var(--hairline); font-weight: 500; }
.btn-ghost:hover { background: var(--surface-2); border-color: var(--indigo-line); color: var(--indigo); }

.btn-danger { background: var(--clay); border-color: var(--clay); color: #fff; }
.btn-danger:hover { background: var(--clay); border-color: var(--clay); filter: brightness(.92); }

.btn-lg { height: var(--s10); padding: 0 var(--s6); font-size: 14px; }
.btn-block { width: 100%; }
.btn-on { background: var(--celadon-soft); border-color: var(--celadon); color: var(--celadon); }
.btn-on:hover { background: var(--celadon-soft); border-color: var(--celadon); color: var(--celadon); }

.icon-btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    width: var(--s8); height: var(--s8);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--muted);
    cursor: pointer;
    transition: color .12s, border-color .12s;
}
.icon-btn:hover { color: var(--indigo); border-color: var(--indigo-line); }
.icon-btn svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }
.icon-btn.is-danger:hover { color: var(--clay); border-color: var(--clay); }
.icon-btn[disabled] { opacity: .4; cursor: not-allowed; }

.card {
    border: 1px solid var(--hairline);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-sm);
}

.label {
    display: block;
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--muted);
    font-weight: 400;
}

.field { display: grid; gap: var(--s2); }
.label-hint { color: var(--faint); letter-spacing: .04em; text-transform: none; }
.field-hint { font-size: 12.5px; color: var(--muted); }

/* Adding words: a list typed into one box, and a checklist back with a row
   for each word, ticked unless there is a reason not to keep it. Wider than
   the plain modal, like the word panel, because a row carries a word, its
   reading, its meaning and what kind of word it is. */
.add-words-card { width: min(520px, 100%); }
.add-words-card .modal-actions { flex-wrap: wrap; }

/* A list rather than a line, so it gets rows to go with it — and is resized
   by hand only in height, the direction a list grows in. Two classes, because
   the plain .input rule further down gives every field a one-line height and
   no padding above or below, and as an equal it would win by coming later. */
.input.lookup-query {
    height: auto;
    padding: var(--s2) var(--s3);
    line-height: 1.5;
    resize: vertical;
}
/* Holds its line while empty, so the form does not jump when the first word
   is typed and the count appears. */
.lookup-count { min-height: 1.45em; line-height: 1.45; }
.lookup-count.is-warn { color: var(--ochre); }

/* Only while showing: .hidden has to keep winning. */
.lookup-result:not(.hidden) { display: grid; gap: var(--s3); }

/* Scrolls on its own, so twenty rows on a phone do not push the lists and the
   Add button a screen away. Contained, so reaching the end of the list does
   not carry on and scroll the card behind it. */
.lookup-list {
    display: grid;
    gap: var(--s2);
    margin: 0;
    padding: 0;
    list-style: none;
    max-height: min(50dvh, 420px);
    overflow-y: auto;
    overscroll-behavior: contain;
}
.lookup-line {
    display: grid;
    grid-template-columns: 16px minmax(0, 1fr);
    align-items: start;
    gap: var(--s3);
    padding: var(--s3);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
}
label.lookup-line { cursor: pointer; }
.lookup-line input { width: 16px; height: 16px; margin: 5px 0 0; accent-color: var(--indigo); }
.lookup-line:has(input:checked) { border-color: var(--indigo-line); background: var(--indigo-soft); }
.lookup-row-body { display: grid; gap: 2px; min-width: 0; }
.lookup-row-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--s2); }
/* Anywhere, not just between words: a query is up to 120 characters with no
   promise of a space in it, and the dialog must not scroll sideways. */
.lookup-row-word { font-family: var(--cjk); font-size: 20px; line-height: 1.3; overflow-wrap: anywhere; }
.lookup-row-word[data-latin="true"] { font-family: var(--read); }
.lookup-row-romaji { font-family: var(--mono); font-size: 12px; color: var(--muted); overflow-wrap: anywhere; }
.lookup-row-meaning { font-family: var(--read); font-size: 15px; line-height: 1.45; overflow-wrap: anywhere; }
.lookup-row-tags { display: flex; flex-wrap: wrap; gap: var(--s1); margin-top: 2px; }
.lookup-row-note { font-size: 12.5px; line-height: 1.45; color: var(--muted); }

/* Nothing to keep on these, so nothing to tick: set back and dashed, with the
   checkbox's column left empty so the words still line up with the rest. */
.lookup-row.is-quiet .lookup-line { border-style: dashed; background: var(--surface-2); }
.lookup-row.is-quiet .lookup-row-word { font-size: 16px; color: var(--muted); }

/* Where the word came from, dictionary or AI: a label, not a claim to read
   first, so it sits at the end of the word's line in the quietest type. */
.lookup-row-source {
    margin-left: auto;
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--muted);
}

/* The other matches belong to the row, so they sit inside its card: the row
   itself draws the card, around the label and the block below it, and is
   coloured when ticked. One border rather than two halves meeting, which
   leave a hairline seam wherever the join lands between pixels. The block is
   outside the label, because a button inside one would tick the box too. */
.lookup-row.has-alternatives {
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
}
.lookup-row.has-alternatives:has(.lookup-line input:checked) {
    border-color: var(--indigo-line);
    background: var(--indigo-soft);
}
.lookup-row.has-alternatives .lookup-line {
    border: 0;
    background: none;
    padding-bottom: var(--s1);
}
.lookup-alts { padding: 0 var(--s3) var(--s2) calc(var(--s3) * 2 + 16px); }
.lookup-alts-toggle {
    display: inline-flex;
    align-items: center;
    gap: var(--s1);
    min-height: var(--s8);
    margin-left: calc(-1 * var(--s2));
    padding: 0 var(--s2);
    border: 0;
    border-radius: var(--radius-sm);
    background: none;
    color: var(--indigo);
    font-size: 12.5px;
    cursor: pointer;
}
.lookup-alts-toggle:hover { background: var(--surface-2); }
.lookup-alts-toggle svg {
    width: 14px; height: 14px;
    fill: none; stroke: currentColor; stroke-width: 1.7;
    stroke-linecap: round; stroke-linejoin: round;
    transition: transform .12s;
}
.lookup-alts-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }
.lookup-alts-list:not(.hidden) {
    display: grid;
    gap: var(--s1);
    margin: var(--s1) 0 0;
    padding: 0;
    list-style: none;
}
.lookup-alt {
    display: grid;
    gap: 2px;
    width: 100%;
    padding: var(--s2);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-sm);
    background: var(--surface);
    text-align: left;
    cursor: pointer;
}
.lookup-alt:hover { border-color: var(--indigo-line); }
.lookup-alt-head { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--s2); }
.lookup-alt-head .pill { align-self: center; }
.lookup-alt-word { font-family: var(--cjk); font-size: 17px; line-height: 1.3; overflow-wrap: anywhere; }
.lookup-alt-meaning { font-family: var(--read); font-size: 14px; line-height: 1.4; overflow-wrap: anywhere; }
.lookup-credit a { color: inherit; text-decoration: underline; }

.input, .select {
    width: 100%;
    height: var(--s10);
    padding: 0 var(--s3);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    font-size: 14px;
}
.input::placeholder { color: var(--faint); }
.input:focus-visible, .select:focus-visible { border-color: var(--indigo); }
.select { appearance: none; padding-right: var(--s8); cursor: pointer; }

/* Safari on iOS zooms the whole page in when you focus a field whose text is
   smaller than 16px, and it does not zoom back out when you leave the field —
   so one tap on a search box leaves the app scrolled sideways until the reader
   pinches it back. 16px is the threshold, not a preference, which is why this
   is a flat value and not a token. Confined to touch pointers so the desk
   layout keeps its 14px fields.

   This sits directly under the base rule on purpose: the fields that set their
   own size later — the drill's 24px answer box — are meant to keep it, and
   only need to be equal to or above the threshold, not equal to this. */
@media (pointer: coarse) {
    .input, .select, .chip-input, textarea { font-size: 16px; }
}

.select-wrap { position: relative; }
.select-wrap::after {
    content: "";
    position: absolute;
    right: var(--s4); top: 50%;
    width: 7px; height: 7px;
    margin-top: -5px;
    border-right: 1.5px solid var(--muted);
    border-bottom: 1.5px solid var(--muted);
    transform: rotate(45deg);
    pointer-events: none;
}

.check {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    height: var(--s10);
    padding: 0 var(--s3);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    cursor: pointer;
    font-size: 14px;
}
.check input { accent-color: var(--indigo); width: 16px; height: 16px; }
.check:has(input:checked) { border-color: var(--indigo-line); background: var(--indigo-soft); color: var(--indigo); }

/* The box that means all the others. Set apart from them, because it is not
   another answer of the same kind — it says what the answers are. */
.check.is-all {
    border-style: dashed;
    margin-right: var(--s2);
}
.check.is-all:has(input:indeterminate) {
    border-color: var(--indigo-line);
    color: var(--indigo);
}
/* Half-ticked, so it should not read as ticked. */
.check.is-all input:indeterminate { accent-color: var(--indigo-line); }

.pill {
    display: inline-block;
    padding: 2px var(--s2);
    border-radius: var(--radius-pill);
    background: var(--indigo-soft);
    color: var(--indigo);
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: .07em;
    text-transform: uppercase;
}
.pill-muted { background: var(--surface-3); color: var(--muted); }
.pill-good { background: var(--celadon-soft); color: var(--celadon); }
.pill-warn { background: var(--ochre-soft); color: var(--ochre); }
.pill-bad { background: var(--clay-soft); color: var(--clay); }

.banner {
    display: flex;
    align-items: flex-start;
    gap: var(--s2);
    padding: var(--s3) var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    font-size: 13.5px;
}
.banner-error { background: var(--clay-soft); border-color: var(--clay); color: var(--clay); }
.banner-ok { background: var(--celadon-soft); border-color: var(--celadon); color: var(--celadon); }
.banner ul { display: grid; gap: var(--s1); }

.empty {
    display: grid;
    justify-items: center;
    gap: var(--s3);
    padding: var(--s12) var(--s5);
    text-align: center;
    color: var(--muted);
}
.empty svg { width: 40px; height: 40px; fill: none; stroke: currentColor; stroke-width: 1.4; stroke-linecap: round; stroke-linejoin: round; opacity: .5; }
.empty-title { font-size: 16px; font-weight: 600; color: var(--ink); }
.empty p { max-width: 44ch; font-size: 13.5px; }

/* --------------------------------------------------------------- toasts -- */

.toasts {
    position: fixed;
    left: 50%;
    bottom: calc(var(--s5) + env(safe-area-inset-bottom));
    transform: translateX(-50%);
    display: grid;
    gap: var(--s2);
    z-index: 90;
    width: min(420px, calc(100vw - var(--s8)));
    pointer-events: none;
}

.toast {
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s3) var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: var(--shadow-lg);
    font-size: 13.5px;
    pointer-events: auto;
    animation: toast-in .18s ease-out;
}
.toast-error { border-color: var(--clay); color: var(--clay); background: var(--clay-soft); }
.toast-ok { border-color: var(--celadon); color: var(--celadon); background: var(--celadon-soft); }
.toast.is-leaving { animation: toast-out .18s ease-in forwards; }
.toast-close { margin-left: auto; border: 0; background: none; color: inherit; cursor: pointer; opacity: .7; font-size: 16px; line-height: 1; }

@keyframes toast-in { from { opacity: 0; transform: translateY(8px); } }
@keyframes toast-out { to { opacity: 0; transform: translateY(8px); } }

/* --------------------------------------------------------------- modal -- */

.modal {
    position: fixed;
    inset: 0;
    z-index: 100;
    display: flex;
    align-items: center;
    justify-content: center;
    padding: var(--s5);
    background: rgba(25, 29, 43, .5);
}
.modal-card {
    width: min(460px, 100%);
    max-height: calc(100dvh - var(--s10));
    overflow-y: auto;
    padding: var(--s6);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-lg);
    display: grid;
    gap: var(--s4);
}
.modal-title { font-size: 17px; }
.modal-actions { display: flex; gap: var(--s2); justify-content: flex-end; }

/* One saved word, opened from its row. Wider than the plain modal because it
   carries the character breakdown, which is a list rather than a sentence. */
.word-modal-card { width: min(520px, 100%); }
.word-detail-head {
    display: flex;
    align-items: flex-start;
    justify-content: space-between;
    gap: var(--s3);
}
.word-detail-word {
    font-family: var(--cjk);
    font-size: 28px;
    line-height: 1.25;
    word-break: break-word;
}
.word-detail-word[data-latin="true"] { font-family: var(--read); }

.word-stats {
    display: grid;
    gap: var(--s2);
    padding-top: var(--s4);
    border-top: 1px solid var(--hairline);
}
.word-stat-rows { display: grid; gap: var(--s1); }
.word-stat {
    display: grid;
    grid-template-columns: 1fr auto auto;
    align-items: center;
    gap: var(--s3);
    padding: var(--s2) var(--s3);
    border-radius: var(--radius);
    background: var(--surface-2);
    font-size: 13px;
}
.word-stat-name { font-weight: 500; }
.word-stat-when {
    font-family: var(--mono);
    font-size: 11.5px;
    color: var(--muted);
    white-space: nowrap;
}
/* A question that has never been put is not a question going badly, and five
   empty bars on a full-strength row would read as the latter. */
.word-stat.is-untouched { opacity: .62; }

/* The lists a word is in, and the box for putting it in another. The first
   row of the dropdown is the input itself — what you type on the left, and a +
   on the right that makes a list of that name. Anything you already have that
   matches shows underneath, to be picked instead. */
.word-tags {
    display: grid;
    gap: var(--s2);
    padding-top: var(--s4);
    border-top: 1px solid var(--hairline);
}
.tagbox { position: relative; display: grid; gap: var(--s2); }
.tagbox-chips { display: flex; flex-wrap: wrap; gap: var(--s1); font-size: 12.5px; color: var(--muted); }
.tagbox-chip {
    display: inline-flex;
    align-items: center;
    gap: var(--s1);
    padding: 3px var(--s1) 3px var(--s3);
    border-radius: var(--radius-pill);
    background: var(--indigo-soft);
    color: var(--indigo);
    font-size: 12.5px;
}
.tagbox-chip-drop {
    display: grid;
    place-items: center;
    width: 18px;
    height: 18px;
    border: 0;
    border-radius: 50%;
    background: none;
    color: inherit;
    cursor: pointer;
    opacity: .65;
}
.tagbox-chip-drop:hover { opacity: 1; background: var(--surface); }
.tagbox-chip-drop svg {
    width: 11px; height: 11px;
    fill: none; stroke: currentColor; stroke-width: 2.2; stroke-linecap: round;
}

.tagbox-field { position: relative; display: flex; align-items: center; }
.tagbox-input { flex: 1; padding-right: calc(var(--s8) + var(--s2)); }
/* Sits inside the input rather than beside it, because the row is one thing:
   what you typed, and the way to keep it. */
.tagbox-add {
    position: absolute;
    right: var(--s2);
    display: grid;
    place-items: center;
    width: var(--s7);
    height: var(--s7);
    border: 0;
    border-radius: var(--radius);
    background: var(--indigo-soft);
    color: var(--indigo);
    cursor: pointer;
}
.tagbox-add:hover { background: var(--indigo); color: var(--on-indigo); }
.tagbox-add[disabled] { opacity: .45; cursor: not-allowed; }
.tagbox-add svg {
    width: 15px; height: 15px;
    fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round;
}

/* In the flow rather than floating over it. The panel this sits in scrolls
   and clips what overflows, so an absolutely positioned menu opening near the
   bottom was cut in half — and a dropdown you have to know to scroll for is
   worse than a list that simply pushes the page down. */
.tagbox-menu {
    margin-top: var(--s1);
    padding: var(--s1);
    list-style: none;
    max-height: 210px;
    overflow-y: auto;
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: var(--shadow-lg);
}
.tagbox-option {
    display: flex;
    align-items: baseline;
    justify-content: space-between;
    gap: var(--s3);
    width: 100%;
    padding: var(--s2) var(--s3);
    border: 0;
    border-radius: var(--radius);
    background: none;
    color: var(--ink);
    font-size: 13.5px;
    text-align: left;
    cursor: pointer;
}
.tagbox-option:hover { background: var(--indigo-soft); }
/* A list the word is already in. Shown so the search visibly found it, and
   greyed so it is plainly not the thing to click. */
.tagbox-option.is-in { color: var(--muted); cursor: default; }
.tagbox-option.is-in:hover { background: none; }
.tagbox-option-count { font-family: var(--mono); font-size: 10.5px; color: var(--muted); }

.tagbox-note { font-size: 12.5px; line-height: 1.45; min-height: 1.45em; color: var(--muted); }
.tagbox-note.is-ok { color: var(--celadon); }
.tagbox-note.is-warn { color: var(--ochre); }
.tagbox-note.is-bad { color: var(--clay); }

/* Choosing the lists while the word is still being looked at, rather than
   keeping it and then opening it again to file it. */
.add-lists {
    display: grid;
    gap: var(--s2);
    padding-top: var(--s4);
    border-top: 1px solid var(--hairline);
}
.add-lists .tagbox-field { position: relative; display: flex; align-items: center; }

/* The row opens the word, so it has to look like it does something. */
tr[data-word-row] { cursor: pointer; }
tr[data-word-row]:hover { background: var(--surface-2); }
tr[data-word-row]:focus-visible {
    outline: 2px solid var(--indigo);
    outline-offset: -2px;
}

/* --------------------------------------------------------------- table -- */

.table-wrap { overflow-x: auto; }

.table { width: 100%; border-collapse: collapse; font-size: 14px; }
.table th {
    text-align: left;
    padding: var(--s2) var(--s4);
    border-bottom: 1px solid var(--hairline);
    font-family: var(--mono);
    font-weight: 400;
    font-size: 10px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--muted);
    white-space: nowrap;
}
.table td {
    padding: var(--s2) var(--s4);
    border-bottom: 1px solid var(--hairline);
    vertical-align: middle;
}
.table tbody tr:hover td { background: var(--surface-2); }
.table .num { font-family: var(--mono); font-variant-numeric: tabular-nums; }
.table .right { text-align: right; }

.pager {
    display: flex;
    align-items: center;
    gap: var(--s3);
    padding: var(--s3) var(--s5);
    border-top: 1px solid var(--hairline);
    font-size: 13px;
    color: var(--muted);
}

/* ================================================================ READ == */

.reader { display: grid; grid-template-columns: minmax(0, 1fr) 300px; flex: 1; min-height: 0; }
.reader-col { display: flex; flex-direction: column; min-width: 0; }

.passage { padding: var(--s8) var(--s8) var(--s10); min-width: 0; }
.passage-meta {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s4);
    margin-bottom: var(--s5);
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--muted);
}

.line {
    font-family: var(--cjk);
    font-size: 27px;
    line-height: 2.05;
    letter-spacing: .02em;
    max-width: 28em;
    /* Kinsoku: a Japanese line may not begin with 。or 、 */
    line-break: strict;
    overflow-wrap: break-word;
}
.line[data-latin="true"] { font-family: var(--read); line-height: 1.85; max-width: 34em; }

/* A conversation is written one turn to a line, and the line breaks are in the
   text. Only a conversation gets this: prose has no newlines to keep, and
   turning it on everywhere would mean any stray whitespace the writer left
   behind showed up on the page. */
.line[data-text-type="dialog"] { white-space: pre-wrap; }

.word {
    border: 0;
    background: none;
    font: inherit;
    color: inherit;
    text-align: left;
    padding: 1px 2px;
    margin: 0 1px;
    border-radius: var(--radius-sm);
    border-bottom: 1.5px solid transparent;
    cursor: pointer;
    transition: background .12s;
}

/* When the passage carries its own spacing and punctuation, the words must sit
   flush against it — any margin would invent gaps the sentence does not have. */
.line[data-tight="true"] .word { margin: 0; padding: 0; }

/* Holds a word together with the punctuation that follows it, so a line can
   never begin with 。or 、. Browsers ignore display:inline on a <button>, so
   the break has to be prevented around it instead. */
.word-group { white-space: nowrap; }
.word:hover { background: var(--indigo-soft); }
.word.is-seen { border-bottom-color: var(--indigo-line); }
.word.is-saved { border-bottom-color: var(--celadon); }
.word[aria-pressed="true"] { background: var(--indigo); color: var(--on-indigo); }

/* A long text is shown a paragraph at a time, each one plain until it is
   tapped for its words. The blocks are the passage — same face, same measure,
   inherited from .line — with a hover tint as the only sign they can be
   pressed, so the page reads as a text and not as a stack of cards. The
   negative side margin lets the tint reach a little past the words while the
   words themselves stay in line with the meta above them; it is smaller than
   the passage's narrowest padding, so nothing is pushed off the edge of a
   phone. Once opened, the block is no longer a button and the tint goes. */
.para-block {
    display: block;
    margin: 0 calc(-1 * var(--s2)) var(--s4);
    padding: var(--s1) var(--s2);
    border-radius: var(--radius-sm);
    transition: background .12s, opacity .12s;
}
.para-block:last-child { margin-bottom: 0; }
.para-block[role="button"] { cursor: pointer; }
.para-block[role="button"]:hover { background: var(--surface); }

/* Dimmed while its words are on the way, because they replace the text; the
   cursor says the tap was taken, so it is not tapped again in the wait. */
.para-block.is-loading { cursor: progress; }
/* The Library's status line, sat under the paragraph's own text on its own
   line. The text is not dimmed: the ring and the label already say what is
   happening, and a reader can keep reading the paragraph while they wait. */
.para-block .para-status { display: flex; margin-top: var(--s2); }

/* The one line that says how a long text works, above the passage. */
.passage-hint { max-width: 34em; margin-bottom: var(--s5); }

.reveal {
    max-width: 52em;
    margin-top: var(--s6);
    padding-top: var(--s5);
    border-top: 1px solid var(--hairline);
    display: grid;
    gap: var(--s5);
}
.reveal-head { display: flex; align-items: center; gap: var(--s3); margin-bottom: var(--s2); }
.reveal p { font-family: var(--read); font-size: 16.5px; line-height: 1.6; }
.reveal p.romaji { font-family: var(--mono); font-size: 13.5px; color: var(--muted); }
.is-blurred { filter: blur(5px); opacity: .5; user-select: none; transition: filter .18s, opacity .18s; }

.reveal-toggle {
    display: inline-flex;
    align-items: center;
    gap: 6px;
    height: 24px;
    padding: 0 var(--s2);
    font-size: 11px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--hairline);
    background: var(--surface);
    color: var(--muted);
    cursor: pointer;
}
.reveal-toggle:hover { color: var(--indigo); border-color: var(--indigo-line); }

.gloss {
    border-left: 1px solid var(--hairline);
    background: var(--surface-2);
    padding: var(--s5);
    display: flex;
    flex-direction: column;
    gap: var(--s4);
    min-width: 0;
    /* Stays with you down a long text. As an ordinary column it scrolled away
       with the paragraphs, so on anything longer than a screen you could tap a
       word and be looking at an empty margin.
       `align-self: start` is what makes the sticking possible: a grid item is
       stretched to the row by default, and an item as tall as its container
       has nowhere to stick. The height then has to be given back, or the panel
       would shrink to its contents and leave the sidebar half painted. */
    position: sticky;
    top: 0;
    align-self: start;
    height: 100vh;
    height: 100dvh;
    overflow-y: auto;
}
/* The sheet's handle, and only a sheet has one — see the bottom-sheet rules
   under the 1020px breakpoint, where this is turned on. */
.gloss-grip { display: none; }

.gloss-empty { font-size: 13.5px; color: var(--muted); }
.gloss-word { font-family: var(--cjk); font-size: 30px; line-height: 1.25; word-break: break-word; }
.gloss-word[data-latin="true"] { font-family: var(--read); }
.gloss-romaji { font-family: var(--mono); font-size: 12.5px; color: var(--muted); margin-top: 2px; }
.gloss-translation { font-family: var(--read); font-size: 17px; margin-top: var(--s3); }
.gloss-tags { display: flex; flex-wrap: wrap; gap: var(--s1); margin-top: var(--s2); }
.gloss-actions { display: flex; gap: var(--s2); margin-top: var(--s5); }
.gloss-actions .btn { flex: 1; }
.gloss-foot {
    margin-top: auto;
    padding-top: var(--s4);
    border-top: 1px solid var(--hairline);
    font-size: 12.5px;
    color: var(--muted);
    display: grid;
    gap: var(--s3);
}
.gloss-foot b { color: var(--ink); font-family: var(--mono); font-variant-numeric: tabular-nums; }

/* The word as the dictionary lists it, between the gloss and its kanji; see
   dictionary_panel.js. Empty until an entry arrives, and an empty item in the
   gloss's column would still take a gap. */
.dictionary-panel { display: grid; gap: var(--s3); min-width: 0; }
.dictionary-panel:empty { display: none; }
.dictionary-title { padding-top: var(--s4); border-top: 1px solid var(--hairline); }
.dictionary-entry {
    display: grid;
    gap: var(--s2);
    min-width: 0;
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    padding: var(--s3) var(--s4) var(--s4);
}
.dictionary-ja { font-family: var(--cjk); }
.dictionary-head { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1) var(--s3); }
.dictionary-headword { font-family: var(--cjk); font-size: 26px; line-height: 1.2; word-break: break-word; }
.dictionary-reading { display: flex; flex-wrap: wrap; align-items: baseline; gap: 0 var(--s2); margin-top: calc(var(--s1) * -1); }
.dictionary-kana { font-family: var(--cjk); font-size: 15px; word-break: break-word; }
.dictionary-romaji { font-family: var(--mono); font-size: 12.5px; color: var(--muted); word-break: break-word; }
/* How the text's form came from this one: beside the entry, not part of it. */
.dictionary-deinflection {
    font-size: 13px;
    line-height: 1.5;
    color: var(--muted);
    padding-left: var(--s2);
    border-left: 2px solid var(--indigo-line);
    word-break: break-word;
}
.dictionary-deinflection .dictionary-ja { color: var(--ink); }
/* Numbered by the stylesheet, since the reset takes list markers away, and
   not numbered at all when there is only the one sense. */
.dictionary-senses { display: grid; gap: var(--s2); counter-reset: sense; }
.dictionary-sense {
    display: grid;
    grid-template-columns: 1.5em minmax(0, 1fr);
    column-gap: var(--s1);
    counter-increment: sense;
}
.dictionary-sense::before {
    content: counter(sense) ".";
    grid-column: 1;
    grid-row: 1 / span 2;
    font-family: var(--mono);
    font-size: 11.5px;
    line-height: 1.9;
    color: var(--muted);
}
.dictionary-sense > * { grid-column: 2; }
.dictionary-senses.is-single .dictionary-sense { grid-template-columns: minmax(0, 1fr); }
.dictionary-senses.is-single .dictionary-sense::before { content: none; }
.dictionary-senses.is-single .dictionary-sense > * { grid-column: 1; }
.dictionary-pos {
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: .05em;
    text-transform: uppercase;
    color: var(--indigo);
}
.dictionary-glosses { font-family: var(--read); font-size: 15px; line-height: 1.4; word-break: break-word; }
.dictionary-notes { font-family: var(--ui); font-size: 12px; color: var(--muted); }
.dictionary-note { font-size: 12.5px; line-height: 1.5; color: var(--muted); }
.dictionary-forms { font-size: 13px; color: var(--muted); word-break: break-word; }
.dictionary-forms b {
    font-family: var(--mono);
    font-size: 10.5px;
    font-weight: 400;
    letter-spacing: .06em;
    text-transform: uppercase;
    margin-right: var(--s2);
}
.dictionary-keep { width: 100%; max-width: 100%; margin-top: var(--s1); }
.dictionary-keep span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.dictionary-more-toggle { justify-self: start; }
.dictionary-more { display: grid; gap: var(--s3); }
.dictionary-credit { font-size: 11.5px; line-height: 1.5; color: var(--muted); }
.dictionary-credit a { color: inherit; }

/* A text's common words, offered beside the gloss before any word is tapped.
   Each is the word, its reading and a meaning cut to one line: enough to
   decide whether to go and look, which is all a suggestion is for. The slot
   is its own item in the gloss's column, empty while a word is shown, and an
   empty item would still take a gap. */
.dictionary-suggest-slot { min-width: 0; }
.dictionary-suggest-slot:empty { display: none; }
.dictionary-suggest { display: grid; gap: var(--s2); margin-top: var(--s1); }
.dictionary-suggest-list { display: grid; gap: var(--s2); }
.dictionary-suggest-list[hidden] { display: none; }
/* On a phone's sheet the heading is the button that opens the list, one row
   under the prompt, so the sheet at rest stays the grip, the prompt and this. */
.dictionary-suggest-toggle {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s2);
    width: 100%;
    min-height: var(--tap);
    padding: 0 var(--s3);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    font: inherit;
    letter-spacing: inherit;
    text-transform: inherit;
    text-align: left;
    cursor: pointer;
    touch-action: manipulation;
}
.dictionary-suggest-toggle:hover { border-color: var(--indigo-line); }
.dictionary-suggest-toggle span { min-width: 0; overflow-wrap: anywhere; }
.dictionary-suggest-toggle svg {
    flex: none;
    width: 16px; height: 16px;
    fill: none; stroke: currentColor; stroke-width: 1.7;
    stroke-linecap: round; stroke-linejoin: round;
    transition: transform .12s;
}
.dictionary-suggest-toggle[aria-expanded="true"] svg { transform: rotate(180deg); }
.dictionary-suggestion {
    display: grid;
    grid-template-columns: auto minmax(0, 1fr);
    align-items: baseline;
    column-gap: var(--s2);
    width: 100%;
    min-height: var(--tap);
    padding: var(--s2) var(--s3);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    text-align: left;
    cursor: pointer;
    touch-action: manipulation;
    transition: border-color .12s, background .12s;
}
.dictionary-suggestion:hover { border-color: var(--indigo-line); background: var(--indigo-soft); }
.dictionary-suggestion-word { font-family: var(--cjk); font-size: 18px; line-height: 1.3; word-break: break-word; }
.dictionary-suggestion-reading,
.dictionary-suggestion-meaning {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
    color: var(--muted);
}
.dictionary-suggestion-reading { font-family: var(--cjk); font-size: 12.5px; }
.dictionary-suggestion-meaning { grid-column: 1 / -1; font-family: var(--read); font-size: 13.5px; }

/* ----------------------------------------------------------- alphabet -- */

/* Wraps because it has to. Everywhere else this holds two chips — Chart and
   Drill — and never needed to; the Kanji library puts five levels in it, and
   without this the row keeps its full width inside a narrower head and takes
   the whole page sideways with it. */
.chip-tabs { display: flex; flex-wrap: wrap; gap: var(--s1); }
/* Nothing to separate. A visitor is not shown the Chart/Drill chips, which
   leaves the rule sitting straight after the title as a stray tick of a line.
   The selector only matches once those chips are gone. */
.page-title + .head-rule { display: none; }

/* Separates the section chips from the controls that belong to that section. */
.head-rule {
    width: 1px;
    align-self: stretch;
    margin: 0 var(--s1);
    background: var(--hairline);
}
.chip[aria-current="page"] {
    background: var(--indigo-soft);
    border-color: var(--indigo-line);
    color: var(--indigo);
    font-weight: 600;
}

.kana-page { padding: var(--s6) var(--s5) var(--s10); display: grid; gap: var(--s8); }
.kana-intro {
    font-family: var(--read);
    font-size: 15px;
    line-height: 1.6;
    color: var(--muted);
    max-width: 62ch;
}
.kana-group-head { display: grid; gap: var(--s1); margin-bottom: var(--s4); }
.kana-hint {
    font-size: 13px;
    line-height: 1.5;
    color: var(--muted);
    max-width: 46ch;
}

/* One measure for the whole page. The chart headings are laid out on the same
   two blocks as the rows beneath them, so "Voiced" sits over the voiced half
   rather than merely near it. */
.kana-page { --kana-cell: clamp(58px, 5.6vw, 92px); }

.kana-grid {
    display: grid;
    grid-template-columns: repeat(var(--kana-cols, 5), var(--kana-cell));
    gap: var(--s2);
}

.kana-chart { display: grid; gap: var(--s2); }
.kana-chart-head,
.kana-row {
    display: grid;
    grid-template-columns:
        calc(5 * var(--kana-cell) + 4 * var(--s2))
        calc(5 * var(--kana-cell) + 4 * var(--s2));
    gap: var(--s2) var(--s10);
    justify-content: start;
}
.kana-chart-head { align-items: end; margin-bottom: var(--s3); }
/* Without this the は row's cells stretch to the height of ば plus ぱ beside
   them, and one row of the chart ends up twice as tall as the rest. */
.kana-row { align-items: start; }
/* は is the one row that voices two ways, ば and ぱ; they stack rather than
   running off to the right, so every other row keeps its place. */
.kana-voiced { display: grid; gap: var(--s2); align-content: start; }

.kana-cell {
    position: relative;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-height: 76px;
    padding: var(--s2);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    cursor: pointer;
    transition: border-color .12s, background .12s, transform .08s;
}
.kana-cell:hover { border-color: var(--indigo-line); background: var(--indigo-soft); }
.kana-cell:active { transform: scale(.97); }
.kana-cell.is-gap {
    border-style: none;
    background: none;
    cursor: default;
    min-height: 0;
}
/* Recording a character that has never been played takes a moment. */
.kana-cell.is-loading { opacity: .55; cursor: progress; }
.kana-cell.is-played { border-color: var(--indigo); }

.kana-char { font-family: var(--cjk); font-size: 28px; line-height: 1.15; }
.kana-romaji {
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: .04em;
    color: var(--muted);
}
.kana-flag {
    position: absolute;
    top: 4px; right: 7px;
    font-family: var(--mono);
    font-size: 13px;
    line-height: 1;
    color: var(--ochre);
}

/* ----------------------------------------------------- alphabet drill -- */

.drill-settings {
    display: grid;
    gap: var(--s4);
    padding: var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
}
.drill-set { border: 0; margin: 0; padding: 0; display: flex; flex-wrap: wrap; gap: var(--s2); }
.drill-set legend { padding: 0; margin-bottom: var(--s2); float: left; width: 100%; }
/* Its own row, always. `width: 100%` alone was being clamped by the 46ch
   measure .kana-hint carries, so whether the first checkbox tucked up beside
   the hint came down to whether it happened to fit in what was left — N5 did
   and "Read it" did not. The panel is narrow enough to keep the line length
   honest without the cap. */
.drill-set-hint { width: 100%; max-width: none; margin: calc(var(--s1) * -1) 0 var(--s1); }
/* How many characters a level contributes, beside its name — so the size of
   the deck you are choosing is visible before you choose it. */
.drill-set-count { font-family: var(--mono); font-size: 11px; color: var(--muted); }

.drill-task { display: flex; align-items: center; justify-content: center; gap: var(--s2); }
/* What is being asked and what you answer with are two things, not one block:
   the character needs room around it to be looked at. */
.drill-body { display: grid; gap: var(--s6); }
.drill-task .drill-prompt-sub { margin-top: 0; }
.option-kana { font-family: var(--cjk); font-size: 26px; line-height: 1.2; }

.drill-answer-actions { display: flex; gap: var(--s2); margin-top: var(--s2); }

/* Writing the word out. The field takes Japanese, so it gets the reading face
   and room for a whole word rather than the app's usual input sizing. */
.typed { display: grid; gap: var(--s3); }
.typed-input {
    width: 100%;
    height: auto;
    padding: var(--s3) var(--s4);
    font-family: var(--cjk);
    font-size: 24px;
    text-align: center;
}
.typed-note { font-size: 13.5px; line-height: 1.45; min-height: 1.45em; color: var(--muted); }
.typed-note.is-ok { color: var(--celadon); }
.typed-note.is-warn { color: var(--ochre); }
.typed-note.is-bad { color: var(--clay); }

/* The answer sits beside the square when there is room and underneath it when
   there is not. Without the wrap the square gives up all its width to the
   answer panel and there is nothing left to have drawn on. */
.draw-area {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: center;
    gap: var(--s3);
}
.draw-pad {
    position: relative;
    /* A basis rather than a width: as a flex item, width:100% resolves against
       the wrong box and the square collapses to half its size. */
    flex: 1 1 220px;
    min-width: 200px;
    max-width: 260px;
    aspect-ratio: 1;
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
}
#draw-canvas {
    display: block;
    width: 100%;
    height: 100%;
    color: var(--ink);
    /* Named here so the canvas can read it and stay in step with the theme. */
    --guide: var(--hairline);
    border-radius: var(--radius);
    cursor: crosshair;
    /* Without this a finger scrolls the page instead of drawing on it. */
    touch-action: none;
}
.draw-answer {
    display: grid;
    align-content: center;
    justify-items: center;
    gap: var(--s2);
    padding: var(--s3);
    border: 1px solid var(--celadon);
    border-radius: var(--radius);
    background: var(--celadon-soft);
    flex: 0 0 auto;
    min-width: 108px;
}
.draw-target { font-family: var(--cjk); font-size: 56px; line-height: 1.1; color: var(--ink); }

/* The character writing itself. The stroke files colour themselves from these
   two properties, so the playback follows the theme like everything else. */
.draw-play { width: 108px; }
.kana-play {
    display: block;
    width: 100%;
    height: auto;
    fill: none;
    stroke: var(--ink);
    /* The source draws at this weight in a 1024 box; anything thinner stops
       looking like a brush and starts looking like a diagram. */
    stroke-width: 96;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.draw-play:empty { display: none; }

.draw-verdict {
    font-size: 13.5px;
    line-height: 1.45;
    padding: var(--s2) var(--s3);
    border-radius: var(--radius);
    border: 1px solid var(--hairline);
}
.draw-verdict.is-ok { border-color: var(--celadon); background: var(--celadon-soft); color: var(--celadon); }
.draw-verdict.is-warn { border-color: var(--ochre); background: var(--ochre-soft); color: var(--ochre); }
.draw-verdict.is-bad { border-color: var(--clay); background: var(--clay-soft); color: var(--clay); }
/* Undo and Clear act on the square but are not part of it. Sitting close under
   its edge they read as a border of the pad rather than as controls. */
.draw-tools { display: flex; justify-content: center; gap: var(--s2); margin-top: var(--s4); }

/* The kanji you have kept. A list rather than a grid: each one carries a
   meaning and a reading, which a chart cell has no room for. */
.kanji-kept {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: var(--s3);
    /* Cards fill their row rather than each stopping at its own content, so a
       row of them ends on one line instead of a ragged edge. Rows are still
       sized to what is in them, so one character with a long note does not
       stretch every card on the page. */
}
/* The kanji library's cards are these cards — same character, same readings,
   same pills — so they share the rule rather than copying it. Only the corner
   differs, and that is in the library's own block below. */
.kanji-kept-item,
.kanji-card {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
    padding: var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
}
.kanji-kept-head { display: flex; align-items: flex-start; gap: var(--s3); }
.kanji-kept-char { font-family: var(--cjk); font-size: 44px; line-height: 1.05; flex: none; }
.kanji-kept-sense { min-width: 0; flex: 1; display: grid; gap: 2px; }
.kanji-kept-meaning { font-family: var(--read); font-size: 16px; line-height: 1.35; }
.kanji-kept-actions { flex: none; display: flex; flex-direction: column; gap: var(--s1); }
.kanji-kept-drop { flex: none; }
.kanji-kept-item .kanji-tags,
.kanji-card .kanji-tags,
.kanji-kept-item .kanji-note { margin-top: 0; }

/* The filters over the kept kanji sit with the cards they narrow, nearer to
   them than the introduction is, rather than in a bar across the page: there
   are three short selects, not a search. min-width lets the row wrap inside
   the page's grid instead of widening its one column. */
.kanji-kept-list { display: grid; gap: var(--s4); min-width: 0; }
.kanji-filters { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); }
.kanji-filters-count { margin-left: var(--s2); }

/* ---------------------------------------------------- kanji library -- */

.klib-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: var(--s3);
}
/* The pills sit at the foot of the card whatever the readings above them run
   to, so a row of cards has one line of pills rather than a staircase. */
.kanji-card .kanji-tags { margin-top: auto; }

/* Past the tenth, for a visitor. The card is still there — how much there is
   is part of what the page is saying — but not readable. This is a gate on
   the reading, not on the data: the characters are in the page either way,
   which is fine, because they are public reference and the page is meant to
   be indexable. */
.kanji-card.is-locked {
    filter: blur(5px);
    user-select: none;
    /* Nothing in it to press, and nothing to press it with: the card is out
       of the tab order and out of the accessibility tree, so a keyboard or a
       screen reader meets the same wall as everybody else. */
    pointer-events: none;
}

.klib-locked-note {
    grid-column: 1 / -1;
    padding: var(--s4) var(--s5);
    border: 1px solid var(--indigo-line);
    border-radius: var(--radius);
    background: var(--indigo-soft);
    font-size: 14px;
    line-height: 1.55;
}

.klib-actions { flex: none; display: flex; align-items: center; }
.klib-kept {
    display: grid;
    place-items: center;
    /* Exactly the size of the + it stands in for, so the card does not change
       shape when a character is added. */
    width: var(--s8);
    height: var(--s8);
    color: var(--celadon);
}
.klib-kept svg {
    width: 18px;
    height: 18px;
    fill: none;
    stroke: currentColor;
    stroke-width: 2;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.kanji-kept-foot {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: var(--s3);
    /* Pushed to the foot of the card, so the rule above it lands at the same
       height on every card of a row however long the note above it runs. */
    margin-top: auto;
    padding-top: var(--s3);
    border-top: 1px solid var(--hairline);
}

.kana-marks { display: flex; flex-wrap: wrap; gap: var(--s3); }
.kana-mark {
    display: flex;
    align-items: center;
    gap: var(--s3);
    padding: var(--s3) var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    max-width: 380px;
}
.kana-mark-name { font-size: 14px; font-weight: 600; }
.kana-mark-note { font-size: 12.5px; color: var(--muted); line-height: 1.45; }
.kana-foot { font-size: 12.5px; color: var(--muted); }
.kana-foot b { color: var(--ochre); }

/* ----------------------------------------------------------- grammar -- */

/* The shelf. Cards rather than a list because a block is a place you go, and
   the blurb is doing the work of telling you whether it is the one you want. */
.grammar-page { padding: var(--s6) var(--s5) var(--s10); display: grid; gap: var(--s6); }
.grammar-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
    gap: var(--s4);
    align-content: start;
}

.grammar-card {
    display: flex;
    align-items: flex-start;
    gap: var(--s4);
    padding: var(--s5);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-sm);
    color: inherit;
    text-decoration: none;
    transition: border-color .12s, background .12s, transform .08s;
}
.grammar-card:hover { border-color: var(--indigo-line); background: var(--indigo-soft); }
.grammar-card:active { transform: scale(.995); }
.grammar-card:focus-visible { outline: 2px solid var(--indigo); outline-offset: 2px; }

.grammar-card-icon {
    flex: none;
    display: grid;
    place-items: center;
    width: 40px;
    height: 40px;
    border-radius: var(--radius);
    background: var(--indigo-soft);
    color: var(--indigo);
}
.grammar-card-icon svg {
    width: 22px;
    height: 22px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}

.grammar-card-body { display: grid; gap: var(--s2); min-width: 0; }
.grammar-card-title { font-size: 17px; font-weight: 600; line-height: 1.3; }
.grammar-card-blurb {
    font-size: 13.5px;
    line-height: 1.55;
    color: var(--muted);
}
.grammar-card-covers { display: flex; flex-wrap: wrap; gap: var(--s1); }

/* Points the way without being a second thing to aim at — the whole card is
   the target. */
.grammar-card-go { flex: none; align-self: center; color: var(--muted); }
.grammar-card-go svg {
    width: 18px;
    height: 18px;
    fill: none;
    stroke: currentColor;
    stroke-width: 1.7;
    stroke-linecap: round;
    stroke-linejoin: round;
}
.grammar-card:hover .grammar-card-go { color: var(--indigo); }

/* ------------------------------------------------------------- verbs -- */

/* A measure, so the page does not stretch a centred derivation across a
   two-thousand-pixel monitor with the reading of it lost somewhere in the
   middle. */
.verb-page {
    padding: var(--s6) var(--s5) var(--s10);
    display: grid;
    gap: var(--s8);
    width: 100%;
    max-width: 1180px;
    margin-inline: auto;
}
.verb-bench-head { display: grid; gap: var(--s1); margin-bottom: var(--s4); }

/* The answer being built on the left, what the form is for on the right. At
   full width the stage put its content in the middle third and left the rest
   of the row empty, and the note had a screen's worth of nothing beside it.
   `align-items: start` keeps the note its own height rather than stretching it
   to match a stage that is taller. */
.verb-bench-body {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 360px);
    gap: var(--s6);
    align-items: start;
}

/* The pickers. Twenty verbs and sixteen forms is more than fits on a phone in
   one line, so they scroll sideways rather than wrapping into a wall — the row
   stays a row, and a row reads as something to choose from. */
.verb-picker { display: grid; gap: var(--s2); margin-bottom: var(--s5); }
.verb-picker-row {
    display: flex;
    gap: var(--s2);
    overflow-x: auto;
    padding-bottom: var(--s2);
    scrollbar-width: thin;
}
.verb-chip {
    flex: none;
    padding: var(--s2) var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-pill);
    background: var(--surface);
    color: var(--ink);
    font-family: var(--cjk);
    font-size: 16px;
    cursor: pointer;
    white-space: nowrap;
    transition: border-color .12s, background .12s, color .12s;
}
.verb-chip.is-form { font-family: inherit; font-size: 13px; }

/* Which class a verb is in is the only thing deciding how it conjugates, so
   the picker says it in front of the run of verbs it applies to rather than
   leaving twenty of them in one undifferentiated row. */
.verb-picker-label {
    flex: none;
    align-self: center;
    padding: 0 var(--s2);
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: .1em;
    text-transform: uppercase;
    color: var(--muted);
    white-space: nowrap;
}
.verb-picker-label.has-rule {
    margin-left: var(--s2);
    padding-left: var(--s4);
    border-left: 1px solid var(--hairline);
}
.verb-chip:hover { border-color: var(--indigo-line); }
.verb-chip.is-on {
    background: var(--indigo-soft);
    border-color: var(--indigo);
    color: var(--indigo);
    font-weight: 600;
}

/* The stage: where the rule happens. Given room, because it is the reason the
   page exists. */
.verb-stage {
    display: grid;
    gap: var(--s5);
    justify-items: center;
    padding: var(--s8) var(--s5);
    text-align: center;
}
.verb-stage-source { display: grid; gap: var(--s1); justify-items: center; }
.verb-stage-word { font-family: var(--cjk); font-size: 26px; line-height: 1.2; }
.verb-stage-meta { font-size: 12.5px; color: var(--muted); }

.verb-build {
    display: flex;
    flex-wrap: wrap;
    align-items: flex-start;
    justify-content: center;
    gap: var(--s2);
}
.verb-build-part {
    display: grid;
    gap: var(--s1);
    justify-items: center;
    padding: var(--s3) var(--s4);
    border-radius: var(--radius);
    border: 1px solid var(--hairline);
    background: var(--surface-2);
}
.verb-build-text { font-family: var(--cjk); font-size: clamp(26px, 3.4vw, 38px); line-height: 1.1; }
.verb-build-label {
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: .06em;
    color: var(--muted);
    white-space: nowrap;
}
.verb-build-join {
    align-self: center;
    font-size: 20px;
    color: var(--muted);
    padding-top: var(--s3);
}

/* Colour carries which piece did what: the stem stayed, the marked one moved.
   The border does the work as well as the text so it survives being read by
   somebody who cannot tell the two hues apart. */
.verb-build-part.is-stem { border-color: var(--hairline); }
.verb-build-part.is-shift,
.verb-build-part.is-fuse {
    border-color: var(--ochre);
    background: var(--surface);
}
.verb-build-part.is-shift .verb-build-text,
.verb-build-part.is-fuse .verb-build-text { color: var(--ochre); }
.verb-build-part.is-shift .verb-build-label,
.verb-build-part.is-fuse .verb-build-label { color: var(--ochre); }
.verb-build-part.is-suffix,
.verb-build-part.is-ending { border-color: var(--indigo-line); }
.verb-build-part.is-suffix .verb-build-text,
.verb-build-part.is-ending .verb-build-text { color: var(--indigo); }
.verb-build-part.is-irregular {
    border-color: var(--clay);
    border-style: dashed;
}
.verb-build-part.is-irregular .verb-build-text { color: var(--clay); }

.verb-stage-result { display: flex; align-items: center; gap: var(--s3); }
.verb-stage-arrow { font-size: 20px; color: var(--muted); }
.verb-stage-answer {
    border: 0;
    background: none;
    color: var(--ink);
    font-family: var(--cjk);
    font-size: clamp(30px, 4vw, 44px);
    line-height: 1.15;
    cursor: pointer;
    padding: var(--s1) var(--s2);
    border-radius: var(--radius);
    transition: background .12s;
}
.verb-stage-answer:hover { background: var(--indigo-soft); }
.verb-stage-answer.is-loading { opacity: .55; cursor: progress; }

.verb-stage-rule { font-size: 14px; line-height: 1.6; max-width: 52ch; }
.verb-stage-warning {
    font-size: 13px;
    line-height: 1.55;
    color: var(--ochre);
    max-width: 52ch;
    padding: var(--s3) var(--s4);
    border: 1px solid var(--ochre);
    border-radius: var(--radius);
}

/* What the chosen form is, when to reach for it, and it doing its job. The
   picker above says the name of a thing, which is the least useful fact about
   it; this says the rest. It sits beside the stage on a wide screen and under
   it on a narrow one. */
.verb-form-note {
    display: grid;
    gap: var(--s2);
    align-content: start;
    padding: var(--s4) var(--s5);
    border: 1px solid var(--hairline);
    border-left: 3px solid var(--indigo);
    border-radius: var(--radius);
    background: var(--surface);
}
.verb-form-note-head {
    display: flex;
    align-items: baseline;
    flex-wrap: wrap;
    gap: var(--s2) var(--s3);
}
.verb-form-note-name { font-size: clamp(16px, 1.5vw, 19px); font-weight: 600; }
.verb-form-note-jp { font-family: var(--cjk); font-size: 15px; color: var(--indigo); }
.verb-form-note-hint { font-size: 13px; color: var(--muted); }
.verb-form-note-what { font-size: 14px; line-height: 1.6; }
.verb-form-note-when { font-size: 13.5px; line-height: 1.6; color: var(--muted); }
.verb-form-note-when b {
    color: var(--ink);
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: .09em;
    text-transform: uppercase;
}

/* The form doing its job in a whole sentence, which is the only place a form
   means anything. Tappable, like every other bit of Japanese on these pages. */
.verb-form-note-example {
    display: grid;
    gap: 2px;
    margin-top: var(--s1);
    padding: var(--s3) var(--s4);
    border: 0;
    border-radius: var(--radius);
    background: var(--surface-2);
    color: var(--ink);
    text-align: left;
    cursor: pointer;
    transition: background .12s;
}
.verb-form-note-example:hover { background: var(--indigo-soft); }
.verb-form-note-example.is-loading { opacity: .55; cursor: progress; }
.verb-form-note-example.is-played { outline: 1px solid var(--indigo); }
.verb-form-note-jp-line { font-family: var(--cjk); font-size: 16px; line-height: 1.6; }
.verb-form-note-romaji { font-family: var(--mono); font-size: 11.5px; color: var(--muted); }
.verb-form-note-meaning { font-size: 12.5px; color: var(--muted); }

/* The old chart, kept but folded away. It is the thing to check once you know
   what you are looking for, not the thing to meet first. */
.verb-reference {
    padding: var(--s4) var(--s5);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
}
.verb-reference summary { cursor: pointer; font-size: 14px; font-weight: 600; }
.verb-reference > *:not(summary) { margin-top: var(--s3); }

.verb-rows { display: grid; gap: 2px; max-width: 560px; }
.verb-row {
    display: grid;
    grid-template-columns: 4.5rem repeat(6, minmax(0, 1fr));
    gap: var(--s2);
    align-items: center;
    padding: var(--s2) var(--s3);
    border-radius: var(--radius);
    background: var(--surface-2);
    font-family: var(--cjk);
    font-size: 15px;
}
.verb-row.is-head {
    background: none;
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: .09em;
    text-transform: uppercase;
    color: var(--muted);
}
.verb-row-ending { font-weight: 600; color: var(--indigo); }
.verb-row-te { color: var(--ochre); }

/* Kept for the drill's prompt, which shows a verb's reading and class under
   the word it is asking about. */
.verb-prompt-meta {
    font-size: 13px;
    color: var(--muted);
    text-align: center;
    margin-top: var(--s2);
}

/* --------------------------------------------------------- particles -- */

.confusions {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: var(--s4);
}
.confusion {
    display: grid;
    gap: var(--s3);
    align-content: start;
    padding: var(--s4);
    border: 1px solid var(--ochre);
    border-radius: var(--radius);
    background: var(--surface);
}
.confusion-title { font-family: var(--cjk); font-size: 18px; color: var(--ochre); }
.confusion-rule { font-size: 13px; line-height: 1.55; color: var(--muted); }
.confusion-examples { display: grid; gap: var(--s2); }
.confusion-example { display: grid; gap: 2px; }

.particle-list {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
    gap: var(--s4);
    align-items: start;
}
.particle-card {
    display: grid;
    gap: var(--s3);
    align-content: start;
    padding: var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface-2);
}
.particle-head { display: flex; align-items: center; gap: var(--s3); }
.particle-mark {
    flex: none;
    display: grid;
    place-items: center;
    width: 44px;
    height: 44px;
    border-radius: var(--radius);
    background: var(--indigo-soft);
    color: var(--indigo);
    font-family: var(--cjk);
    font-size: 22px;
}
.particle-head-body { display: grid; gap: 1px; min-width: 0; flex: 1; }
.particle-role { font-size: 14px; font-weight: 600; }
.particle-gloss { font-size: 12.5px; color: var(--muted); }
.particle-reading {
    font-family: var(--mono);
    font-size: 11.5px;
    color: var(--muted);
}
.particle-when { font-size: 13px; line-height: 1.55; color: var(--muted); }
.particle-note {
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--ochre);
    border-left: 3px solid var(--ochre);
    padding-left: var(--s3);
}

.particle-examples { display: grid; gap: var(--s2); }
.particle-example {
    display: grid;
    gap: 2px;
    padding: var(--s3);
    border: 0;
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    text-align: left;
    cursor: pointer;
    transition: background .12s;
}
.particle-example:hover { background: var(--indigo-soft); }
.particle-example.is-loading { opacity: .55; cursor: progress; }
.particle-example.is-played { outline: 1px solid var(--indigo); }
.particle-sentence { font-family: var(--cjk); font-size: 15px; line-height: 1.6; }
.particle-romaji { font-family: var(--mono); font-size: 11.5px; color: var(--muted); }
.particle-meaning { font-size: 12.5px; color: var(--muted); line-height: 1.45; }

/* The gap in the drill's sentence. Empty it is a rule to write on; filled it
   is the answer sitting where it belongs. */
.particle-prompt { line-height: 1.7; }
.particle-blank {
    display: inline-block;
    min-width: 2ch;
    margin: 0 .1em;
    border-bottom: 3px solid var(--indigo-line);
    text-align: center;
}
.particle-blank.is-filled { border-bottom-color: var(--celadon); color: var(--celadon); }
/* Filled in for you after a wrong answer, which is a different thing from
   having filled it in yourself. */
.particle-blank.is-corrected { border-bottom-color: var(--clay); color: var(--clay); }
.particle-prompt-meaning {
    font-size: 14px;
    color: var(--muted);
    text-align: center;
    margin-top: var(--s2);
}
.particle-why { font-size: 13px; line-height: 1.55; color: var(--muted); }

/* --------------------------------------------------- demonstratives -- */

/* The sections are grid items, and a grid item's default min-width is its
   min-content — which, for the section holding the twelve-word table, is the
   whole table. Without this the table refuses to shrink on a phone and drags
   the page sideways instead of scrolling inside its own box. Scoped to this
   page: the kana and particle charts are grid items of the same wrapper and
   have no reason to be touched. */
.koso-page .kana-group { min-width: 0; }

/* The four distances are the page's one idea, so they get colour and every
   word on the page inherits it: a こ card, a こ heading and a こ cell are all
   the same hue. The reader is meant to stop reading the labels quite quickly
   and go by the colour, which only works if it never means anything else. */
.koso-distance[data-distance="ko"], .koso-word[data-distance="ko"] { --koso: var(--indigo); }
.koso-distance[data-distance="so"], .koso-word[data-distance="so"] { --koso: var(--celadon); }
.koso-distance[data-distance="a"], .koso-word[data-distance="a"] { --koso: var(--ochre); }
.koso-distance[data-distance="do"], .koso-word[data-distance="do"] { --koso: var(--clay); }

/* The distances follow .kana-group-head, which already carries the gap.
   The words follow the series rule instead, so they supply their own. */
.koso-distances,
.koso-words {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
    gap: var(--s3);
}
.koso-words { margin-top: var(--s4); }

.koso-distance {
    display: flex;
    align-items: center;
    gap: var(--s3);
    padding: var(--s3) var(--s4);
    border: 1px solid var(--hairline);
    border-left: 3px solid var(--koso);
    border-radius: var(--radius);
    background: var(--surface);
}
.koso-prefix {
    font-family: var(--cjk);
    font-size: 30px;
    line-height: 1;
    flex: none;
    color: var(--koso);
}
.koso-distance-body { display: grid; gap: 2px; min-width: 0; }
.koso-distance-label { font-size: 14px; font-weight: 600; }
.koso-distance-when { font-size: 12.5px; line-height: 1.45; color: var(--muted); }

/* All twelve words at once. A table because it is one — the reading is across
   a row, and the row header says which shape the row is. */
.koso-grid-wrap { overflow-x: auto; }
.koso-grid {
    width: 100%;
    min-width: 560px;
    border-collapse: collapse;
}
.koso-grid th, .koso-grid td {
    padding: var(--s3);
    text-align: left;
    vertical-align: top;
    border-bottom: 1px solid var(--hairline);
}
.koso-grid thead th { border-bottom-width: 2px; }
.koso-grid tbody tr:last-child th,
.koso-grid tbody tr:last-child td { border-bottom: 0; }
.koso-grid-prefix { font-family: var(--cjk); font-size: 20px; line-height: 1.2; display: block; }
.koso-grid-head {
    display: block;
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
    font-weight: 400;
}
/* The column colours again, applied to whole columns so a glance down the
   table finds the one it wants. nth-child is safe here because the header and
   the body are generated from the same list, in the same order. */
.koso-grid tr > *:nth-child(2) .koso-grid-prefix,
.koso-grid tr > *:nth-child(2) .koso-grid-word { color: var(--indigo); }
.koso-grid tr > *:nth-child(3) .koso-grid-prefix,
.koso-grid tr > *:nth-child(3) .koso-grid-word { color: var(--celadon); }
.koso-grid tr > *:nth-child(4) .koso-grid-prefix,
.koso-grid tr > *:nth-child(4) .koso-grid-word { color: var(--ochre); }
.koso-grid tr > *:nth-child(5) .koso-grid-prefix,
.koso-grid tr > *:nth-child(5) .koso-grid-word { color: var(--clay); }

.koso-grid-role {
    font-family: var(--mono);
    font-size: 11px;
    font-weight: 400;
    color: var(--muted);
    white-space: nowrap;
}
.koso-grid-word { font-family: var(--cjk); font-size: 19px; line-height: 1.3; display: block; }
.koso-grid-reading { font-family: var(--mono); font-size: 11px; color: var(--muted); display: block; }
.koso-grid-gloss { font-size: 12.5px; color: var(--muted); display: block; margin-top: 2px; }

/* The rule for a whole series — the one sentence that stops the shape being
   used in the wrong slot, so it is louder than the prose around it. */
.koso-rule {
    margin-top: var(--s3);
    padding: var(--s3) var(--s4);
    border-left: 3px solid var(--indigo);
    border-radius: 0 var(--radius) var(--radius) 0;
    background: var(--indigo-soft);
    font-size: 13.5px;
    line-height: 1.55;
}
.koso-warning {
    margin-top: var(--s3);
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--muted);
    max-width: 62ch;
}

.koso-word {
    display: grid;
    gap: var(--s2);
    align-content: start;
    padding: var(--s3) var(--s4);
    border: 1px solid var(--hairline);
    border-top: 3px solid var(--koso);
    border-radius: var(--radius);
    background: var(--surface);
}
.koso-word-head { display: flex; align-items: baseline; gap: var(--s3); }
.koso-word-mark {
    font-family: var(--cjk);
    font-size: 24px;
    line-height: 1.2;
    color: var(--koso);
    flex: none;
}
.koso-word-body { display: grid; gap: 1px; min-width: 0; }
.koso-word-gloss { font-size: 14px; font-weight: 600; }
.koso-word-reading { font-family: var(--mono); font-size: 11px; color: var(--muted); }
.koso-word-when { font-size: 12.5px; line-height: 1.5; color: var(--muted); }

.koso-examples {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
    gap: var(--s2);
    margin-top: var(--s4);
}

/* Right and wrong, one above the other. The wrong one is worth printing
   because it is the sentence the reader is about to write. */
.koso-good { font-family: var(--cjk); font-size: 15px; line-height: 1.6; }
.koso-bad {
    font-family: var(--cjk);
    font-size: 14px;
    line-height: 1.6;
    color: var(--clay);
    /* Struck through in its own colour rather than a softer one: --clay-soft
       is a background token and in the dark theme it is nearly the page
       colour, which left the line invisible on the half of the app that
       needs it most. */
    text-decoration: line-through;
    text-decoration-thickness: 1px;
}

/* -------------------------------------------------------- adjectives -- */

/* The adjectives chart borrows the This and that page's pieces whole. What
   it adds is the いい tiles: eleven short forms, lost in columns sized for
   example sentences, so they get narrower ones and a surface of their own.
   The forms built from よい are marked down the edge in the warning colour,
   since those are the ones a beginner gets wrong. */
.adj-ii { grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); }
.adj-ii .confusion-example {
    padding: var(--s3);
    border-left: 3px solid var(--hairline);
    border-radius: var(--radius-sm);
    background: var(--surface);
}
.adj-ii .confusion-example.is-from-yoi { border-left-color: var(--ochre); }

/* ----------------------------------------------------------- numbers -- */

/* Every tappable thing on the numbers page is one of these: a figure, its
   reading, and a hit area big enough to mean it. They differ in what else they
   carry, so the shared part is here and the rest is per block. */
.num-cell,
.num-rank,
.num-change,
.num-row {
    display: flex;
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    color: var(--ink);
    cursor: pointer;
    text-align: left;
    transition: border-color .12s, background .12s, transform .08s;
}
.num-cell:hover,
.num-rank:hover,
.num-change:hover,
.num-row:hover { border-color: var(--indigo-line); background: var(--indigo-soft); }
.num-cell:active,
.num-rank:active,
.num-change:active,
.num-row:active { transform: scale(.98); }
.num-cell.is-loading,
.num-rank.is-loading,
.num-change.is-loading,
.num-row.is-loading { opacity: .55; cursor: progress; }
.num-cell.is-played,
.num-rank.is-played,
.num-change.is-played,
.num-row.is-played { border-color: var(--indigo); }

.num-reading { font-family: var(--cjk); font-size: 14px; line-height: 1.35; }
.num-note { font-size: 12.5px; color: var(--muted); line-height: 1.45; }

/* The five sound changes are marked in every table they appear in, so a
   learner scanning the hundreds sees at a glance which three to be careful
   about. Colour alone would not say it, so the marked ones also carry a rule
   down their leading edge. */
.num-reading.is-change,
.num-row.is-change .num-reading { color: var(--ochre); font-weight: 600; }
.num-row.is-change { border-left: 3px solid var(--ochre); }

.num-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
    gap: var(--s2);
}
.num-cell {
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 2px;
    min-height: 96px;
    padding: var(--s3) var(--s2);
    text-align: center;
}
.num-figure { font-family: var(--mono); font-size: 24px; font-weight: 600; }
.num-kanji { font-family: var(--cjk); font-size: 15px; color: var(--muted); }
.num-alt { font-family: var(--cjk); font-size: 12px; color: var(--muted); }

.num-ranks {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
    gap: var(--s2);
}
.num-rank { align-items: center; gap: var(--s3); padding: var(--s3) var(--s4); }
.num-rank-kanji { font-family: var(--cjk); font-size: 30px; line-height: 1; flex: none; }
.num-rank-body { display: grid; gap: 1px; min-width: 0; }
.num-rank-figure {
    font-family: var(--mono);
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    color: var(--muted);
}

.num-changes {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
    gap: var(--s2);
}
.num-change {
    flex-direction: column;
    gap: 2px;
    padding: var(--s3) var(--s4);
    border-left: 3px solid var(--ochre);
}
.num-change-figure {
    font-family: var(--mono);
    font-size: 20px;
    font-weight: 600;
    font-variant-numeric: tabular-nums;
}
.num-change-not {
    font-family: var(--cjk);
    font-size: 12.5px;
    color: var(--muted);
    text-decoration: line-through;
}

.num-tables {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
    gap: var(--s6);
    align-items: start;
}
.num-table { display: grid; gap: var(--s1); align-content: start; }
.num-table .label { margin-bottom: var(--s1); }
.num-row {
    align-items: baseline;
    gap: var(--s3);
    padding: var(--s2) var(--s3);
}
.num-row-figure {
    flex: none;
    min-width: 4.5ch;
    font-family: var(--mono);
    font-size: 13px;
    font-variant-numeric: tabular-nums;
    color: var(--muted);
}

.num-counters {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
    gap: var(--s4);
}
.num-counter {
    display: grid;
    gap: var(--s3);
    align-content: start;
    padding: var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface-2);
}
.num-counter-head { display: flex; align-items: center; gap: var(--s3); }
.num-counter-kanji { font-family: var(--cjk); font-size: 28px; line-height: 1; flex: none; }
.num-counter-body { display: grid; gap: 1px; min-width: 0; }
.num-counter-what { font-size: 12.5px; color: var(--muted); line-height: 1.45; }
.num-counter-rows { display: grid; gap: var(--s1); }

/* The characters a word is written with, under the word itself. */
.kanji-title { padding-top: var(--s4); border-top: 1px solid var(--hairline); }
.kanji-list { display: grid; gap: var(--s3); margin-top: var(--s3); }
.kanji-item {
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    padding: var(--s3) var(--s4) var(--s4);
}
.kanji-head { display: flex; gap: var(--s3); align-items: flex-start; }
.kanji-char {
    font-family: var(--cjk);
    font-size: 34px;
    line-height: 1.1;
    flex: none;
}
.kanji-sense { min-width: 0; }
.kanji-meaning { font-family: var(--read); font-size: 15px; line-height: 1.35; }
.kanji-readings {
    font-family: var(--cjk);
    font-size: 12.5px;
    color: var(--muted);
    margin-top: 2px;
    word-break: break-word;
}
.kanji-readings b {
    font-family: var(--mono);
    font-size: 10.5px;
    text-transform: uppercase;
    letter-spacing: .06em;
    color: var(--muted);
    margin-right: var(--s2);
}
.kanji-tags { display: flex; flex-wrap: wrap; gap: var(--s1); margin-top: var(--s3); }
.kanji-note {
    font-size: 12.5px;
    line-height: 1.5;
    color: var(--muted);
    margin-top: var(--s3);
}
.kanji-keep { width: 100%; margin-top: var(--s3); }
.kanji-loading {
    display: flex;
    align-items: center;
    gap: var(--s2);
    font-size: 12.5px;
    color: var(--muted);
    margin-top: var(--s3);
}
.kanji-limit { font-size: 12px; color: var(--muted); margin-top: var(--s3); }

.legend { display: flex; flex-wrap: wrap; gap: var(--s3); font-size: 12px; color: var(--muted); }
.legend i { display: inline-block; width: 18px; border-bottom: 1.5px solid; margin-right: var(--s1); vertical-align: 4px; }

.picker { position: relative; }
.picker-panel {
    position: absolute;
    z-index: 30;
    top: calc(100% + var(--s1));
    left: 0;
    width: 220px;
    padding: var(--s2);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    box-shadow: var(--shadow-lg);
    display: grid;
    gap: var(--s1);
    max-height: 280px;
    overflow-y: auto;
}
.picker-item {
    display: flex;
    align-items: center;
    gap: var(--s2);
    padding: var(--s1) var(--s2);
    border-radius: var(--radius-sm);
    font-size: 13.5px;
    cursor: pointer;
}
.picker-item:hover { background: var(--surface-2); }
.picker-item input { accent-color: var(--indigo); }

/* =============================================================== DRILL == */

/* The drill claims the height left under the head, so it has to use it. It
   used to pin the card to the top of that box and leave the rest empty. */
.drill { flex: 1; display: grid; place-items: start center; padding: var(--s8) var(--s5); }
.drill-inner {
    width: 100%;
    /* Grows with the window between about 1100 and 1600px, along with the
       prompt and the answer buttons below, so the card scales as one thing.
       440 was chosen for a laptop and looks marooned on anything larger. */
    max-width: clamp(440px, 40vw, 560px);
    display: grid;
    gap: var(--s4);
    /* Centred by auto margins rather than by place-items, because the tallest
       card — the alphabet drill with its deck panel open and the drawing
       square — is taller than a laptop screen. Auto margins take the free
       space when there is some and collapse to nothing when there is not, so
       a card too big for the window starts at the top where it can be reached
       instead of hanging off both ends. */
    margin-block: auto;
}

/* The card is the object on this screen. The counter above it and the buttons
   below it are not, and they were sitting flush against it — the spacing had
   been added per footer as each drill needed it, and the typed footer that the
   verbs and numbers drills use had never been given any. Spacing the whole
   stack from the container covers every drill and every one of the footers,
   including the ones added next. */
#state-card { display: grid; gap: var(--s5); }

.drill-head { display: flex; align-items: center; gap: var(--s3); }
.progress { flex: 1; height: 4px; border-radius: 2px; background: var(--surface-3); overflow: hidden; }
.progress > i { display: block; height: 100%; width: 0; background: var(--indigo); transition: width .25s; }
.count { font-family: var(--mono); font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }

.drill-card { padding: var(--s6) var(--s5); text-align: center; display: grid; gap: var(--s5); }
.drill-prompt { font-family: var(--cjk); font-size: clamp(40px, 3.6vw, 56px); line-height: 1.2; word-break: break-word; }
.drill-prompt[data-latin="true"] { font-family: var(--read); }
.drill-prompt-sub { font-family: var(--mono); font-size: 11px; letter-spacing: .08em; text-transform: uppercase; color: var(--muted); margin-top: var(--s1); }

.options { display: grid; grid-template-columns: 1fr 1fr; gap: var(--s2); }
.option {
    /* Floor is the touch target; the cap holds two lines of a long meaning
       without the button becoming a slab. */
    min-height: clamp(var(--tap), 4vw, 64px);
    padding: var(--s2) var(--s3);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface-2);
    color: var(--ink);
    font-family: var(--read);
    font-size: clamp(15.5px, 1.4vw, 17.5px);
    cursor: pointer;
    display: flex;
    align-items: center;
    justify-content: center;
    gap: var(--s2);
    text-align: center;
    transition: border-color .12s, background .12s;
}
.option:hover:not([disabled]) { border-color: var(--indigo-line); }
.option kbd {
    font-family: var(--mono);
    font-size: 10px;
    color: var(--muted);
    border: 1px solid var(--hairline);
    border-radius: 3px;
    padding: 1px 4px;
    background: var(--surface);
    flex: none;
}
.option.is-right { background: var(--celadon-soft); border-color: var(--celadon); color: var(--celadon); }
.option.is-wrong { background: var(--clay-soft); border-color: var(--clay); color: var(--clay); }
.option[disabled] { cursor: default; }

.drill-answer { display: grid; gap: var(--s2); padding-top: var(--s4); border-top: 1px solid var(--hairline); }
.drill-answer-romaji { font-family: var(--mono); font-size: 13px; color: var(--muted); }
.drill-answer-meaning { font-family: var(--read); font-size: 16px; }

/* A word written with kanji, spelt out in kana: under the word a card asks
   you to read, under each word on a Find button (and under the verb a verb
   card's answer names, on the prompt or a button), and beside the word in
   the drill's answer. It is there to say the word by, not to be looked at, so it
   is smaller and quieter than the word, and in the kana face whatever the
   face of what it sits in. Shared by the word drill and every Learn page. */
.prompt-with-kana { min-width: 0; max-width: 100%; }
.word-kana {
    display: block;
    font-family: var(--cjk);
    font-size: clamp(17px, 1.6vw, 21px);
    font-weight: 400;
    line-height: 1.35;
    color: var(--muted);
    overflow-wrap: anywhere;
}
/* Held close under the prompt: the card's own gaps would set it apart. */
.prompt-kana { margin-top: var(--s1); }
/* On a button, small enough that four of them keep their shape and the
   number stays the only thing beside the word. Marked right or wrong, it
   takes the button's colour with the rest of it. */
.option .word-kana { font-size: 13.5px; line-height: 1.3; margin-top: 2px; }
.option.is-right .word-kana,
.option.is-wrong .word-kana { color: inherit; opacity: .8; }
/* In the answer it follows the word on the same line. */
.drill-answer-meaning .word-kana { display: inline; font-size: 15px; margin-left: .4em; }

.drill-foot { display: flex; gap: var(--s2); }

/* The draw card is taller than the others and its own contents are spaced
   further apart, so the gap that suits the rest reads as tight under it. */
.draw-foot { margin-top: var(--s2); }

.drill-help { display: grid; justify-items: center; gap: var(--s3); }
.drill-help-toggle {
    width: 26px; height: 26px;
    border: 1px solid var(--hairline);
    border-radius: 50%;
    background: none;
    color: var(--faint);
    font-size: 13px;
    line-height: 1;
    cursor: pointer;
    display: grid;
    place-items: center;
    transition: color .12s, border-color .12s;
}
.drill-help-toggle:hover { color: var(--indigo); border-color: var(--indigo-line); }
.drill-help-toggle[aria-expanded="true"] {
    color: var(--indigo);
    border-color: var(--indigo-line);
    background: var(--indigo-soft);
}

.scale {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    justify-content: center;
    font-family: var(--mono);
    font-size: 10.5px;
    color: var(--muted);
}
.scale span { padding: 2px var(--s2); border-radius: var(--radius-pill); background: var(--surface-2); }

/* ========================================================== DICTIONARY == */

.dict-tools {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    align-items: center;
    padding: var(--s3) var(--s5);
    border-bottom: 1px solid var(--hairline);
    background: var(--surface);
}
.dict-search { flex: 1; min-width: 180px; height: var(--s8); border-radius: var(--radius-pill); padding: 0 var(--s4); }
.dict-select { width: auto; height: var(--s8); border-radius: var(--radius-pill); font-size: 13px; padding: 0 var(--s6) 0 var(--s3); }

/* A select is as wide as its longest option, and a list's name can be forty
   characters with its count after it. On a phone that is wider than the bar
   and takes the page sideways. Capped at the row, the closed select cuts the
   name short; the open menu still shows it whole. */
.dict-tools .select-wrap,
.kanji-filters .select-wrap { min-width: 0; max-width: 100%; }
.dict-tools .dict-select,
.kanji-filters .dict-select { max-width: 100%; text-overflow: ellipsis; }

/* The filter pills opt down to 13px, which puts them back under the zoom
   threshold the base rule just cleared. They are the most-tapped controls on
   the page, so they cannot be the exception. */
@media (pointer: coarse) {
    .dict-select { font-size: 16px; }
}

.dict-word { font-family: var(--cjk); font-size: 19px; }
.dict-word[data-latin="true"] { font-family: var(--read); }
.dict-romaji { font-family: var(--mono); font-size: 12px; color: var(--muted); }
.dict-verb { display: block; font-size: 11px; color: var(--faint); }

/* The lists a word is in, in the row rather than only behind a click. Sized
   and coloured like the chips in the word panel, so the same fact looks the
   same in both places. */
.dict-lists {
    display: flex;
    flex-wrap: wrap;
    gap: 2px var(--s1);
    max-width: 20ch;
}
.dict-list {
    display: inline-block;
    margin: 1px var(--s1) 1px 0;
    padding: 2px var(--s2);
    border-radius: var(--radius-pill);
    background: var(--indigo-soft);
    color: var(--indigo);
    font-size: 11.5px;
    line-height: 1.5;
    white-space: nowrap;
}
.dict-meaning { font-family: var(--read); font-size: 15px; }

.bars { display: inline-flex; gap: 2px; vertical-align: middle; }
.bars i { width: 5px; height: 13px; border-radius: 1px; background: var(--surface-3); }
.bars i.on { background: var(--celadon); }

.due { font-family: var(--mono); font-size: 11.5px; font-variant-numeric: tabular-nums; white-space: nowrap; }
.due-now { color: var(--clay); }
.due-soon { color: var(--ochre); }
.due-later { color: var(--muted); }

/* ============================================================= LIBRARY == */

.tag-bar {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2);
    padding: var(--s3) var(--s5);
    border-bottom: 1px solid var(--hairline);
}

.lib-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
    gap: var(--s4);
    padding: var(--s5);
    align-content: start;
}

.lib-card {
    display: flex;
    flex-direction: column;
    gap: var(--s3);
    padding: var(--s5);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-lg);
    background: var(--surface);
    box-shadow: var(--shadow-sm);
}
.lib-card:hover { border-color: var(--indigo-line); }

.lib-card-link { text-decoration: none; color: inherit; display: grid; gap: var(--s2); }
.lib-title { font-size: 17px; line-height: 1.3; }
.lib-preview {
    font-family: var(--cjk);
    font-size: 15px;
    line-height: 1.7;
    color: var(--muted);
    display: -webkit-box;
    -webkit-line-clamp: 3;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.lib-preview[data-latin="true"] { font-family: var(--read); line-height: 1.5; }

.lib-meta,
.lib-tags {
    display: flex;
    flex-wrap: wrap;
    gap: var(--s2) var(--s3);
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: .07em;
    text-transform: uppercase;
    color: var(--faint);
}
.lib-tags { text-transform: none; letter-spacing: 0; }
.lib-tags .pill { text-decoration: none; }

.lib-foot { display: flex; align-items: center; gap: var(--s2); margin-top: auto; }
.lib-rating { font-size: 13px; color: var(--ochre); font-variant-numeric: tabular-nums; }
.lib-rating-count { color: var(--faint); }
.lib-rating-none { color: var(--faint); }
.lib-owner { font-size: 12px; color: var(--faint); }

.lib-progress { display: flex; align-items: center; gap: var(--s2); }
.lib-progress-bar { display: flex; align-items: center; gap: var(--s3); margin-bottom: var(--s5); max-width: 52em; }

/* ---- the paragraph reader ---- */

.lib-passage { padding-bottom: var(--s12); }
.lib-hint { max-width: 52em; margin-bottom: var(--s6); }

.para {
    max-width: 52em;
    padding: var(--s4) var(--s4) var(--s5);
    margin-bottom: var(--s4);
    border: 1px solid transparent;
    border-radius: var(--radius);
    cursor: pointer;
    transition: border-color .12s, background .12s;
}
.para:hover { border-color: var(--hairline); background: var(--surface); }
.para.is-read { border-color: var(--hairline); }

/* Only a words load dims the paragraph, because that call replaces the text.
   Translating leaves it readable — you may still be reading it while it runs. */
.para.is-loading { cursor: progress; }
.para.is-loading .para-body { opacity: .5; }

.para-status {
    display: inline-flex;
    align-items: center;
    gap: var(--s2);
    font-family: var(--mono);
    font-size: 10.5px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--indigo);
}

.spinner {
    width: 13px;
    height: 13px;
    flex: none;
    border: 2px solid var(--indigo-line);
    border-top-color: var(--indigo);
    border-radius: 50%;
    animation: spin .6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }

/* With motion reduced the ring cannot spin, so the wording carries it and the
   ring pulses gently instead of sitting there looking frozen. */
@media (prefers-reduced-motion: reduce) {
    .spinner { animation: none; border-top-color: var(--indigo); opacity: .8; }
}

.para-head {
    display: flex;
    align-items: center;
    gap: var(--s3);
    margin-bottom: var(--s2);
}
.para-number {
    font-family: var(--mono);
    font-size: 11px;
    color: var(--faint);
    min-width: 2ch;
    font-variant-numeric: tabular-nums;
}
/* Script toggle: icon only, with its name on hover and on focus. */
.para-script {
    position: relative;
    width: 32px;
    height: 28px;
    border-radius: var(--radius-pill);
}
.para-script svg { width: 19px; height: 19px; }
.para-script[aria-pressed="true"] {
    background: var(--indigo-soft);
    border-color: var(--indigo-line);
    color: var(--indigo);
}

.para-script-label {
    position: absolute;
    bottom: calc(100% + 6px);
    left: 50%;
    transform: translateX(-50%);
    padding: var(--s1) var(--s2);
    border-radius: var(--radius-sm);
    background: var(--ink);
    color: var(--washi);
    font-family: var(--ui);
    font-size: 11px;
    letter-spacing: 0;
    text-transform: none;
    white-space: nowrap;
    opacity: 0;
    pointer-events: none;
    transition: opacity .12s;
    z-index: 20;
}
.para-script:hover .para-script-label,
.para-script:focus-visible .para-script-label { opacity: 1; }

/* A rewritten paragraph is still tappable: the conversion comes back split
   into words, each mapping back to its gloss. */

.para-body { cursor: pointer; }
.para .word { cursor: pointer; }

.para-gloss {
    display: grid;
    gap: var(--s4);
    margin-top: var(--s4);
    padding-top: var(--s4);
    border-top: 1px solid var(--hairline);
}
.para-transcription { font-family: var(--mono); font-size: 13.5px; color: var(--muted); }
.para-translation { font-family: var(--read); font-size: 16.5px; line-height: 1.6; }

/* ---- ratings ---- */

.rating { display: inline-flex; align-items: center; gap: 2px; }
.rating-star {
    border: 0;
    background: none;
    color: var(--surface-3);
    font-size: 18px;
    line-height: 1;
    padding: 2px;
    cursor: pointer;
    transition: color .12s;
}
.rating-star[aria-pressed="true"] { color: var(--ochre); }
.rating:hover .rating-star { color: var(--ochre); }
.rating-star:hover ~ .rating-star { color: var(--surface-3); }
.rating-summary {
    margin-left: var(--s2);
    font-family: var(--mono);
    font-size: 11px;
    color: var(--muted);
    font-variant-numeric: tabular-nums;
}

/* ---- the upload form ---- */

.lib-form { max-width: 720px; }
.lib-body { height: auto; min-height: 320px; padding: var(--s3); line-height: 1.6; resize: vertical; }
.lib-share { width: fit-content; }
.field-hint.is-over { color: var(--clay); font-weight: 600; }

.chip-icon { width: 15px; height: 15px; fill: none; stroke: currentColor; stroke-width: 1.7; stroke-linecap: round; stroke-linejoin: round; }

/* ============================================================= ACCOUNT == */

.account { display: grid; grid-template-columns: 232px minmax(0, 1fr); flex: 1; min-height: 0; }

.tabs {
    display: flex;
    flex-direction: column;
    gap: var(--s1);
    padding: var(--s4);
    border-right: 1px solid var(--hairline);
    background: var(--surface-2);
}
.tab {
    display: grid;
    gap: 2px;
    padding: var(--s3);
    border: 1px solid transparent;
    border-radius: var(--radius);
    color: var(--ink);
    text-decoration: none;
}
.tab:hover { background: var(--surface-3); }
.tab[aria-current="page"] { background: var(--indigo-soft); border-color: var(--indigo-line); }
.tab-name { font-size: 14px; font-weight: 600; }
.tab[aria-current="page"] .tab-name { color: var(--indigo); }
.tab-meta { font-size: 12px; color: var(--muted); }

.panel { padding: var(--s6) var(--s6) var(--s10); min-width: 0; display: grid; gap: var(--s5); align-content: start; }
.panel-head { display: flex; align-items: center; gap: var(--s3); flex-wrap: wrap; }
.panel-title { font-size: 17px; }

.stats { display: flex; flex-wrap: wrap; gap: var(--s2); }
.stat {
    padding: var(--s3) var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    min-width: 128px;
}
.stat-value { font-size: 22px; font-weight: 600; font-variant-numeric: tabular-nums; }
.stat-value small { font-size: 13px; font-weight: 400; color: var(--muted); }

.form-grid { display: grid; gap: var(--s4); max-width: 480px; }
.check-row { display: flex; flex-wrap: wrap; gap: var(--s2); }

.saved-list { display: grid; gap: var(--s2); }
.saved-item {
    display: flex;
    align-items: flex-start;
    gap: var(--s3);
    padding: var(--s4);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface);
    color: inherit;
}
.saved-item:hover { border-color: var(--indigo-line); background: var(--surface-2); }

/* The card opens the text; Share is a separate destination, so the two are
   separate links rather than one nested inside the other. */
.saved-open {
    display: flex;
    flex: 1;
    min-width: 0;
    text-decoration: none;
    color: inherit;
}
.saved-share { flex: none; align-self: center; }
.saved-body { min-width: 0; flex: 1; display: grid; gap: var(--s1); }
.saved-text { font-family: var(--cjk); font-size: 17px; line-height: 1.7; }
.saved-text[data-latin="true"] { font-family: var(--read); }
.saved-translation { font-family: var(--read); font-size: 14px; color: var(--muted); }
.saved-meta {
    font-family: var(--mono);
    font-size: 10px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--faint);
    display: flex;
    flex-wrap: wrap;
    gap: var(--s3);
}

/* ================================================================ AUTH == */

.auth-page { display: grid; place-items: center; min-height: 100vh; min-height: 100dvh; padding: var(--s6); }
.auth-card { width: min(420px, 100%); padding: var(--s8); display: grid; gap: var(--s5); }
.auth-brand { font-family: var(--mono); font-size: 11px; letter-spacing: .14em; text-transform: uppercase; color: var(--muted); }
.auth-title { font-size: 26px; letter-spacing: -.01em; }
.auth-sub { color: var(--muted); font-size: 14px; margin-top: var(--s1); }
.auth-switch { font-size: 13.5px; color: var(--muted); text-align: center; }

/* ========================================================== RESPONSIVE == */

@media (max-width: 1020px) {
    .reader { grid-template-columns: minmax(0, 1fr); }
    /* The stage needs the whole width once there is not much of it. */
    .verb-bench-body { grid-template-columns: minmax(0, 1fr); }
    .gloss {
        border-left: 0;
        border-top: 1px solid var(--hairline);
        position: sticky;
        /* Undo the full-height sidebar above: down here it is a sheet held to
           the bottom of the screen, and a column that is one viewport tall
           held to the top is the opposite of that. */
        top: auto;
        /* And sat at the bottom of its row rather than stretched across it. A
           stretched item that cannot fill its row — which is any item with a
           max-height, so this one always — is laid out at the row's start, and
           the shorter the sheet the further it floated above the tab bar.
           Anchoring the end instead keeps it against the bottom edge at either
           of its two heights, while still taking up room in the flow so the
           passage never runs underneath it. */
        align-self: end;
        height: auto;
        bottom: 0;
        max-height: 46vh;
        overflow-y: auto;
        padding-bottom: calc(var(--s5) + env(safe-area-inset-bottom));
        box-shadow: 0 -8px 24px -18px rgba(0, 0, 0, .6);
        /* Between its two heights. Both ends are real lengths, so this
           animates; the sheet dropping out of the way rather than vanishing is
           what says it is still there to bring back. */
        transition: max-height .22s ease;
    }
    /* Once the gloss is a bottom sheet its height is capped, and the legend and
       the kept-count are reference material rather than something you act on.
       The word itself gets that space instead. */
    .gloss-foot { display: none; }

    /* The grip carries the heading down here, so the heading itself would be
       one of two. */
    .gloss > .label { display: none; }

    .gloss-grip {
        display: flex;
        align-items: center;
        gap: var(--s3);
        /* Out to the sheet's edges: a handle you have to aim for is not much
           of a handle. The negative margins undo the sheet's padding, and
           sticking to the top keeps it in reach after the gloss has been
           scrolled. */
        margin: calc(var(--s5) * -1) calc(var(--s5) * -1) 0;
        padding: var(--s3) var(--s5);
        position: sticky;
        top: 0;
        z-index: 1;
        border: 0;
        background: var(--surface-2);
        color: var(--ink);
        font-family: var(--cjk);
        font-size: 13px;
        text-align: left;
        cursor: pointer;
        /* No double-tap zoom to wait on before the tap counts. */
        touch-action: manipulation;
    }
    .gloss-grip-bar {
        flex: none;
        width: 32px;
        height: 4px;
        border-radius: var(--radius-pill);
        background: var(--hairline);
    }
    .gloss-grip-label {
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
    }
    .gloss-grip-label[data-latin="true"] { font-family: var(--read); }

    /* Down: capped at the grip, which is measured as it goes so the figure is
       right whatever the type does. Scrolling goes with it — there is nothing
       to scroll to, and a sheet that moved under the finger while it was shut
       would just be a smaller version of the problem. */
    .gloss.is-peeked {
        max-height: var(--gloss-peek, 44px);
        overflow: hidden;
    }
    .passage { padding: var(--s6) var(--s5) var(--s8); }
}

@media (max-width: 860px) {
    .account { grid-template-columns: minmax(0, 1fr); }
    .tabs {
        flex-direction: row;
        overflow-x: auto;
        border-right: 0;
        border-bottom: 1px solid var(--hairline);
        padding: var(--s3) var(--s4);
    }
    .tab { flex: none; }
    .tab-meta { display: none; }
    .panel { padding: var(--s5) var(--s4) var(--s10); }
}

/* Two five-cell blocks need about 800px between them and the rail. Below that
   the voiced row drops beneath the row it comes from — still next to its
   origin, just downwards rather than across. */
@media (max-width: 860px) {
    .kana-chart-head, .kana-row { grid-template-columns: minmax(0, 1fr); gap: var(--s1); }
    .kana-chart { gap: var(--s4); }
    .kana-chart-head { gap: var(--s4); margin-bottom: 0; align-items: start; }
    .kana-voiced:not(:empty) { margin-bottom: var(--s3); }
}

@media (max-width: 720px) {
    /* One value for the tab bar's height: the bar is set to it, and everything
       that must clear the bar offsets by it. Guessing separately is how the
       gloss buttons ended up underneath it. */
    :root { --rail-w: 0px; --tabbar-h: calc(64px + env(safe-area-inset-bottom)); }

    .app { grid-template-columns: minmax(0, 1fr); }

    .rail {
        position: fixed;
        left: 0; right: 0; bottom: 0;
        top: auto;
        height: var(--tabbar-h);
        flex-direction: row;
        justify-content: space-around;
        gap: 0;
        border-right: 0;
        border-top: 1px solid var(--hairline);
        padding: var(--s1) var(--s2) calc(var(--s1) + env(safe-area-inset-bottom));
        z-index: 80;
    }
    /* Six items — Today, Read, Library, Grammar & Vocab, Scenarios, You — so each takes
       an equal share of whatever there is rather than a fixed width. The cap
       is what the longest label measures at the narrowest phone: below it the
       label truncates, and above it the tabs drift apart on a tablet. */
    .rail-item {
        width: auto;
        flex: 1;
        max-width: 96px;
        min-height: var(--tap);
        padding-left: 0;
        padding-right: 0;
        gap: 2px;
    }
    /* Only the rail has room for the whole name. See app.html. */
    .rail-label-tail { display: none; }
    .rail-label {
        font-size: 9.5px;
        max-width: 100%;
        overflow: hidden;
        text-overflow: ellipsis;
        white-space: nowrap;
    }
    .rail-spacer { display: none; }

    /* The public bar wraps to three rows on a phone — brand, links, buttons —
       which is most of the first screen before any of the page it is on. Two:
       the brand and the way in on one, the reference links under them. */
    .public-bar { padding: var(--s2) var(--s4); }
    .public-nav { order: 3; flex-basis: 100%; }
    .public-brand { font-size: 14px; }
    /* The brand and both buttons come to a few pixels more than a 375px
       phone has, and the pair would drop to a row of its own. A few pixels
       is all this is buying back. */
    .public-auth .chip,
    .public-auth .btn { height: var(--s8); padding: 0 var(--s3); font-size: 12.5px; }
}

/* On a phone the name and both buttons will not share a row, and the row they
   push onto costs more of the first screen than the name is worth. The mark
   stays, and the page title under it says where you are anyway. */
@media (max-width: 480px) {
    .public-brand span { display: none; }
    .public-cta { margin: var(--s8) var(--s4) var(--s6); padding: var(--s5); }

    /* No tab bar down there for a visitor, so nothing to leave room for. */
    .app-public .main { padding-bottom: 0; }

    /* The brief asks for 240px of its own, which on a phone leaves the
       conversation the rest and takes the page sideways. One column, with
       the room described above the conversation that happens in it. */
    .scenario-stage {
        grid-template-columns: minmax(0, 1fr);
        /* Two rows now, one above the other, and each as tall as it needs to
           be — the page scrolls here rather than the panel. The single capped
           row above is for the two-column layout, where the panel has to fit
           beside the conversation instead of before it. */
        grid-template-rows: none;
        gap: var(--s4);
        padding: var(--s4);
    }
    .scenario-brief { grid-template-rows: none; }
    /* Stacked, so the page is the thing that scrolls. A list with its own
       scrollbar inside a page that also scrolls is two gestures for one
       list, and on a touchscreen the wrong one wins about half the time. */
    .scenario-vocab,
    .scenario-gloss { max-height: none; }
    .scenario-brief-scene { height: 72px; }
    /* Capped so the box you type in is on screen without scrolling past the
       whole conversation to reach it. */
    .scenario-thread { max-height: 58vh; }
    .scenario-line { max-width: 92%; }
    .scenario-grid { grid-template-columns: minmax(0, 1fr); }

    /* The gloss column asks for 260px, which on a phone leaves the passage
       beside it about ninety — one word per line, which is not what the
       reader looks like and so not worth showing. */
    .landing { gap: var(--s10); padding: var(--s8) var(--s4) var(--s10); }
    .landing-demo { grid-template-columns: minmax(0, 1fr); padding: var(--s4); }
    .landing-close { padding: var(--s6) var(--s5); }

    /* The grid keeps its five columns and gives up cell width instead: a chart
       that wraps mid-row stops being a chart. */
    .kana-page { padding: var(--s5) var(--s4) var(--s8); gap: var(--s6); }
    .kana-grid { gap: var(--s1); grid-template-columns: repeat(var(--kana-cols, 5), minmax(0, 1fr)); }
    .kana-cell { min-height: 62px; padding: var(--s1); }
    .kana-char { font-size: 22px; }
    .kana-romaji { font-size: 10px; }
    .kana-mark { max-width: none; }

    .main { padding-bottom: var(--tabbar-h); }

    /* The sheet sits on the tab bar, not behind it. */
    .gloss {
        bottom: var(--tabbar-h);
        padding-bottom: var(--s5);
        max-height: calc(52vh - var(--tabbar-h));
    }

    .line { font-size: 23px; }
    .drill-prompt { font-size: 34px; }
    .options { grid-template-columns: 1fr; }
    .passage { padding: var(--s5) var(--s4) var(--s6); }
    .page-head { padding: var(--s3) var(--s4); }
    .lib-grid { grid-template-columns: minmax(0, 1fr); padding: var(--s4); gap: var(--s3); }
    .tag-bar { padding: var(--s3) var(--s4); }
    .para { padding: var(--s3); }
    .dict-tools { padding: var(--s3) var(--s4); }

    /* Eight columns will not fit a phone. The table becomes one block per word:
       the word and its reading lead, the meaning follows, and the details sit
       on a single line underneath. Nothing is dropped — it is re-stacked, and
       the header row is redundant once each value is on its own line. */
    .dict-table thead { display: none; }
    .dict-table,
    .dict-table tbody,
    .dict-table tr,
    .dict-table td { display: block; }

    .dict-table tr {
        position: relative;
        padding: var(--s4);
        border-bottom: 1px solid var(--hairline);
    }
    /* Only the first two lines sit beside the buttons; the detail line below
       them gets the full width instead of being needlessly indented. */
    .dict-table .c-word,
    .dict-table .c-reading { padding-right: 84px; }
    .dict-table tbody tr:hover td { background: none; }

    .dict-table td { padding: 0; border: 0; }
    .dict-table .c-word { margin-bottom: 2px; }
    .dict-table .c-word .dict-word { font-size: 22px; }
    .dict-table .c-meaning { margin-top: var(--s1); font-size: 16px; }

    /* Part of speech, strength and next review share one line. */
    .dict-table .c-part,
    .dict-table .c-lists,
    .dict-table .c-owner,
    .dict-table .c-strength,
    .dict-table .c-due {
        display: inline-block;
        vertical-align: middle;
        margin: var(--s2) var(--s3) 0 0;
    }
    /* The cap is for a table column; stacked there is the whole width. */
    .dict-lists { max-width: none; display: inline-flex; }
    .dict-table .c-part .dict-verb { display: inline; }
    .dict-table .c-part .dict-verb::before { content: " · "; }

    .dict-table .c-actions {
        position: absolute;
        top: var(--s4);
        right: var(--s4);
        display: flex;
        gap: var(--s2);
    }

    .table-wrap { overflow-x: visible; }
    .toasts { bottom: calc(var(--tabbar-h) + var(--s3)); }
}

/* ==========================================================================
   Texts like this one — the row of related texts under a library text
   ========================================================================== */

/* It follows the reader, which fills the column above it; the rule is what
   keeps it from reading as more of the passage. */
.lib-similar { border-top: 1px solid var(--hairline); }
/* Narrower columns than the library list: the cards carry less, and four
   should fit one row on a laptop. Later in the file than the phone-width
   rule for .lib-grid, so it wins there too — one column, as auto-fill gives
   at that width anyway. */
.lib-similar-grid { grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); }
.lib-similar-label { grid-column: 1 / -1; }

/* ================================================================ TODAY == */
/* The reader's own material, set like the front of a paper: a dated
   masthead, then bands of columns divided by rules rather than boxes.
   Cards said "here is a thing" six times over and left the page two-thirds
   empty; with rules, the content is the layout. The reading face carries
   the date and the headline, and the Mincho face everything the reader
   said, kept or met, the way the reader sets a passage. */
/* Centred, like a page rather than a sidebar: on a wide screen a measure
   pinned to the left leaves everything past it dead, and letting the
   columns stretch instead would run the reading line past what an eye
   holds. One track that may shrink below its content, or a single long
   unbroken line of Japanese would widen the whole page past the screen. */
.today-paper { padding: var(--s6) var(--s12) var(--s10); display: grid; grid-template-columns: minmax(0, 1fr); gap: var(--s6); max-width: 1320px; margin-inline: auto; width: 100%; box-sizing: border-box; }
.today-paper > * { min-width: 0; }
.today-lead { display: grid; gap: var(--s2); }
.today-date { font-family: var(--read); font-size: 16px; line-height: 1.3; color: var(--muted); }
.today-headline { margin: 0; font-family: var(--read); font-weight: 500; font-size: 44px; line-height: 1.1; letter-spacing: -.01em; }
.today-summary { margin: 0; font-size: 16px; line-height: 1.55; color: var(--muted); }
.today-summary a { color: var(--indigo); font-weight: 500; text-decoration: none; }
.today-summary a:hover { text-decoration: underline; }

/* A band shares its width among its columns in fixed proportions — 5/4/3
   for what was said, kept and slipping, 4/8 for the week and the shelf —
   as flex weights rather than grid tracks, so a column with nothing to
   show is left out and the rest close up without leaving a hole. */
.today-band { display: flex; gap: var(--s6); padding-top: var(--s5); border-top: 1px solid var(--hairline); }
/* A flex item is min-content wide unless told otherwise, and a column
   holding a long line of unbroken Japanese would sooner be that wide than
   wrap — the same trap the scenario stage documents. */
.today-col { flex: 1 1 0; min-width: 0; display: grid; gap: var(--s3); align-content: start; }
.today-col + .today-col { border-left: 1px solid var(--hairline); padding-left: var(--s6); }
.today-said { flex-grow: 5; }
.today-words { flex-grow: 4; }
.today-slipping { flex-grow: 3; }
.today-week-col { flex-grow: 4; }
.today-new { flex-grow: 8; }

.today-item { display: grid; gap: var(--s1); min-width: 0; }
.today-rule { padding-top: var(--s3); border-top: 1px solid var(--hairline); }
.today-row { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1) var(--s3); min-width: 0; }
.today-link { font-size: 14px; font-weight: 600; line-height: 1.45; color: var(--indigo); text-decoration: none; white-space: nowrap; }
.today-link:hover { text-decoration: underline; }
.today-title { min-width: 0; font-size: 15px; font-weight: 600; line-height: 1.4; color: var(--ink); overflow-wrap: anywhere; }
.today-title[lang="ja"] { font-family: var(--cjk); font-size: 17px; font-weight: 500; }
.today-title-lead { font-weight: 400; color: var(--muted); }
.today-note { margin: 0; font-size: 13px; line-height: 1.5; color: var(--muted); }
.today-note a { color: var(--indigo); font-weight: 600; text-decoration: none; }
.today-note a:hover { text-decoration: underline; }
.today-meta { font-family: var(--mono); font-size: 11px; letter-spacing: .04em; text-transform: uppercase; color: var(--muted); }
.today-sub { font-size: 12.5px; line-height: 1.4; color: var(--faint); }
.today-sep { color: var(--faint); }

/* Said better. Your line quietly, the better one at full weight, the note
   under it. The arrow keeps a gutter of its own so both lines start at the
   same edge and are compared down the column rather than across it. No
   ochre and no clay: a line somebody reached for is not due and not a
   mark against them. */
.today-pairs { display: grid; gap: var(--s3); min-width: 0; }
.today-pair { display: grid; grid-template-columns: 20px minmax(0, 1fr); row-gap: 1px; min-width: 0; }
.today-pair-mine,
.today-pair-better,
.today-pair-note { grid-column: 2; overflow-wrap: anywhere; }
/* The opening bracket hangs into the gutter, so it is the words that line
   up rather than the brackets. */
.today-pair-mine { grid-row: 1; font-family: var(--cjk); font-size: 16px; line-height: 1.4; color: var(--muted); text-indent: -.5em; }
.today-pair-better { grid-row: 2; font-family: var(--cjk); font-size: 18.5px; line-height: 1.35; color: var(--ink); text-indent: -.5em; }
.today-pair-arrow { grid-column: 1; grid-row: 2; align-self: center; width: 12px; height: 12px; fill: none; stroke: var(--faint); stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.today-pair-note { grid-row: 3; padding-top: 1px; font-size: 13px; line-height: 1.4; color: var(--muted); }
.today-links { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s1) var(--s3); min-width: 0; }

/* Your words. One bar for every kept word, split in the buckets every
   strength bar uses: celadon solid, indigo forming, the bare rule new. */
.today-split { display: flex; gap: 2px; min-width: 0; }
.today-split i { display: block; flex: 1 1 0; height: 3px; border-radius: 2px; }
.today-split .is-solid { background: var(--celadon); }
.today-split .is-forming { background: var(--indigo); }
.today-split .is-new { background: var(--hairline); }
.today-count { color: var(--ink); }
.today-count.is-due { color: var(--ochre); }
.today-kept { display: grid; gap: var(--s1); min-width: 0; padding-top: var(--s1); }
/* One line each on a wide screen. Where it came from is what gives way
   when the line is too long, then the meaning; the word itself never. */
.today-kept-row { display: flex; align-items: baseline; gap: var(--s2); min-width: 0; white-space: nowrap; }
.today-kept-word { flex: none; max-width: 50%; overflow: hidden; text-overflow: ellipsis; font-size: 16px; line-height: 1.4; color: var(--ink); }
.today-kept-word[lang="ja"] { font-family: var(--cjk); font-size: 17px; }
.today-kept-reading { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 12.5px; line-height: 1.4; color: var(--muted); }
.today-kept-meaning { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 13px; line-height: 1.4; color: var(--ink); }
.today-kept-from { flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; font-size: 13px; line-height: 1.4; color: var(--muted); }
.today-kept-from [lang="ja"] { font-family: var(--cjk); }
.today-kept-reading[lang="ja"],
.today-word-reading[lang="ja"] { font-family: var(--cjk); }

/* Slipping. The word, its reading, what it means, and the five bars every
   strength on the site is drawn with. */
.today-rows { display: grid; gap: 6px; min-width: 0; }
.today-word { display: flex; align-items: baseline; gap: var(--s2); min-width: 0; }
.today-word-ja { flex: none; min-width: 40px; max-width: 50%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 16px; line-height: 1.4; color: var(--ink); }
.today-word-ja[lang="ja"] { font-family: var(--cjk); font-size: 17px; }
.today-word-reading { flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12.5px; line-height: 1.4; color: var(--muted); }
.today-word-meaning { flex: 1 1 0; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 13px; line-height: 1.4; color: var(--muted); }
.today-strength { display: inline-flex; flex: none; align-self: center; gap: 2px; }
.today-strength i { width: 10px; height: 4px; border-radius: 2px; background: var(--surface-3); }
.today-strength i.on { background: var(--indigo); }
.today-actions { display: grid; gap: 6px; min-width: 0; padding-top: var(--s3); border-top: 1px solid var(--hairline); }
.today-action { display: grid; min-width: 0; }
.today-action .today-note { font-size: 12.5px; }

/* Read again, or the text on offer when there is nothing to reread. */
.today-again-title { font-family: var(--cjk); font-size: 22px; line-height: 1.35; color: var(--ink); text-indent: -.5em; overflow-wrap: anywhere; }
.today-again-words { margin: 0; font-size: 14px; line-height: 1.5; color: var(--ink); overflow-wrap: anywhere; }
.today-again-words [lang="ja"] { font-family: var(--cjk); }
.today-preview { margin: 0; font-family: var(--cjk); font-size: 18px; line-height: 1.9; font-weight: 500; display: -webkit-box; -webkit-line-clamp: 5; -webkit-box-orient: vertical; overflow: hidden; overflow-wrap: anywhere; }
.today-preview[data-latin] { font-family: var(--read); font-size: 17px; line-height: 1.7; }

/* Kanji met: the few met most, large enough to know at a glance, each with
   how often it turned up. */
.today-met { display: flex; flex-wrap: wrap; gap: var(--s3) var(--s6); padding-top: 2px; }
.today-met-item { display: grid; justify-items: center; gap: var(--s1); }
.today-met-char { font-family: var(--cjk); font-size: 28px; line-height: 1; color: var(--ink); }
.today-met-count { font-family: var(--mono); font-size: 10.5px; letter-spacing: .04em; line-height: 1.3; color: var(--muted); }

.today-progress { display: flex; align-items: center; gap: var(--s3); min-width: 0; }
.today-bar { flex: none; width: 140px; height: 3px; border-radius: 2px; background: var(--surface-3); overflow: hidden; }
.today-bar i { display: block; height: 100%; background: var(--indigo); }

/* Seven marks for the week: filled on a day something was done, ringed
   on today. No bars and no scores — the marks say "you were here". */
.today-week { display: flex; gap: var(--s4); align-items: flex-end; padding-top: 2px; }
.today-day { display: grid; justify-items: center; gap: 6px; }
.today-dot { width: 10px; height: 10px; border-radius: 50%; background: var(--surface-3); }
.today-day.is-active .today-dot { background: var(--indigo); }
/* Today is ringed whether or not anything is in it yet. Before anything,
   the ring is the whole mark; after, it sits outside the filled dot, so
   the two states still read apart. */
.today-day.is-today .today-dot { background: transparent; box-shadow: inset 0 0 0 1.5px var(--indigo); }
.today-day.is-today.is-active .today-dot { background: var(--indigo); box-shadow: none; outline: 1.5px solid var(--indigo); outline-offset: 2px; }
.today-day-letter { font-family: var(--mono); font-size: 10.5px; line-height: 1.3; color: var(--muted); }
.today-day.is-today .today-day-letter { color: var(--ink); }

/* New in the library: up to three texts side by side, each ruled off from
   the next the way the columns are. */
.today-shelf { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: var(--s5); min-width: 0; }
.today-lib { display: grid; gap: 2px; align-content: start; min-width: 0; color: inherit; text-decoration: none; }
.today-lib + .today-lib { padding-left: var(--s5); border-left: 1px solid var(--hairline); }
.today-lib-head { display: flex; align-items: center; gap: var(--s2); min-width: 0; }
.today-lib-head .pill,
.today-lib-head .today-meta { flex: none; }
.today-lib-title { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 15px; font-weight: 600; line-height: 1.4; color: var(--ink); }
.today-lib-title[lang="ja"] { font-family: var(--cjk); font-size: 16px; font-weight: 500; }
.today-lib:hover .today-lib-title { color: var(--indigo); }
.today-lib-open { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; line-height: 1.55; color: var(--muted); }
.today-lib-open[lang="ja"] { font-family: var(--cjk); }
.today-lib-kept { font-size: 12.5px; }
.today-lib-kept [lang="ja"] { font-family: var(--cjk); }

/* Below a laptop the 5/4/3 split leaves the slipping column too narrow for
   a word, its reading and its bars on one line, so the columns stack in
   the order they are written, each ruled off above rather than beside. */
@media (max-width: 1024px) {
    .today-paper { padding-inline: var(--s8); }
    .today-band { flex-direction: column; gap: var(--s5); }
    .today-col { flex: none; }
    .today-col + .today-col { border-left: 0; padding-left: 0; padding-top: var(--s5); border-top: 1px solid var(--hairline); }
}

@media (max-width: 720px) {
    .today-paper { padding: var(--s5) var(--s5) var(--s6); gap: var(--s4); }
    .today-date { font-size: 15px; }
    .today-headline { font-size: 34px; }
    .today-summary { font-size: 14.5px; }
    .today-band { gap: var(--s4); padding-top: var(--s4); }
    .today-col + .today-col { padding-top: var(--s4); }
    .today-pair-mine { font-size: 15px; }
    .today-pair-better { font-size: 16.5px; }
    /* The word on one line and where it came from on the next: a phone has
       no room to cut either short. */
    .today-kept-row { flex-wrap: wrap; row-gap: 0; white-space: normal; }
    .today-kept-dot { display: none; }
    .today-kept-from { flex-basis: 100%; overflow: visible; font-size: 12.5px; }
    .today-met { gap: var(--s3) var(--s5); }
    .today-shelf { grid-template-columns: minmax(0, 1fr); gap: var(--s3); }
    .today-lib + .today-lib { padding-left: 0; border-left: 0; padding-top: var(--s3); border-top: 1px solid var(--hairline); }
    /* A link longer than a narrow phone is wide wraps rather than pushing
       the page sideways. */
    .today-link { white-space: normal; }
    .today-dot { width: 9px; height: 9px; }
}

/* Where a page is filed, for the two kanji pages that sit under Grammar &
   Vocab without being blocks of it. The rail lights the Grammar tab for
   them, but on a phone that is a small glow at the bottom of the screen;
   this is the sign on the page itself. */
.page-crumb {
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--muted);
    text-decoration: none;
}
.page-crumb:hover { color: var(--indigo); }
.page-crumb::after { content: "/"; margin-left: var(--s2); color: var(--faint); }

@media (max-width: 480px) {
    .today-page { padding: var(--s4) var(--s4) var(--s8); }
    .today-grid { grid-template-columns: minmax(0, 1fr); }
}

/* =============================================================== STREAK == */
/* How many right in a row, in the card's head beside the count. The badge
   goes quiet at zero rather than disappearing, so the space it takes does
   not jump when a run starts, and it lights up when the run is the best
   one so far. Three sizes of celebration, in the app's own colours, each
   over inside a second and a half; with motion turned down, the words
   only. */
.streak {
    position: relative;
    display: inline-flex;
    align-items: center;
    gap: var(--s1);
    margin-left: auto;
    padding: 0 var(--s2);
    height: 24px;
    border-radius: var(--radius-pill);
    border: 1px solid var(--hairline);
    background: var(--surface-2);
    color: var(--ink);
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: .04em;
    white-space: nowrap;
    transition: color .2s, border-color .2s, background .2s, opacity .2s;
}
.streak svg { width: 12px; height: 12px; fill: currentColor; stroke: none; }
.streak-count { font-weight: 700; font-size: 12px; }
.streak.is-zero { color: var(--faint); opacity: .7; }
.streak.is-best { color: var(--ochre); border-color: var(--ochre); background: var(--ochre-soft); }
.streak-best { font-family: var(--mono); font-size: 10.5px; letter-spacing: .06em; text-transform: uppercase; color: var(--faint); white-space: nowrap; }
.streak-best:empty { display: none; }

.streak.is-pulse { animation: streak-pulse .6s cubic-bezier(.2, .9, .3, 1.4); }
@keyframes streak-pulse {
    0% { transform: scale(1); }
    40% { transform: scale(1.28); }
    100% { transform: scale(1); }
}

/* The ring: one circle expanding out of the badge and fading, for ten and
   beyond. Positioned on the badge so it is thrown from the number. */
.streak-ring {
    position: absolute;
    inset: -2px;
    border-radius: var(--radius-pill);
    border: 2px solid var(--indigo);
    pointer-events: none;
    animation: streak-ring .8s ease-out forwards;
}
@keyframes streak-ring {
    from { transform: scale(1); opacity: .9; }
    to { transform: scale(2.6); opacity: 0; }
}

/* The words, floating up out of the head and gone. Absolute, so they take
   no room and push nothing. */
.drill-head { position: relative; }
.streak-label {
    position: absolute;
    right: 0;
    top: calc(100% + 6px);
    font-family: var(--mono);
    font-size: 11px;
    letter-spacing: .08em;
    text-transform: uppercase;
    color: var(--ochre);
    pointer-events: none;
    animation: streak-label 1.6s ease-out forwards;
}
@keyframes streak-label {
    0% { opacity: 0; transform: translateY(6px); }
    15% { opacity: 1; transform: translateY(0); }
    75% { opacity: 1; transform: translateY(-4px); }
    100% { opacity: 0; transform: translateY(-14px); }
}

/* The burst: pieces thrown up from the card's top edge. Each piece carries
   its own throw in custom properties set when it is made. */
.streak-burst { position: absolute; left: 0; right: 0; top: 0; height: 0; overflow: visible; pointer-events: none; }
.streak-piece {
    position: absolute;
    left: var(--x);
    top: 0;
    width: 6px;
    height: 10px;
    border-radius: 1px;
    opacity: 0;
    animation: streak-piece 1.3s cubic-bezier(.2, .7, .4, 1) var(--delay) forwards;
}
.streak-piece-0 { background: var(--indigo); }
.streak-piece-1 { background: var(--ochre); }
.streak-piece-2 { background: var(--celadon); }
.streak-piece-3 { background: var(--clay); }
@keyframes streak-piece {
    0% { opacity: 1; transform: translate(0, 0) rotate(0deg); }
    60% { opacity: 1; }
    100% { opacity: 0; transform: translate(var(--dx), calc(var(--dy) + 90px)) rotate(var(--rot)); }
}

@media (prefers-reduced-motion: reduce) {
    .streak.is-pulse, .streak-ring, .streak-piece { animation: none; }
    .streak-ring, .streak-burst { display: none; }
    .streak-label { animation: streak-label-still 1.6s ease-out forwards; }
    @keyframes streak-label-still { 0% { opacity: 0; } 15% { opacity: 1; } 75% { opacity: 1; } 100% { opacity: 0; } }
}

/* ================================================================ LEARN == */
/* A lesson: the drill's card and head, with a strip of the ten above the
   card, a caption under the character, and two cards of its own for between
   rounds and for the end. Nothing here moves — the strip and the buttons
   change colour and that is all — so there is nothing for reduced motion to
   turn off beyond what the streak and the stroke playback already do. */
.learn-stage { display: grid; gap: var(--s5); }

/* Ten cells, wrapping rather than squeezing: a kept word can be a phrase, and
   ten phrases do not fit a phone's width in one row. Each is capped and cut
   short, because the cell is a marker, not the place to read the word. */
.learn-strip {
    display: flex;
    flex-wrap: wrap;
    justify-content: center;
    gap: var(--s1);
    margin: 0;
    padding: 0;
    list-style: none;
}
.learn-cell {
    min-width: 30px;
    max-width: 8em;
    height: 30px;
    padding: 0 var(--s1);
    border: 1px solid var(--hairline);
    border-radius: var(--radius-sm);
    background: var(--surface);
    color: var(--faint);
    font-family: var(--cjk);
    font-size: 15px;
    line-height: 28px;
    text-align: center;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}
.learn-cell[data-latin="true"] { font-family: var(--read); font-size: 13px; }
.learn-cell.is-seen { color: var(--ink); }
.learn-cell.is-cleared { background: var(--celadon-soft); border-color: var(--celadon); color: var(--celadon); }
.learn-cell.is-slipped { background: var(--clay-soft); border-color: var(--clay); color: var(--clay); }
/* Slipped and then got right: done, but with the slip still showing at the
   edge, since the round is not clean because of it. */
.learn-cell.is-cleared.is-slipped { background: var(--celadon-soft); color: var(--celadon); border-color: var(--clay); }
.learn-cell.is-current { border-color: var(--indigo); box-shadow: inset 0 0 0 1px var(--indigo); }
.learn-cell.is-current:not(.is-cleared):not(.is-slipped) { background: var(--indigo-soft); color: var(--indigo); }

/* The character, with its strokes being written beside it on the first
   meeting. Wraps under it when a long word takes the width. */
.learn-figure {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: var(--s4);
    min-width: 0;
}
.learn-glyph { min-width: 0; max-width: 100%; }
/* A meaning is a sentence where a character is a shape, and at the size a
   character is shown it runs to four lines. */
.learn-glyph[data-latin="true"] { font-size: clamp(26px, 2.6vw, 36px); line-height: 1.25; }
.learn-play { width: 96px; flex: none; }
.learn-play:empty { display: none; }

.learn-options .option span { min-width: 0; overflow-wrap: anywhere; }

.learn-caption { display: grid; justify-items: center; gap: var(--s2); }
.learn-caption-line { font-family: var(--read); font-size: 17px; line-height: 1.4; overflow-wrap: anywhere; }
.learn-caption-line b { color: var(--indigo); font-weight: 600; }
.learn-caption-sub { font-family: var(--mono); font-size: 13px; color: var(--muted); overflow-wrap: anywhere; }
/* A card's note mixes English labels with Japanese forms ("past 高かった"),
   so it is set in the reading face rather than the reading line's mono, which
   would make the forms look like code. */
.learn-caption-note { font-family: var(--read); font-size: 14px; line-height: 1.5; color: var(--muted); text-align: center; overflow-wrap: anywhere; }
/* The same words after a slip, set off from the buttons above them as the
   drill's answer is. */
.learn-caption.learn-reminder { padding-top: var(--s4); border-top: 1px solid var(--hairline); }
.learn-caption.learn-reminder b { color: var(--celadon); }

.learn-actions { display: flex; flex-wrap: wrap; gap: var(--s2); }

.learn-between {
    display: grid;
    justify-items: center;
    gap: var(--s3);
    padding: var(--s6) var(--s5);
    text-align: center;
}
.learn-between-title { font-size: 17px; font-weight: 600; color: var(--ink); }
.learn-between-title.is-clean { color: var(--celadon); }
.learn-between-title.is-slips { color: var(--ochre); }
.learn-between-next { max-width: 44ch; }

.learn-done {
    display: grid;
    justify-items: center;
    gap: var(--s3);
    padding: var(--s6) var(--s5);
    text-align: center;
}
.learn-done-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
    gap: var(--s2);
    width: 100%;
    margin: var(--s2) 0 0;
    padding: 0;
    list-style: none;
}
.learn-done-item {
    display: grid;
    align-content: center;
    gap: 2px;
    min-width: 0;
    padding: var(--s2);
    border: 1px solid var(--hairline);
    border-radius: var(--radius);
    background: var(--surface-2);
}
.learn-done-glyph { font-family: var(--cjk); font-size: 22px; line-height: 1.25; color: var(--ink); overflow-wrap: anywhere; }
.learn-done-glyph[data-latin="true"] { font-family: var(--read); font-size: 16px; }
.learn-done-answer { font-family: var(--read); font-size: 13px; line-height: 1.35; color: var(--muted); overflow-wrap: anywhere; }
.learn-done-actions { display: flex; flex-wrap: wrap; gap: var(--s2); width: 100%; margin-top: var(--s2); }

/* The answers' group takes the focus while a question waits, so the next Tab
   lands on the first answer rather than at the top of the page. It is
   somewhere to stand, not something to press, so it draws no ring. */
.learn-options:focus { outline: none; }

/* The phone breakpoint RESPONSIVE uses, where the tab bar comes in and the
   drill's answers stack. */
@media (max-width: 720px) {
    .learn-stage { gap: var(--s4); }
    .learn-cell { min-width: 28px; height: 28px; font-size: 14px; line-height: 26px; }
    .learn-glyph[data-latin="true"] { font-size: 24px; }
    .learn-play { width: 80px; }
    .learn-caption-line { font-size: 16px; }
    .learn-between, .learn-done { padding: var(--s5) var(--s4); }
    .learn-done-grid { grid-template-columns: repeat(auto-fill, minmax(84px, 1fr)); }
}
