/* The shell around the card.
 *
 * `card.css` owns everything inside `.kanji-card` and is copied from the deck
 * unchanged; this file owns the page around it and deliberately sets no rule that
 * reaches in. The two share a palette by repeating its values rather than by
 * importing them, because the card's tokens are scoped to `.kanji-card` on purpose:
 * it has to survive being pasted into Anki, where this file does not exist.
 */

:root {
  --paper: #ffffff;
  --sunk: #f2f4f8;
  --ink: #14161d;
  --ink-2: #4c5366;
  --ink-3: #7a8298;
  --rule: #dfe3ec;
  --accent: #23407a;
  --accent-soft: #e6ecf8;

  --shell: 3.9rem;
  --list: 23rem;
  color-scheme: light dark;
}

/* Dark, whether the reader asked for it or the system did.
 *
 * `Appearance` in the settings panel writes `data-theme` on <html>, the same word
 * the card reads from its own root - one preference, two elements, because the card
 * has to keep working pasted into Anki where this file does not exist.
 *
 * The system rule is written `:not([data-theme="light"])` so an explicit `light`
 * beats a dark OS, and `[data-theme="dark"]` is stated separately so an explicit
 * `dark` beats a light one. `auto` matches neither and falls through to whatever
 * the system says, which is what auto means. */
:root[data-theme="dark"] {
  --paper: #16181f;
  --sunk: #1d212a;
  --ink: #e8eaf1;
  --ink-2: #a3abbe;
  --ink-3: #7d8598;
  --rule: #2b3140;
  --accent: #8facee;
  --accent-soft: #1b2440;
}

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) {
    --paper: #16181f;
    --sunk: #1d212a;
    --ink: #e8eaf1;
    --ink-2: #a3abbe;
    --ink-3: #7d8598;
    --rule: #2b3140;
    --accent: #8facee;
    --accent-soft: #1b2440;
  }
}

/* The browser paints scrollbars and form controls from this, so it has to move with
   the tokens or a dark page grows white scrollbars. */
:root[data-theme="light"] { color-scheme: light; }
:root[data-theme="dark"] { color-scheme: dark; }

* { box-sizing: border-box; }

/* `hidden` is only a user-agent `display: none`, and a user-agent declaration loses
   to any author one whatever the specificity suggests - so a single
   `.something { display: flex }` anywhere below silently outranks it. Said once,
   with the weight it takes, rather than remembered at each of the places that set
   the attribute. */
[hidden] { display: none !important; }

/* One scroller, and it is never the document.
 *
 * The list scrolls and the pane scrolls; the page itself must not, and `100%` was
 * not enough to stop it on a phone. A mobile browser sizes the layout viewport to
 * the screen with its address bar HIDDEN, so `height: 100%` is taller than what is
 * actually visible while the bar is showing - which leaves the document scrollable
 * by exactly the height of that bar. That scroll belongs to nobody: the header is
 * sticky inside a flex column that does not scroll, so when the document takes a
 * scroll instead the header slides away and whatever was on screen becomes the top
 * of the page, with no way to get back above it.
 *
 * `dvh` is the same number measured against what is visible now, and
 * `overflow: hidden` means there is nothing to take the scroll even if it were.
 * `overscroll-behavior: none` turns off pull-to-refresh, which fires on exactly the
 * gesture someone makes to scroll a list back up.
 */
html, body {
  height: 100%;
  overflow: hidden;
  overscroll-behavior: none;
}

@supports (height: 100dvh) {
  html, body { height: 100dvh; }
}

body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font-family: -apple-system, "Segoe UI Variable Text", "Segoe UI", Roboto, system-ui, sans-serif;
  font-size: 15px;
  line-height: 1.5;
  display: flex;
  flex-direction: column;
}

/* The index is 471 KB and the axes another 163 KB; until they land there is
   nothing to show, and a flash of an empty two-pane layout reads as a broken
   page rather than a loading one. */
body.booting main { visibility: hidden; }

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

/* ----------------------------------------------------------------------- header */

.top {
  display: flex;
  align-items: center;
  gap: 0.9rem;
  padding: 0.55rem clamp(0.7rem, 2vw, 1.1rem);
  border-bottom: 1px solid var(--rule);
  background: var(--paper);
  position: sticky;
  top: 0;
  z-index: 5;
}

.brand {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  text-decoration: none;
  color: inherit;
  flex: none;
}

.brand-glyph {
  font-size: 1.45rem;
  line-height: 1;
  color: var(--accent);
}

.box { flex: 1; min-width: 0; position: relative; }

#search {
  width: 100%;
  padding: 0.5rem 0.75rem;
  border: 1px solid var(--rule);
  border-radius: 0.5rem;
  background: var(--sunk);
  outline-offset: 2px;
}

#search:focus-visible { outline: 2px solid var(--accent); border-color: var(--accent); }

.lang {
  flex: none;
  padding: 0.4rem 0.7rem;
  border: 1px solid var(--rule);
  border-radius: 0.5rem;
  background: transparent;
  cursor: pointer;
  font-size: 0.82rem;
  color: var(--ink-2);
}

.lang:hover { border-color: var(--accent); color: var(--accent); }

/* ------------------------------------------------------------------------ split */

.split {
  flex: 1;
  min-height: 0;
  display: grid;
  grid-template-columns: var(--list) minmax(0, 1fr);
}

.list {
  border-right: 1px solid var(--rule);
  display: flex;
  flex-direction: column;
  min-height: 0;
  background: var(--paper);
}

/* One line: the filters, then the count pushed to the end of it. The count is what
   the filters produced, so it reads as their result rather than as a heading of its
   own - and the line it used to have back is a line of results on a phone. */
.list-head {
  display: flex;
  align-items: center;
  gap: 0.4rem;
  padding: 0.45rem 0.6rem;
  border-bottom: 1px solid var(--rule);
}

/* The chips keep their own size and wrap onto a second row when they run out of
   one. Both alternatives lose something a reader needs: letting them shrink clips
   the value the chip exists to show, and sliding the row sideways hides whole
   filters off the edge with nothing to say they are there. A second row is visible.

   The count stays on the first row either way - `.filters` takes the space left
   over and wraps inside it. */
.filters {
  display: flex;
  flex-wrap: wrap;
  gap: 0.3rem;
  flex: 1 1 auto;
  min-width: 0;
}

/* A chip never shrinks and never clips; the row wraps instead. */
.f-select, .f-radicals {
  flex: none;
  padding: 0.25rem 0.95rem 0.25rem 0.4rem;
  border: 1px solid var(--rule);
  border-radius: 0.4rem;
  background: var(--paper);
  color: var(--ink-2);
  font-size: 0.76rem;
  font-family: inherit;
  line-height: 1.35;
  cursor: pointer;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* `inline-block`, not `inline-flex`. Making a native control a flex container is
   the same class of move as the two that already went wrong here - it works in the
   engine it was written against and is undefined territory elsewhere - and it buys
   nothing: the width is the same either way, measured. */
.f-select {
  display: inline-block;
  /* Between them these make the chip as wide as the option it is showing rather
     than as its widest one. A select given the platform's own appearance reserves
     room for "Ngoài JLPT" while displaying "JLPT", and the four of them together
     kept 60 px of a 348 px row for text nobody could see - which is the room the
     count needed to share the line.

     Measured, not assumed: dropping `appearance` is what does it here, and
     `field-sizing` says the same thing in the one word meant for it. Where a
     browser does neither the chips are merely wider and the row wraps sooner,
     which is why wrapping is the fallback and not an error. */
  appearance: none;
  field-sizing: content;
  /* The arrow, drawn here rather than by the platform, which sizes its own at about
     16 px and would take a fifth of the chip. */
  background-image: linear-gradient(45deg, transparent 50%, currentColor 50%),
                    linear-gradient(135deg, currentColor 50%, transparent 50%);
  background-position: right 0.45rem top 0.62rem, right 0.23rem top 0.62rem;
  background-size: 0.22rem 0.22rem, 0.22rem 0.22rem;
  background-repeat: no-repeat;
}

/* The menu is the platform's, drawn from the control's own colours - so a chip that
   is bold and accent-coloured because its filter is set would hand those to every
   row of its menu. Said plainly here, the menu stays a menu. */
.f-select option {
  color: var(--ink);
  background: var(--paper);
  font-weight: 400;
}

.f-select.is-set, .f-radicals.is-set {
  border-color: var(--accent);
  color: var(--accent);
  font-weight: 600;
}

.f-select:focus-visible, .f-radicals:focus-visible {
  outline: 2px solid var(--accent);
  outline-offset: 1px;
}

.count {
  margin: 0;
  margin-left: auto;
  flex: none;
  font-size: 0.72rem;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

.banner {
  margin: 0;
  padding: 0.45rem 0.75rem;
  background: var(--accent-soft);
  color: var(--accent);
  font-size: 0.82rem;
  display: flex;
  align-items: center;
  gap: 0.5rem;
  border-bottom: 1px solid var(--rule);
}

.banner-gloss { color: var(--ink-2); font-size: 0.78rem; }

.banner-close {
  margin-left: auto;
  border: 0;
  background: transparent;
  cursor: pointer;
  font-size: 1.1rem;
  line-height: 1;
  color: inherit;
  padding: 0 0.2rem;
}

.results { flex: 1; overflow-y: auto; overscroll-behavior: contain; }

.r-row {
  display: flex;
  gap: 0.65rem;
  align-items: center;
  padding: 0.45rem 0.75rem;
  border-bottom: 1px solid var(--rule);
  text-decoration: none;
  color: inherit;
}

.r-row:hover { background: var(--sunk); }
.r-row.is-current { background: var(--accent-soft); }
.r-row.is-current .r-glyph { color: var(--accent); }

.r-row .r-glyph {
  font-size: 1.6rem;
  line-height: 1.15;
  flex: none;
  width: 1.6em;
  text-align: center;
}

.r-row .r-body { min-width: 0; flex: 1; display: flex; flex-direction: column; gap: 0.1rem; }

/* Two lines, then an ellipsis. On one line most of a meaning was simply not there -
   "nổi bật, trứ danh; viết sách; mặc (áo) (biến…" stops before the part that tells
   two similar characters apart - and a row is scanned for exactly that. Two is
   where it stops: the rows stay a regular height to run an eye down, and a
   three-line meaning would make the list a page of paragraphs.

   It also stops the row from insisting on a width. `nowrap` makes the whole string
   the element's minimum size, which the grid track above it honoured by growing
   past the window. */
.r-row .r-meaning {
  font-size: 0.84rem;
  line-height: 1.35;
  color: var(--ink-2);
  overflow: hidden;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
}

/* Reading and counters on one line, so the reading stays flush left down the whole
   column and the numbers stay out of its way. */
.r-row .r-head {
  display: flex;
  align-items: baseline;
  gap: 0.5rem;
  min-width: 0;
}

.r-row .r-meta {
  display: flex;
  gap: 0.4rem;
  align-items: center;
  font-size: 0.72rem;
  color: var(--ink-3);
}

/* The thing a Vietnamese reader scans for: first line, accent colour, and the only
   part of the row set in capitals. */
.r-row .r-hv {
  color: var(--accent);
  font-weight: 650;
  font-size: 0.86rem;
  letter-spacing: 0.03em;
  /* This one does stay on a single line, and clips: it is the column a reader runs
     an eye down, and a reading that wraps moves the row below it out of line. Its
     minimum is contained by `min-width: 0` on the flex parents. */
  min-width: 0;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

.r-row .r-reading { color: var(--ink-2); }

/* Watched, not seen: it marks where the next page has to be built and must never
   push the last row up or leave a gap under it. */
.r-sentinel { height: 1px; }

.r-row .r-pill {
  border: 1px solid var(--rule);
  border-radius: 0.7rem;
  padding: 0 0.35rem;
  font-size: 0.66rem;
}

.r-row .r-kind {
  border-color: transparent;
  background: var(--sunk);
  color: var(--ink-3);
  font-weight: 700;
  letter-spacing: 0.04em;
}

.r-match {
  color: var(--ink-3);
  font-size: 0.72rem;
}

/* ------------------------------------------------------------------ detail pane */

.pane { min-height: 0; overflow-y: auto; overscroll-behavior: contain; display: flex; flex-direction: column; }

/* Hidden where both panes are on screen: with the list beside the card there is
   nothing to go back to. */
.back { display: none; }

/* The credit line is the last thing on a card and should read as the last thing.
   Four rem below it, on top of the card's own 24px, left about ninety pixels of
   nothing under the final sentence - which reads as a page still loading rather
   than a page that has finished. */
.detail { flex: 1; padding: clamp(0.5rem, 2vw, 1.25rem) 0.5rem 1rem; }

/* The empty pane is the first thing a visitor sees, and it is most of the screen.
   Centred and given the one line that teaches what this site can be asked - the
   Sino-Vietnamese axis is not guessable from a search box. */
.detail:has(> .empty), .detail:has(> .pane-note) {
  display: flex;
  align-items: center;
  justify-content: center;
}

.card-host { max-width: 44rem; margin: 0 auto; }
.radical-card-host { max-width: 40rem; }

.entity-tabs {
  width: max-content;
  max-width: calc(100% - 5rem);
  margin: 0 auto 0.45rem;
  padding: 0.18rem;
  display: flex;
  gap: 0.15rem;
  border: 1px solid var(--rule);
  border-radius: 999px;
  background: var(--sunk);
}

.entity-tab {
  padding: 0.32rem 0.75rem;
  border-radius: 999px;
  color: var(--ink-3);
  font-size: 0.7rem;
  font-weight: 750;
  letter-spacing: 0.05em;
  text-decoration: none;
}

.entity-tab:hover { color: var(--accent); }
.entity-tab.is-active { color: var(--accent); background: var(--paper); box-shadow: 0 1px 4px rgba(10, 12, 18, 0.08); }

.pane-note, .empty { color: var(--ink-3); text-align: center; padding: 3rem 1rem; }

.empty-title { margin: 0 0 0.3rem; font-size: 0.95rem; color: var(--ink-2); }
.empty-hint { margin: 0; font-size: 0.8rem; }

.empty-try {
  margin: 1.1rem 0 0;
  display: flex;
  gap: 0.4rem;
  justify-content: center;
  flex-wrap: wrap;
}

.try-chip {
  background: var(--sunk);
  border: 1px solid var(--rule);
  border-radius: 0.4rem;
  padding: 0.2rem 0.55rem;
  font-size: 0.85rem;
  color: var(--ink-2);
  cursor: pointer;
}

.try-chip:hover { border-color: var(--accent); color: var(--accent); }

/* The two axes, under the card. Not part of the card: the runtime has no idea
   there are other characters on this site, and giving it one would mean a copy of
   `_kanji.js` that Anki could not use. */
.axis-rail {
  max-width: 44rem;
  margin: 1.1rem auto 0;
  padding: 0.75rem clamp(0.75rem, 3vw, 1.5rem);
  border-top: 1px solid var(--rule);
  display: flex;
  flex-wrap: wrap;
  gap: 0.4rem;
}

.axis-chip {
  display: inline-flex;
  align-items: baseline;
  gap: 0.35rem;
  padding: 0.22rem 0.5rem;
  border: 1px solid var(--rule);
  border-radius: 0.4rem;
  text-decoration: none;
  color: var(--ink-2);
  font-size: 0.8rem;
}

.axis-chip:hover { border-color: var(--accent); color: var(--accent); }
.axis-chip b { font-size: 1rem; font-weight: 600; }
.axis-chip span { font-size: 0.7rem; color: var(--ink-3); font-variant-numeric: tabular-nums; }

/* ---------------------------------------------------------------------- narrow */

@media (max-width: 860px) {
  .box-hint { display: none; }
  .brand-title { display: none; }

  /* One pane at a time. The list is the home state and the card slides over it,
     which is what keeps a reader's place in the results: the list is still there,
     scrolled where they left it, rather than rebuilt when they come back. */
  /* `minmax(0, 1fr)`, never a bare `1fr`: a flexible track's automatic minimum is
     its content's minimum, so one unbreakable row was enough to widen the track
     past the window and scroll the whole page sideways - taking the result count
     with it, off the right-hand edge. */
  .split { grid-template-columns: minmax(0, 1fr); }
  .pane { display: none; }
  body.showing-detail .list { display: none; }
  body.showing-detail .pane { display: block; }

  /* In the corner of the character, floating over it. The negative bottom margin is
     what keeps it from costing a row: it takes its own height back out of the
     column, so the card starts at the top of the pane and the button sits in the
     empty margin beside the centred glyph. Sticky rather than absolute so it stays
     reachable once the reader has scrolled into the readings. */
  .back {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    position: sticky;
    top: 0.5rem;
    align-self: flex-start;
    width: 2.1rem;
    height: 2.1rem;
    margin: 0.5rem 0 -2.6rem 0.5rem;
    padding: 0;
    border: 1px solid var(--rule);
    border-radius: 50%;
    background: var(--paper);
    color: var(--ink-2);
    box-shadow: 0 0.15rem 0.5rem rgba(10, 12, 18, 0.1);
    cursor: pointer;
    z-index: 4;
  }

  .back svg { width: 1.1rem; height: 1.1rem; }
  .back:hover { border-color: var(--accent); color: var(--accent); }

  /* Room for it beside the glyph, which is centred and leaves the corner empty. */
  .detail { padding-top: 0.6rem; }
}

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

/* --------------------------------------------------------------- radicals */

/* Shares the chip shape with the selects above; only the room for chosen radicals
   is its own - `水 小 艹` has to fit, and past that it clips like the rest. */
.f-radicals {
  display: inline-flex;
  align-items: center;
  max-width: 5.5rem;
  padding-right: 0.4rem;
  padding-left: 0.4rem;
}

.picker {
  width: min(56rem, 94vw);
  max-height: 88vh;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 0.7rem;
  background: var(--paper);
  color: var(--ink);
  overflow: hidden;
}

.picker::backdrop { background: rgba(10, 12, 18, 0.45); }

.picker-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.7rem 1rem;
  border-bottom: 1px solid var(--rule);
  margin: 0;
}

.picker-head h2 { margin: 0; font-size: 1rem; font-weight: 650; }

.picker-x {
  border: 0;
  background: transparent;
  font-size: 1.3rem;
  line-height: 1;
  cursor: pointer;
  color: var(--ink-3);
  padding: 0 0.2rem;
}

.picker-body { display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(0, 1fr); }

.picker-left { padding: 0.8rem 1rem; border-right: 1px solid var(--rule); min-width: 0; }
.picker-right { padding: 0.8rem 1rem; min-width: 0; display: flex; flex-direction: column; }

.picked-head, .strokes-head, .preview-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--ink-3);
  margin-bottom: 0.4rem;
}

.strokes-head, .preview-head { margin-top: 0.9rem; }
.picker-right .preview-head { margin-top: 0; }

.picked-clear {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  border: 1px solid var(--rule);
  background: transparent;
  border-radius: 0.35rem;
  cursor: pointer;
  color: var(--ink-3);
  padding: 0;
  flex: none;
}

.picked-clear svg { width: 1.05rem; height: 1.05rem; }

/* Red on hover only: emptying the basket is easy to undo - the radicals are still
   in the grid - but it should still say what it is before it happens. */
.picked-clear:hover { border-color: #c0392b; color: #c0392b; }

.picked { display: flex; flex-wrap: wrap; gap: 0.35rem; min-height: 2.2rem; }
.picked-none { color: var(--ink-3); font-size: 0.8rem; align-self: center; }

.picked-chip {
  display: inline-flex;
  align-items: center;
  gap: 0.25rem;
  padding: 0.2rem 0.4rem;
  border: 1px solid var(--accent);
  border-radius: 0.4rem;
  background: var(--accent-soft);
  color: var(--accent);
  cursor: pointer;
}

.picked-chip b { font-size: 1.15rem; font-weight: 500; line-height: 1; }
.picked-x { font-size: 0.85rem; opacity: 0.7; }

.strokes { display: flex; flex-wrap: wrap; gap: 0.25rem; }

.stroke-tab {
  min-width: 2rem;
  padding: 0.2rem 0.4rem;
  border: 1px solid var(--rule);
  border-radius: 0.35rem;
  background: var(--paper);
  cursor: pointer;
  font-size: 0.82rem;
  font-variant-numeric: tabular-nums;
}

.stroke-tab.is-on { background: var(--accent); border-color: var(--accent); color: #fff; font-weight: 600; }
.stroke-tab-wide { min-width: auto; padding-inline: 0.6rem; }

.radicals {
  margin-top: 0.5rem;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(2.7rem, 1fr));
  gap: 0.3rem;
  max-height: 16rem;
  overflow-y: auto;
  padding-right: 0.2rem;
}

.radical {
  min-height: 2.9rem;
  padding: 0.2rem 0.1rem;
  border: 1px solid var(--rule);
  border-radius: 0.35rem;
  background: var(--paper);
  cursor: pointer;
  line-height: 1.05;
  color: var(--ink);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 0.05rem;
}

/* Spans the row it starts, so the grid keeps its columns and the label keeps its
   place at the left edge where a scanning eye already is. */
.radical-rule {
  grid-column: 1 / -1;
  font-size: 0.68rem;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--ink-3);
  padding: 0.35rem 0 0.1rem;
  border-top: 1px solid var(--rule);
  font-variant-numeric: tabular-nums;
}

.radical-rule:first-child { border-top: 0; padding-top: 0; }

.radical:hover:not(:disabled) { border-color: var(--accent); color: var(--accent); }
/* The shape belonging to this stroke count, full size. */
.radical-main { font-size: 1.25rem; }

/* And what it is short for, or what is short for it - readable, under it, muted.
   The superscript this replaces was 10 px and nobody could read it. */
.radical-alt { font-size: 0.78rem; color: var(--ink-3); }
.radical.has-alt .radical-main { font-size: 1.15rem; }
.radical.is-on .radical-alt { color: var(--accent); opacity: 0.75; }
.radical.is-on { background: var(--accent-soft); border-color: var(--accent); color: var(--accent); }

/* Dimmed, not hidden: a radical that cannot narrow the current pick is still worth
   seeing in its place, so the grid does not reflow under the finger and so the
   reader can tell "not with this" from "not in the list". */
.radical:disabled { opacity: 0.22; cursor: default; }

.preview {
  flex: 1;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(2.6rem, 1fr));
  gap: 0.3rem;
  overflow-y: auto;
  align-content: start;
  max-height: 22rem;
}

.preview-cell {
  aspect-ratio: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  border: 1px solid var(--rule);
  border-radius: 0.35rem;
  font-size: 1.3rem;
  text-decoration: none;
  color: inherit;
}

.preview-cell:hover { border-color: var(--accent); color: var(--accent); }

.picker-foot {
  padding: 0.7rem 1rem;
  border-top: 1px solid var(--rule);
  display: flex;
  justify-content: flex-end;
}

.picker-apply {
  padding: 0.45rem 1.1rem;
  border: 0;
  border-radius: 0.4rem;
  background: var(--accent);
  color: #fff;
  font-weight: 600;
  cursor: pointer;
}

@media (max-width: 860px) {
  .picker { width: 96vw; max-height: 92vh; }
  .picker-body { grid-template-columns: minmax(0, 1fr); overflow-y: auto; }
  .picker-left { border-right: 0; border-bottom: 1px solid var(--rule); }
  .radicals { max-height: 11rem; }

  /* Five rows before it starts to scroll, not three. Stacked rather than
     side-by-side on a phone, the preview was the shortest thing on screen and the
     first to run out - and it is the half that tells you whether the pick was the
     one you meant. Written as the arithmetic so it stays five rows if the cell
     size changes: five cells, four gaps, and a sliver so the sixth row peeks and
     says there is more. */
  .preview { max-height: calc(5 * 2.6rem + 4 * 0.3rem + 0.6rem); }
}

/* ------------------------------------------------- the card as a control */

/* Anything in the card that names a set. Underlined on hover rather than always,
   because a card with thirty underlines reads as a page of links instead of as a
   card - the affordance is worth less than the calm. */
/* No radius here. This class is added to things that already have a shape - a pill
   is a 999px capsule, a component chip is a 0.45rem rounded box - and a radius of
   its own flattened both of them the moment they became clickable: JLPT and Lớp
   came out square beside a Phổ biến that was still round. What `.k-tap` is for is
   the pointer, the hover and the focus ring, and none of those need one. */
.k-tap { cursor: pointer; }
.k-tap:hover { color: var(--accent); text-decoration: underline; text-underline-offset: 2px; }
.k-tap:focus-visible { outline: 2px solid var(--accent); outline-offset: 1px; }
.k-chip.k-tap:hover { text-decoration: none; }

/* --------------------------------------------------------------- settings */

.gear {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 2.1rem;
  height: 2.1rem;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 0.5rem;
  background: transparent;
  color: var(--ink-2);
  cursor: pointer;
}

.gear svg { width: 1.15rem; height: 1.15rem; }
.gear:hover { border-color: var(--accent); color: var(--accent); }

/* Hangs off the gear rather than covering the page; `placeSheet` sets top and left
   when it opens, and the width is fixed here so it can do that before the contents
   arrive. Scrolling belongs to the body, so the title and the close button stay put
   on a short window. */
/* `display` belongs to the open state alone.
 *
 * The browser hides a closed popover with `[popover]:not(:popover-open) { display:
 * none }` from its own stylesheet - and any author declaration beats a user-agent
 * one whatever the specificity says, so `display: flex` here meant the panel was
 * never hidden. It stayed laid out and hit-testable over the top of the page after
 * being closed, invisible only because the transition leaves it at `opacity: 0`,
 * and swallowed every wheel and touch that landed on it: `elementFromPoint` in the
 * middle of a closed page came back `SPAN.k-set-head-what`. Scrolling down still
 * worked - that gesture was reaching the page around the panel - and scrolling up
 * did nothing at all, because the panel's own body was already at its top.
 */
.sheet {
  position: fixed;
  flex-direction: column;
  width: min(21rem, calc(100vw - 1rem));
  max-height: min(80vh, calc(100vh - 4.6rem));
  /* Same reason as `body` above: on a phone `vh` is measured against the screen
     with the address bar hidden, so a panel sized in `vh` hangs below the visible
     edge while the bar is showing - and it is fixed, so nothing can scroll to it. */
  max-height: min(80dvh, calc(100dvh - 4.6rem));
  margin: 0;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 0.7rem;
  background: var(--paper);
  color: var(--ink);
  box-shadow: 0 0.6rem 1.8rem rgba(10, 12, 18, 0.18);
  overflow: hidden;
}

.sheet:popover-open { display: flex; }

.sheet-head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 0.6rem 0.55rem 0.6rem 0.9rem;
  border-bottom: 1px solid var(--rule);
}

.sheet-head h2 {
  margin: 0;
  font-size: 0.72rem;
  font-weight: 650;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  color: var(--ink-2);
}

.sheet-body { padding: 0.7rem 0.85rem 0.9rem; overflow-y: auto; }

/* Above the folded groups, and never folded itself. */
/* The version on the left, the way to fetch a new one on the right - the same
   shape the card's own panel uses at its foot, for the same reason. */
.sheet-foot {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 0.5rem;
  margin-top: 0.7rem;
  padding-top: 0.6rem;
  border-top: 1px solid var(--rule);
}

.sheet-ver {
  display: inline-flex;
  align-items: baseline;
  flex-wrap: wrap;
  gap: 0.35rem;
  min-width: 0;
  font-size: 0.72rem;
  color: var(--ink-3);
}

.sheet-ver-k { text-transform: uppercase; letter-spacing: 0.05em; }
.sheet-ver-v { font-family: inherit; color: var(--ink-2); font-variant-numeric: tabular-nums; }

/* Only there when there is something to say. "Up to date" is the answer nine times
   in ten, and spending the line on it leaves nowhere for the one that matters. */
.sheet-ver-new {
  padding: 0.05rem 0.35rem;
  border-radius: 999px;
  background: var(--accent-soft);
  color: var(--accent);
  font-weight: 600;
}

.sheet-refresh {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 1.9rem;
  height: 1.9rem;
  padding: 0;
  border: 1px solid var(--rule);
  border-radius: 0.45rem;
  background: transparent;
  color: var(--ink-3);
  cursor: pointer;
}

.sheet-refresh svg { width: 1.05rem; height: 1.05rem; }
.sheet-refresh:hover { border-color: var(--accent); color: var(--accent); }
.sheet-refresh.on { border-color: var(--accent); background: var(--accent); color: #fff; }

/* A dot on the gear, so a reader who never opens the panel still finds out. */
.gear { position: relative; }

.gear.has-update::after {
  content: "";
  position: absolute;
  top: 0.28rem;
  right: 0.28rem;
  width: 0.42rem;
  height: 0.42rem;
  border-radius: 999px;
  background: var(--accent);
  box-shadow: 0 0 0 2px var(--paper);
}

.sheet-lang { margin-bottom: 0.7rem; }

.sheet-lang-title {
  margin: 0 0 0.4rem;
  font-size: 0.7rem;
  font-weight: 650;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--ink-2);
}

/* Offline is the one thing in this panel that is not a preference - it does
   something and takes time doing it - so it is the one thing drawn as an action
   rather than as a row of choices. */
.sheet-offline { display: flex; flex-direction: column; gap: 0.5rem; padding: 0.15rem 0 0.1rem; }

.sheet-offline-line { margin: 0; font-size: 0.76rem; line-height: 1.4; color: var(--ink-2); }

.sheet-offline-track {
  height: 0.3rem;
  border-radius: 0.15rem;
  background: var(--accent-soft);
  overflow: hidden;
}

.sheet-offline-track span { display: block; height: 100%; width: 0; background: var(--accent); }

.sheet-offline-tools { display: flex; gap: 0.4rem; }

.sheet-btn {
  padding: 0.35rem 0.7rem;
  border: 1px solid var(--accent);
  border-radius: 0.4rem;
  background: var(--accent);
  color: #fff;
  font: inherit;
  font-size: 0.78rem;
  font-weight: 600;
  cursor: pointer;
}

.sheet-btn:hover { filter: brightness(1.06); }

.sheet-btn-quiet {
  border-color: var(--rule);
  background: transparent;
  color: var(--ink-3);
  font-weight: 500;
}

.sheet-btn-quiet:hover { border-color: var(--accent); color: var(--accent); filter: none; }
.sheet-btn-stop { border-color: var(--rule); background: transparent; color: var(--ink-2); }

.sheet-lang-opts { display: grid; grid-auto-columns: 1fr; grid-auto-flow: column; gap: 0.35rem; }
.sheet-lang-opts .k-set-opt {
  margin: 0;
  padding: 0.4rem 0.5rem;
  font: inherit;
  font-size: 0.9rem;
  line-height: 1.5;
  text-align: center;
  color: var(--ink-3);
  background: var(--paper);
  border: 1px solid var(--rule);
  border-radius: 0.4rem;
  box-shadow: none;
  cursor: pointer;
}
.sheet-lang-opts .k-set-opt:hover { color: var(--accent); border-color: var(--accent); }
.sheet-lang-opts .k-set-opt.k-set-on {
  color: var(--accent);
  border-color: var(--accent);
  background: var(--accent-soft);
  font-weight: 600;
}

/* Opens with a short drop rather than appearing - the panel belongs to the button
   above it, and the movement says which. `allow-discrete` is what lets a property
   the browser flips outright take part in a transition. */
@media (prefers-reduced-motion: no-preference) {
  .sheet {
    opacity: 0;
    transform: translateY(-0.35rem);
    transition: opacity 0.13s ease, transform 0.13s ease,
                overlay 0.13s allow-discrete, display 0.13s allow-discrete;
  }

  .sheet:popover-open { opacity: 1; transform: none; }

  @starting-style {
    .sheet:popover-open { opacity: 0; transform: translateY(-0.35rem); }
  }
}

/* The panel is drawn with the card's own class names so `card.css` styles it, and
   that stylesheet is copied from the deck byte for byte. `.kanji-card` is on the
   host only to inherit `--tone-*`; everything it does to a page - the column, the
   width cap, the padding, the paper - the panel supplies for itself. */
.k-set-host {
  display: block;
  max-width: none;
  margin: 0;
  padding: 0;
  background: transparent;
  gap: 0;
}

.k-set-host .k-set-panel { margin-top: 0; }

/* ------------------------------------------------------------- handwriting */

.draw { width: min(50rem, 94vw); }
.draw-body { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); }
.draw-left { padding: 0.9rem 1rem; border-right: 1px solid var(--rule); }
.draw-right { padding: 0.9rem 1rem; display: flex; flex-direction: column; min-width: 0; }

/* Square whatever the dialog's width, because the templates are square and a pad
   that is not distorts every drawing the same way the reader cannot see. */
.pad-wrap { aspect-ratio: 1; max-height: 46vh; margin: 0 auto; }

#pad {
  width: 100%;
  height: 100%;
  display: block;
  border: 1px solid var(--rule);
  border-radius: 0.5rem;
  background: var(--sunk);
  touch-action: none;      /* or the browser pans the page instead of drawing */
  cursor: crosshair;
  --guide: #ccd3e2;
  --brush: #14161d;
}

:root[data-theme="dark"] #pad { --guide: #363d4f; --brush: #e8eaf1; }

@media (prefers-color-scheme: dark) {
  :root:not([data-theme="light"]) #pad { --guide: #363d4f; --brush: #e8eaf1; }
}

.draw-tools { display: flex; align-items: center; gap: 0.4rem; margin-top: 0.6rem; }

.draw-btn {
  padding: 0.3rem 0.7rem;
  border: 1px solid var(--rule);
  border-radius: 0.4rem;
  background: var(--paper);
  color: var(--ink-2);
  font-size: 0.82rem;
  cursor: pointer;
}

.draw-btn:hover { border-color: var(--accent); color: var(--accent); }

.draw-count {
  margin-left: auto;
  font-size: 0.75rem;
  color: var(--ink-3);
  font-variant-numeric: tabular-nums;
}

.draw-hint { margin: 0.6rem 0 0; font-size: 0.74rem; line-height: 1.45; color: var(--ink-3); }

@media (max-width: 860px) {
  .draw { width: 96vw; }
  .draw-body { grid-template-columns: minmax(0, 1fr); }
  .draw-left { border-right: 0; border-bottom: 1px solid var(--rule); }
  .pad-wrap { max-height: 38vh; }
  .draw-right .preview { max-height: calc(3 * 2.6rem + 2 * 0.3rem + 0.6rem); }
}
