/* Electroharmonix (Ray Larabie), the faux-katakana display face used for the
   wordmark and nothing else. The file is vendored rather than linked: this page
   is opened from file:// as often as from a server, and a remote font would make
   the title depend on being online. */
@font-face {
  font-family: Electroharmonix;
  font-style: normal;
  font-weight: 400;
  /* Shown in the fallback face until the file arrives rather than hidden, so the
     name is never a blank space on a slow read from disk. */
  font-display: swap;
  src: url("assets/fonts/electroharmonix.woff") format("woff");
}
/* Shippori Mincho B1 Bold (SIL OFL), cut down by scripts/build_sumi_font.py to
   the kanji and kana the app can show. The games set their big characters in
   it: the system Mincho the stack below asks for is an optional download on
   Windows, and without it every kanji fell back to a Gothic. Japanese only —
   Latin in the same element carries on in its own face. */
@font-face {
  font-family: "Sumi Mincho";
  font-style: normal;
  font-weight: 700;
  font-display: swap;
  src: url("assets/fonts/sumi-mincho-bold.woff2") format("woff2");
  unicode-range: U+2E80-2FDF, U+3000-30FF, U+3400-4DBF, U+4E00-9FFF, U+F900-FAFF, U+FF00-FFEF, U+20000-2FFFF;
}

:root {
  /* A lit cabinet rather than a printed page: everything sits on one deep
     ground, every panel is a surface catching light, and saturated colour is
     spent only on the things that change — progress, the answer, the strokes
     being asked about. The old paper palette made the app read as an article
     about kanji instead of a thing you play. */
  --bg: #0d213e;
  --panel: rgba(255, 255, 255, 0.08);
  --panel-lit: rgba(255, 255, 255, 0.12);
  --well: rgba(3, 18, 40, 0.58);
  --ink: #ecf4ff;
  --muted: #9bb3ca;
  --line: rgba(160, 206, 255, 0.26);
  --line-lit: rgba(138, 201, 255, 0.58);
  --paper: #1a3252;
  --blue: #58a6ff;
  --blue-deep: #b7dbff;
  --orange: #ffaf45;
  --orange-pale: rgba(255, 175, 69, 0.16);
  --green: #6fe7b7;
  --red: #ff8a87;
  /* Stars climb rather than all wearing one colour: the low rungs are earned in
     an evening, the high ones only over weeks, and the palette should say so. */
  --star-low: #d9b25f;
  --star: #ffd166;
  --shadow: 0 26px 64px rgba(0, 0, 0, 0.55);
  --lift: 0 14px 34px rgba(0, 0, 0, 0.45);
  /* Japanese glyph stacks. Never fall back to the generic `serif`/`sans-serif`
     keywords for CJK: on a machine with no Japanese font the browser resolves
     those to a Chinese face (SimSun, Microsoft YaHei), which draws Chinese
     variants of 糸, 言, 金, 牛 and hops fonts per character, so components
     printed beside a kanji no longer match the kanji. Both stacks therefore end
     on faces that ship with Windows/macOS and cover the radical blocks. */
  --jp-serif: "Yu Mincho", "YuMincho", "Hiragino Mincho ProN", "Noto Serif JP",
    "Noto Serif CJK JP", "BIZ UDMincho", "MS Mincho", "Yu Gothic", "Hiragino Sans",
    "Noto Sans JP", "MS Gothic", sans-serif;
  --jp-sans: "Yu Gothic UI", "Yu Gothic", "Hiragino Sans", "Noto Sans JP",
    "Meiryo", "MS Gothic", sans-serif;
  --ui: "Segoe UI", "Trebuchet MS", Tahoma, "Arial", sans-serif;
}

* { box-sizing: border-box; }

body {
  margin: 0;
  min-width: 320px;
  min-height: 100dvh;
  color: var(--ink);
  background-color: var(--bg);
  /* Three soft blue and sunlit layers aimed at the page plane, tuned to a
     messenger-like desktop background. Fixed, so scrolling moves the content
     across the light instead of dragging it with it. */
  background-image:
    linear-gradient(180deg, #17385e 0%, #0f2d4f 52%, #0d2546 100%),
    radial-gradient(980px 590px at 10% -14%, rgba(102, 177, 255, 0.26), transparent 64%),
    radial-gradient(840px 620px at 92% 12%, rgba(255, 154, 71, 0.12), transparent 66%);
  background-attachment: fixed;
  font-family: var(--ui);
}

/* A faint board grid, faded out below the fold. It is the cheapest way to say
   "play surface" without putting a texture behind the kanji, which have to stay
   the only detailed thing on screen. */
body::before {
  content: "";
  position: fixed;
  z-index: 0;
  inset: 0;
  pointer-events: none;
  background-image: linear-gradient(rgba(255, 255, 255, 0.013) 1px, transparent 1px),
                    linear-gradient(90deg, rgba(255, 255, 255, 0.013) 1px, transparent 1px);
  background-size: 46px 46px;
  opacity: 0.55;
  -webkit-mask-image: radial-gradient(circle at 50% 0%, #000, transparent 82%);
  mask-image: radial-gradient(circle at 50% 0%, #000, transparent 82%);
}

/* Radicals drifting up behind everything. It sits between the board grid and the
   shell, and stays far enough down in contrast that the kanji on a card is still
   the only detailed thing on screen — the moment it competes for the eye it has
   stopped being a background. Transform-only animation, so it never lays out. */
.kanji-drift { position: fixed; z-index: 0; inset: 0; overflow: hidden; pointer-events: none; }
.kanji-drift span {
  position: absolute;
  bottom: -22vh;
  color: var(--blue-deep);
  opacity: 0.04;
  font-family: var(--jp-serif);
  line-height: 1;
  user-select: none;
  will-change: transform;
  animation: kanji-drift var(--drift-time) var(--drift-delay) linear infinite;
}

@keyframes kanji-drift {
  from { transform: translate3d(0, 0, 0) rotate(0deg); }
  to { transform: translate3d(var(--drift-sway), -128vh, 0) rotate(var(--drift-turn)); }
}

/* Square corners throughout. This is a cabinet of panels and keys rather than a
   deck of soft cards, and the character on screen is the only thing allowed a
   curve. Set once rather than as a value on each surface, so a panel added later
   cannot arrive rounded on its own. */
*, *::before, *::after { border-radius: 0; }
body.is-playing .hero,
body.is-playing .start-menu,
body.is-playing .game,
body.is-playing .hud,
body.is-playing .card-face,
body.is-playing .flash-card,
body.is-playing .answer-form,
body.profile-locked .profile-panel,
.start-menu,
.deck-option,
.hud-menu-panel .text-button,
.session-done-panel,
.completion-popup {
  border-radius: 12px;
}

button,
input,
select,
textarea {
  border-radius: 10px;
}

button { font: inherit; }
button:focus-visible { outline: 2px solid var(--blue); outline-offset: 3px; }

body.profile-locked { overflow: hidden; }
body.profile-locked .shell,
body.profile-locked .kanji-drift { visibility: hidden; }

.profile-gate {
  position: fixed;
  z-index: 50;
  inset: 0;
  display: grid;
  place-items: center;
  overflow-y: auto;
  padding: 24px;
  background:
    radial-gradient(680px 420px at 20% 0%, rgba(85, 164, 255, 0.2), transparent 70%),
    radial-gradient(620px 420px at 90% 100%, rgba(255, 161, 77, 0.15), transparent 70%),
    rgba(5, 9, 17, 0.96);
}
.profile-gate[hidden] { display: none; }
.profile-panel { width: calc(100vw - 48px); max-width: 610px; min-width: 0; overflow: hidden; padding: 30px; text-align: center; background: var(--paper); border: 1px solid var(--line-lit); box-shadow: var(--shadow); }
.profile-panel h1 { margin: 5px 0 8px; font-family: var(--ui); font-size: clamp(1.75rem, 6vw, 2.7rem); letter-spacing: -0.035em; text-transform: none; }
.profile-intro { margin: 0 0 24px; color: var(--muted); font-size: 0.9rem; }
.profile-picker-view[hidden],
.profile-choices[hidden] { display: none; }
.profile-choices { display: grid; min-width: 0; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 16px; }
.profile-choice { display: grid; min-width: 0; gap: 12px; justify-items: center; padding: 16px; cursor: pointer; color: var(--ink); background: rgba(255, 255, 255, 0.05); border: 1px solid var(--line); transition: transform 150ms ease, border-color 150ms ease, background 150ms ease, box-shadow 150ms ease; }
.profile-choice:hover { transform: translateY(-3px); background: rgba(85, 164, 255, 0.11); border-color: var(--line-lit); box-shadow: var(--lift); }
.profile-choice img { display: block; width: 100%; max-width: 190px; aspect-ratio: 1; object-fit: cover; border: 3px solid rgba(255, 255, 255, 0.16); border-radius: 50%; }
.profile-choice strong { font-size: 1.05rem; letter-spacing: 0.08em; text-transform: uppercase; }
.profile-choice small { margin-top: -8px; color: var(--muted); font-size: 0.62rem; }
.profile-password { display: grid; width: min(100%, 360px); justify-items: stretch; margin: 0 auto; text-align: left; }
.profile-password[hidden] { display: none; }
.profile-password img { display: block; width: 126px; aspect-ratio: 1; margin: 4px auto 12px; object-fit: cover; border: 3px solid var(--line-lit); border-radius: 50%; }
.profile-password p { margin: 0; text-align: center; color: var(--muted); font-size: 0.74rem; }
.profile-password h2 { margin: 2px 0 18px; text-align: center; }
.profile-password label { margin-bottom: 7px; color: var(--muted); font-size: 0.7rem; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; }
.profile-password input { width: 100%; height: 52px; padding: 0 14px; color: var(--ink); background: var(--well); border: 1px solid var(--line-lit); font: 1rem var(--ui); }
.profile-password input:focus { outline: 2px solid var(--blue); outline-offset: 2px; }
.profile-submit { margin-top: 12px; padding: 14px 18px; cursor: pointer; color: #08101d; background: var(--blue); border: 1px solid var(--blue); font-weight: 850; letter-spacing: 0.07em; text-transform: uppercase; }
.profile-back { justify-self: start; margin: 0 0 14px; padding: 0; cursor: pointer; color: var(--blue-deep); background: transparent; border: 0; font-size: 0.72rem; font-weight: 800; }
.profile-password .profile-feedback { min-height: 1.3em; margin-top: 10px; color: var(--red); font-weight: 750; }

.shell { position: relative; z-index: 1; width: min(100% - 34px, 1400px); margin: 0 auto; padding: 26px 0 40px; }

/* Playing is a screen, not a section of a page: the frame is exactly the window,
   the HUD sits at the top of it and the controls at the bottom, and nothing
   scrolls out from under the card. This is claimed at every size. What used to
   gate it on a window being big enough was the worry that a short one cannot
   hold a kanji and five answer boxes at once — so the way to always fit is to
   let the contents shrink, below, rather than to let the page grow. The stage
   keeps overflow-y and safe centring as the last resort: on a window too small
   for any layout the card scrolls inside its own box, so nothing is ever
   unreachable, but the frame around it still holds. */
body.is-playing { overflow: hidden; }
body.is-playing .shell { display: flex; height: 100dvh; flex-direction: column; width: min(100% - 24px, 1560px); padding: 14px 0 18px; }
body.is-playing .game { display: flex; flex: 1 1 auto; min-height: 0; flex-direction: column; }
body.is-playing .flashcard { display: flex; flex: 1 1 auto; min-height: 0; }
/* min-height:0 lets the stage shrink below the card's own minimum on a short
   window, and safe centring keeps the top of a card that outgrows the stage
   reachable instead of centring it out of reach. */
body.is-playing .card-face { flex: 1 1 auto; min-height: 0; overflow-y: auto; justify-content: safe center; scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.22) transparent; }
body.is-playing .card-face::-webkit-scrollbar { width: 8px; }
body.is-playing .card-face::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.18); }
body.is-playing .card-face::-webkit-scrollbar-track { background: transparent; }

/* The same bargain for the stages the rule above was never extended to. Each is
   a column with a bar at the top, a body, and the controls at the foot; the body
   is what takes the height and what scrolls when there is none, so the controls
   stay on the frame and nothing is unreachable. Without this the flash stage was
   a stack of three items that refused to shrink — a card with a 344px minimum,
   430px once its picture is open — so on a window under about 600px tall the
   controls rendered past the bottom of a frame that does not scroll, and the
   button that opens the answer boxes could not be pressed at all. */
body.is-playing .flash-stage,
body.is-playing .snap-stage,
body.is-playing .grid-stage { flex: 1 1 auto; min-height: 0; }
/* The card has to become a column for its face to be a flex item of it, or the
   face keeps its own content height and spills out through the bottom of the
   card instead of scrolling inside it. It is sized by its content and allowed to
   shrink, not told to grow: a card stretched to fill a tall stage is a lot of
   green around a small kanji. */
body.is-playing .flash-card { display: flex; flex-direction: column; flex: 0 1 auto; min-height: 0; }
/* What gives way, in order: the picture first, then the face scrolls. Never the
   answers — they are the card. Left shrinkable they collapsed to a nought-high
   scroller and the boxes vanished off a card that still had room for them. */
body.is-playing .flash-answers { flex: 0 0 auto; }
body.is-playing .flash-face,
body.is-playing .snap-table,
body.is-playing .grid-cards { flex: 1 1 auto; min-height: 0; overflow-y: auto; justify-content: safe center; scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.22) transparent; }
body.is-playing .flash-controls,
body.is-playing .snap-controls,
body.is-playing .grid-finish { flex: 0 0 auto; }
body.is-playing .flash-face::-webkit-scrollbar,
body.is-playing .snap-table::-webkit-scrollbar,
body.is-playing .grid-cards::-webkit-scrollbar { width: 8px; }
body.is-playing .flash-face::-webkit-scrollbar-thumb,
body.is-playing .snap-table::-webkit-scrollbar-thumb,
body.is-playing .grid-cards::-webkit-scrollbar-thumb { background: rgba(255, 255, 255, 0.18); }
body.is-playing .flash-face::-webkit-scrollbar-track,
body.is-playing .snap-table::-webkit-scrollbar-track,
body.is-playing .grid-cards::-webkit-scrollbar-track { background: transparent; }
/* The maze is left out on purpose: its canvas is sized in JS from the card
   count, a flex basis would change the map it draws, and it has no control at
   its foot for a short window to put out of reach. */

/* Fitting a short window is a matter of giving back everything that is padding
   rather than content. The character is sized in vh already, so it comes down on
   its own; these are the fixed costs around it — the tall uppercase labels
   above each box, and the padding on every nested panel. */
@media (max-height: 760px) {
  body.is-playing .shell { padding: 8px 0 10px; }
  body.is-playing .game { padding: 10px; }
  body.is-playing .hud { margin-top: 7px; padding: 6px 10px; }
  body.is-playing .card-face { min-height: 0; padding: 12px 14px; }
  body.is-playing .answer-form { margin-top: 8px; padding: 10px 12px 8px; }
  body.is-playing .answer-fields { gap: 9px; }
  body.is-playing .answer-field label { min-height: 0; margin-bottom: 4px; }
  body.is-playing .answer-field input { height: 40px; }
  body.is-playing .streak-status { padding: 5px 11px; }
  /* The status line and the deck's kicker are the first things the bar can lose
     when the height is wanted for the character. */
  body.is-playing .hud-status, body.is-playing #deck-kicker { display: none; }
  /* The same for the stages' own hint lines, and the study card's picture comes
     down before its answers do — the picture is the hook, the boxes are the card. */
  body.is-playing .flash-hint, body.is-playing .snap-hint, body.is-playing .grid-hint { display: none; }
  body.is-playing .flash-mnemonic { height: 110px; min-height: 72px; max-height: 110px; }
  body.is-playing .flash-card, body.is-playing .flash-face,
  body.is-playing .flash-card.has-mnemonic, body.is-playing .flash-card.has-mnemonic .flash-face { min-height: 0; }
  body.is-playing .flash-stage { gap: 10px; }
  body.is-playing .flash-face { padding: 14px 18px; }
  /* Only the clamp *floor* comes down here. A 5rem minimum is what overflows a
     short window, so the character has to be allowed to shrink — but the vh term
     stays generous, because this block applies to any window under 760px tall
     and that includes plenty of ordinary laptops with room to spare. Clamping
     the vh term here as well was what kept the character small on a 695px
     window: this rule won, and every ceiling raised elsewhere never applied. */
  body.is-playing .glyph { margin: 0; font-size: clamp(2.6rem, min(58vw, 40vh), 16rem); }
  body.is-playing .flashcard.is-partial-card .glyph { margin: 0; font-size: clamp(2.6rem, min(54vw, 40vh), 16rem); }
  body.is-playing .flashcard.is-form-card .glyph { margin: 0; font-size: clamp(2.6rem, min(30vw, 40vh), 16rem); }
}

/* Shorter still: the prompt chip and the progress readout are the last things
   worth the space they take, so they go before the character does. */
@media (max-height: 560px) {
  body.is-playing .prompt { font-size: 0.82rem; }
  body.is-playing .answer-form { margin-top: 5px; }
  body.is-playing .answer-field input { height: 36px; font-size: 0.95rem; }
  body.is-playing .card-face { padding: 8px 10px; }
  body.is-playing .glyph { margin: 0; font-size: clamp(1.9rem, min(54vw, 36vh), 11rem); }
  body.is-playing .flashcard.is-partial-card .glyph { margin: 0; font-size: clamp(1.9rem, min(50vw, 36vh), 11rem); }
  body.is-playing .flashcard.is-form-card .glyph { margin: 0; font-size: clamp(1.9rem, min(28vw, 36vh), 11rem); }
}

/* Past this width the console stands beside the stage rather than under it. The
   character keeps the middle of the screen at full size, the boxes sit off to
   one side where the hands are, and the card stops being a form on a page. */
@media (min-width: 1240px) and (min-height: 700px) {
  body.is-playing .card-face { flex-direction: row; align-items: center; gap: 26px; padding: 22px; }
  body.is-playing .card-stage { flex: 1 1 auto; min-width: 0; }
  body.is-playing .answer-form,
  body.is-playing .flashcard.is-form-card .answer-form,
  body.is-playing .flashcard.is-partial-card .answer-form { flex: 0 0 clamp(400px, 31%, 540px); width: auto; align-self: center; margin-top: 0; }
  /* The side console is a narrow column, so the boxes pair up two to a row. */
  body.is-playing .answer-field { flex-basis: 44%; }
  /* With the boxes off to the side the stage has its whole height to spend, and
     it should spend it on the character: this is the thing being learned, and at
     this size the strokes are readable rather than merely identifiable. */
  /* The console stands beside the stage here, so the character has roughly half
     the window's width rather than all of it — hence the tighter vw terms. */
  body.is-playing .glyph { font-size: clamp(7rem, min(34vw, 48vh), 27rem); }
  body.is-playing .flashcard.is-form-card .glyph { font-size: clamp(5.5rem, min(25vw, 41vh), 21rem); }
  body.is-playing .flashcard.is-partial-card .glyph { font-size: clamp(6rem, min(32vw, 44vh), 23rem); }
}

/* The masthead is for the menu. Once a card is on screen it is just text
   between you and the kanji. */
body.is-playing .hero { display: none; }

/* A title bar holding the name and nothing else. The strapline and the category
   badge that used to sit beside it were copy about the app rather than part of
   it, and a game does not explain itself above its own menu. With the name
   standing alone it is centred: left-aligned type reads as the start of a
   paragraph that never arrives. */
.hero { position: relative; max-width: none; margin: 0 0 16px; padding: 15px 18px; text-align: center; background: linear-gradient(180deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.045)); border: 1px solid var(--line-lit); }
.eyebrow { margin: 0; color: var(--blue-deep); font-size: 0.66rem; font-weight: 800; letter-spacing: 0.15em; text-transform: uppercase; }
.hero h1 { margin: 0; }
.active-profile { position: absolute; top: 50%; right: 12px; display: flex; align-items: center; gap: 8px; padding: 5px 10px 5px 5px; cursor: pointer; color: var(--ink); background: rgba(255, 255, 255, 0.055); border: 1px solid var(--line); transform: translateY(-50%); }
.active-profile:hover { border-color: var(--line-lit); background: rgba(88, 166, 255, 0.16); }
.active-profile img { width: 31px; height: 31px; object-fit: cover; border-radius: 50%; }
.active-profile-copy { display: grid; justify-items: start; gap: 1px; }
.active-profile-copy span { font-size: 0.68rem; font-weight: 850; letter-spacing: 0.07em; text-transform: uppercase; }
.active-profile-copy small { color: var(--muted); font-size: 0.55rem; font-weight: 700; letter-spacing: 0.025em; }

h1, h2, p { margin-top: 0; }
h1 {
  margin-bottom: 0;
  /* Electroharmonix draws each Latin letter as a katakana lookalike, which is the
     whole wordmark — so it is named alone with no fallback stack. If the file
     fails to load the browser's default face shows the name in plain letters,
     which is the right failure: legible, just not styled. */
  font-family: Electroharmonix;
  font-size: clamp(1.9rem, 4.4vw, 3rem);
  font-weight: 400;
  line-height: 1.15;
  /* The glyphs are dense and read as kana, so they need air between them; the
     indent gives back the trailing letter-space so the centring is optical. */
  letter-spacing: 0.16em;
  text-indent: 0.16em;
  color: #d9f0ff;
  /* A soft highlight with a darker edge keeps the retro window vibe without
     flattening the lettering on bright gradients. */
  text-shadow: 0 2px 0 #0a2648, 0 0 20px rgba(88, 166, 255, 0.22);
}
h2 { margin-bottom: 0; font-size: clamp(1.2rem, 2.6vw, 1.6rem); font-weight: 850; letter-spacing: -0.015em; }

/* Level select. At full width one deck per row would be a mile of list for
   fifty-eight decks; a grid of tiles is something to scan and pick from, and it
   fills the screen the way a game's stage-select does. The tile itself carries
   only what tells decks apart: a glyph, a name, one line, and how far in you
   are. Anything more turned the grid into a wall at about deck fifteen. */
/* One row per line, the full width of the panel. Two and three columns packed
   more sets onto the screen but made the list a grid to hunt around; a single
   column is a list you run down, which is what up and down are steering. */
.deck-picker { display: grid; grid-template-columns: 1fr; gap: 10px; }
/* The card list stands in the picker's place, so the picker has to be able to
   go away: a class-set display beats the browser's own [hidden] rule. */
.deck-picker[hidden] { display: none; }
.menu-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 15px; padding-bottom: 13px; border-bottom: 1px solid var(--line); }
.menu-head h2 { margin: 0; }
/* Which study mode the list is in, said in the smallest way that still reads:
   the set names are long enough that a second clause in the heading wraps. */
.menu-mode { display: inline-block; margin-left: 9px; padding: 4px 10px; vertical-align: 0.18em; color: var(--blue-deep); background: rgba(85, 164, 255, 0.14); border: 1px solid rgba(85, 164, 255, 0.34); font-family: var(--ui); font-size: 0.64rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
.menu-head .text-button { flex: 0 0 auto; }
.menu-head .text-button[hidden] { display: none; }

.deck-progress { display: flex; align-items: center; gap: 8px; margin-top: 9px; color: var(--muted); font-size: 0.66rem; font-weight: 800; font-variant-numeric: tabular-nums; letter-spacing: 0.04em; }
.deck-progress i { display: block; flex: 1 1 auto; height: 6px; overflow: hidden; background: rgba(5, 9, 17, 0.6); border: 1px solid var(--line); }
.deck-progress b { display: block; height: 100%; background: linear-gradient(90deg, #3f8fe0, var(--blue)); box-shadow: 0 0 12px rgba(85, 164, 255, 0.55); transition: width 220ms ease; }
.deck-progress span { flex: 0 0 auto; }
.deck-progress.is-complete b { background: linear-gradient(90deg, #2fb987, var(--green)); box-shadow: 0 0 12px rgba(79, 224, 166, 0.55); }
.deck-progress.is-complete span { color: var(--green); }
/* A practice row is a score rather than a progress bar, and is coloured apart
   from one so the two are never read as the same claim. */
.deck-practice b { background: linear-gradient(90deg, #d8952f, var(--star)); box-shadow: 0 0 12px rgba(255, 209, 102, 0.45); }
.deck-practice span { color: var(--star); }
.deck-best { flex: 0 0 auto; color: rgba(255, 255, 255, 0.5); }
/* A practice game has no ladder, so the two readouts that report one are not
   shown during it: a card's clean passes and its stars are claims about learning
   that this kind of game does not make and must not appear to. */
body.is-practice .streak-status,
body.is-practice .card-stars { display: none; }
.stat i { font-style: normal; }

.start-menu { width: 100%; margin: 0 0 18px; padding: 20px; background: linear-gradient(180deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05)); border: 1px solid var(--line-lit); box-shadow: var(--shadow); }
.start-menu-copy { margin: 16px 0 0; color: var(--muted); font-size: 0.78rem; line-height: 1.5; }

.deck-option { position: relative; display: flex; gap: 13px; align-items: center; width: 100%; padding: 13px 15px; overflow: hidden; cursor: pointer; text-align: left; color: var(--ink); background: linear-gradient(158deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.042)); border: 1px solid var(--line); transition: transform 150ms ease, border-color 150ms ease, box-shadow 150ms ease, background 150ms ease; }
.deck-option-text { flex: 1 1 auto; min-width: 0; }
.deck-option:hover { transform: translateY(-2px); background: linear-gradient(158deg, rgba(255, 255, 255, 0.14), rgba(255, 255, 255, 0.054)); border-color: var(--line-lit); box-shadow: var(--lift), 0 0 26px rgba(138, 201, 255, 0.22); }
.deck-option.active { border-color: rgba(88, 166, 255, 0.7); box-shadow: 0 0 0 1px rgba(88, 166, 255, 0.3), 0 0 26px rgba(88, 166, 255, 0.2); }
.deck-icon { display: grid; flex: 0 0 54px; height: 54px; place-items: center; color: var(--blue); background: radial-gradient(circle at 32% 24%, rgba(88, 166, 255, 0.3), rgba(88, 166, 255, 0.08)); border: 1px solid rgba(88, 166, 255, 0.35); box-shadow: inset 0 0 20px rgba(88, 166, 255, 0.12); font-family: var(--jp-serif); font-size: 2rem; text-shadow: 0 0 18px rgba(88, 166, 255, 0.35); transition: transform 150ms ease; }
.deck-option:hover .deck-icon { transform: scale(1.06); }
.deck-option strong, .deck-option small { display: block; }
.deck-option strong { margin-bottom: 3px; font-size: 0.92rem; font-weight: 800; letter-spacing: -0.005em; }
.deck-option small { color: var(--muted); font-size: 0.73rem; line-height: 1.35; }
/* The sets list explains itself in a line under the name. Capped in measure
   rather than left to run the full width of a wide window, where a single
   sentence spanning 1400px is a line you lose your place in. */
.deck-about { max-width: 74ch; margin-bottom: 6px; color: var(--ink); }
/* The keyboard is the primary way through this list, so the focused row is
   styled as the selection rather than as a stray outline on a button. */
.deck-option:focus-visible { outline: none; border-color: var(--line-lit); background: linear-gradient(158deg, rgba(255, 255, 255, 0.13), rgba(255, 255, 255, 0.05)); box-shadow: 0 0 0 1px var(--line-lit), var(--lift), 0 0 30px rgba(85, 164, 255, 0.2); }
.deck-option:focus-visible .deck-go { transform: translateX(3px); color: var(--blue); }
.deck-go { flex: 0 0 auto; color: rgba(255, 255, 255, 0.28); font-size: 1.15rem; transition: transform 150ms ease, color 150ms ease; }
.deck-option:hover .deck-go { transform: translateX(3px); color: var(--blue); }

/* A deck row is picked rather than entered, so it ends in a box that fills in
   instead of an arrow that points somewhere. Empty until ticked: an unticked
   box still has to read as something you can press. */
.deck-tick { display: grid; flex: 0 0 auto; width: 26px; height: 26px; place-items: center; color: transparent; background: rgba(5, 9, 17, 0.5); border: 1px solid var(--line); font-size: 0.85rem; font-weight: 900; transition: color 140ms ease, background 140ms ease, border-color 140ms ease; }
.deck-option:hover .deck-tick { border-color: var(--line-lit); }
.deck-option.is-picked { border-color: rgba(79, 224, 166, 0.5); background: linear-gradient(158deg, rgba(79, 224, 166, 0.16), rgba(79, 224, 166, 0.05)); box-shadow: 0 0 0 1px rgba(79, 224, 166, 0.22), 0 0 26px rgba(79, 224, 166, 0.13); }
.deck-option.is-picked .deck-tick { color: #04211a; background: var(--green); border-color: var(--green); }

/* The Learning Path. Six tiers down the page, each a row of nodes over the same
   pile of cards, the rows getting shorter as the nodes get wider. It is drawn as
   a tree rather than as a list because the shape is the thing being said: what
   sits above what, and therefore what has to be cleared before what. */
.path-tree { display: grid; grid-template-columns: minmax(0, 172px) minmax(0, 1fr); column-gap: 16px; align-items: center; }
/* The tier is a grouping in the markup and nothing in the layout: its label and
   its row of nodes are cells of the tree's own grid, so every label lines up
   down the left and every row of nodes lines up down the right. */
.path-tier { display: contents; }

/* The name went from over the nodes to beside them. Over them it sat in the one
   band the branches have to run through, and a tree whose limbs are cut by a
   caption every second row is a stack of lists with lines drawn on it. */
.path-tier-label { grid-column: 1; display: flex; flex-wrap: wrap; gap: 3px 8px; align-items: center; padding: 10px 0; }
.path-tier-icon { display: grid; flex: 0 0 26px; height: 26px; place-items: center; color: var(--orange); background: radial-gradient(circle at 32% 24%, rgba(255, 161, 77, 0.26), rgba(255, 161, 77, 0.06)); border: 1px solid rgba(255, 161, 77, 0.3); font-family: var(--jp-serif); font-size: 0.95rem; }
.path-tier-label strong { font-family: var(--ui); font-size: 0.68rem; font-weight: 900; letter-spacing: 0.09em; text-transform: uppercase; color: var(--blue-deep); }
.path-tier-label small { flex: 1 1 100%; color: var(--muted); font-size: 0.68rem; line-height: 1.3; }
.path-tier-label .deck-progress { flex: 1 1 100%; margin-top: 2px; }

/* The band between two rows of nodes, and the whole of what makes this a tree:
   a line up from each node to every node it waits on, the siblings meeting on
   one horizontal and going down into their child as a single stem. Stretched to
   fit rather than measured, so it is right at any width without watching for a
   resize; the strokes are told not to scale with it, or the horizontals would
   come out hairlines and the verticals bars. */
/* Tall enough for a limb to read as a limb. At thirty pixels the branches were
   a sliver between two solid bands; the space around a node is as much a part of
   a tree as the node is. */
.path-branches { grid-column: 2; display: block; width: 100%; height: 46px; overflow: visible; }
.path-branches path { fill: none; stroke: rgba(255, 255, 255, 0.26); stroke-width: 1.25; }
/* A limb you can walk lights up; one already walked goes the colour of the tile
   at the end of it. Shut ones stay the colour of a rule, which is what they are. */
.path-branches path.is-open { stroke: rgba(85, 164, 255, 0.75); }
.path-branches path.is-cleared { stroke: rgba(79, 224, 166, 0.7); }

/* One column a set, the same grid under every tier, and a node spanning the
   columns its own cards sit in. That is what makes this a tree rather than six
   lists: a Range tile stands over the two Snap tiles it waits on, and a reader
   can see what opens what without reading a word. */
.path-nodes { grid-column: 2; align-self: stretch; display: grid; grid-template-columns: repeat(var(--path-columns, 10), minmax(0, 1fr)); gap: 7px; }
/* Centred in the columns it spans rather than stretched across them: the tile
   is one column wide wherever it sits, so the tiers narrow — ten, ten, five,
   three, one — and the narrowing is what says this is a tree. */
.path-node { justify-self: center; height: 100%; padding: 8px 9px; gap: 7px; }
.path-node strong { margin-bottom: 1px; font-size: 0.78rem; font-variant-numeric: tabular-nums; }
.path-node small { font-size: 0.63rem; line-height: 1.25; }
.path-node .deck-tick { width: 20px; height: 20px; font-size: 0.7rem; color: var(--muted); }

/* Open, and next. The arrow is the only saturated thing on an untouched tile,
   because on a tree where most tiles are shut the ones you can press are what
   the eye is looking for. */
.path-node:not(.is-locked):not(.is-cleared) .deck-tick { color: var(--blue); border-color: rgba(85, 164, 255, 0.4); }

.path-node.is-cleared { border-color: rgba(79, 224, 166, 0.42); background: linear-gradient(158deg, rgba(79, 224, 166, 0.13), rgba(79, 224, 166, 0.035)); }
.path-node.is-cleared .deck-tick { color: #04211a; background: var(--green); border-color: var(--green); }
.path-node.is-cleared .deck-icon { color: var(--green); background: radial-gradient(circle at 32% 24%, rgba(79, 224, 166, 0.26), rgba(79, 224, 166, 0.06)); border-color: rgba(79, 224, 166, 0.3); box-shadow: inset 0 0 18px rgba(79, 224, 166, 0.1); text-shadow: 0 0 16px rgba(79, 224, 166, 0.3); }

/* Locked, but still readable and still reachable by keyboard: its sub-line names
   the node it is waiting on, which is the one useful thing it has to say. It is
   dimmed and given the default cursor rather than disabled, because a disabled
   button cannot take focus and the keyboard would then walk straight past the
   only tile explaining why the tier is shut. */
.path-node.is-locked { cursor: default; opacity: 0.46; }
.path-node.is-locked:hover { transform: none; background: linear-gradient(158deg, rgba(255, 255, 255, 0.075), rgba(255, 255, 255, 0.028)); border-color: var(--line); box-shadow: none; }
.path-node.is-locked:hover .deck-icon { transform: none; }

.path-too-small { max-width: 66ch; margin: 8px 0 0; color: var(--muted); font-size: 0.8rem; line-height: 1.5; }

/* Narrow windows drop the tree to one node a row. The tiers still read in order
   down the page, which is the part that has to survive. */
@media (max-width: 700px) {
  /* Ten columns on a phone is ten unreadable slivers, so the tree falls back to
     one node a row, the label back over its nodes, and the branches away
     entirely — there is no shape left for them to describe. The tiers still read
     in order down the page, which is the part that has to survive; the spans go
     with the columns they were counted in, or a node three sets wide would ask
     for three of the one column there now is. */
  .path-tree { grid-template-columns: minmax(0, 1fr); }
  .path-tier-label, .path-nodes { grid-column: 1; }
  .path-nodes { grid-template-columns: minmax(0, 1fr); }
  /* The span goes, and the width worked out from it has to go with it, or a
     node four sets wide would ask for a quarter of the one column there now is. */
  .path-node { grid-column: span 1 !important; width: 100% !important; }
  .path-branches { display: none; }
  .path-tier-label { padding-top: 16px; }
}

/* The word maze. It stands where the card stands; the card comes back over the
   top of it the moment a creature is shot, so the thing you answer in is the
   same panel it has always been. */
.maze-stage { position: relative; display: none; flex-direction: column; gap: 10px; align-items: center; width: 100%; }
body.is-wordmaze .maze-stage { display: flex; }
/* Idle in the maze nothing is being asked, so the whole card stands down —
   and a cursor left sitting in a hidden box would swallow the arrow keys that
   are steering you. Holding a creature brings the card back exactly as it is in
   the typing mode, word and all: you cannot answer what you cannot see. */
body.is-wordmaze:not(.is-engaged) .flashcard { display: none; }
/* Holding a creature lifts the maze out of the layout and stands it behind the
   card rather than above it. Left in the flow it pushed the answer boxes off the
   bottom of the window — and the frame does not scroll, so there was no reaching
   them. The card belongs exactly where a card belongs. */
body.is-wordmaze.is-engaged .maze-stage { position: absolute; z-index: 0; inset: 16px; opacity: 0.18; filter: blur(3px); pointer-events: none; }
body.is-wordmaze.is-engaged .maze-stage #maze-canvas { max-height: 100%; object-fit: contain; }
body.is-wordmaze.is-engaged .flashcard { position: relative; z-index: 1; }
/* The word's trip from where it was shot to where it is asked. */
.maze-flyer { position: fixed; z-index: 40; color: #f4f7ff; font-family: var(--jp-serif); font-size: 26px; font-weight: 700; line-height: 1; white-space: nowrap; pointer-events: none; text-shadow: 0 0 22px rgba(255, 209, 102, 0.75); }
.flashcard.is-arriving .glyph { opacity: 0; }
.maze-bar { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; width: 100%; max-width: 100%; }
.maze-hearts { margin: 0; color: var(--green); font-size: 0.72rem; font-weight: 850; letter-spacing: 0.09em; text-transform: uppercase; }
.maze-hearts[data-slow="true"] { color: var(--red); }
.maze-hint { margin: 0; color: var(--muted); font-size: 0.7rem; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; }
/* The canvas is drawn at a size that keeps the glyphs at reading size and only
   ever scales down to fit a narrow window — never up past its own resolution,
   which would soften every character in it. */
#maze-canvas { max-width: 100%; height: auto; background: #0b1120; border: 1px solid var(--line-lit); box-shadow: var(--lift), 0 0 40px rgba(85, 164, 255, 0.12); image-rendering: auto; }

/* ImageGen's mobile control language: one quiet ink-ring pad and one separate
   vermilion action. They float over the playfield so movement never pushes the
   maze below the fold, and each control remains a real keyboard-focusable
   button rather than a gesture-only canvas region. */
.maze-controller {
  position: absolute;
  z-index: 6;
  right: 14px;
  bottom: 14px;
  left: 14px;
  display: none;
  align-items: end;
  justify-content: space-between;
  pointer-events: none;
}
.maze-move-pad,
.maze-fire {
  display: grid;
  flex: 0 0 auto;
  place-items: center;
  margin: 0;
  padding: 0;
  color: #112d50;
  font: inherit;
  -webkit-tap-highlight-color: transparent;
  touch-action: none;
  user-select: none;
  pointer-events: auto;
}
.maze-move-pad {
  position: relative;
  width: 116px;
  height: 116px;
  overflow: visible;
  background: rgba(255, 250, 242, .76);
  border: 2px solid rgba(255, 250, 242, .9);
  border-radius: 50%;
  box-shadow: 0 0 0 2px rgba(17, 45, 80, .68), inset 0 0 0 1px rgba(17, 45, 80, .24), 0 8px 20px rgba(5, 12, 20, .28);
  backdrop-filter: blur(5px);
}
.maze-move-pad::before,
.maze-move-pad::after {
  position: absolute;
  color: rgba(17, 45, 80, .76);
  font-size: 17px;
  line-height: 1;
  pointer-events: none;
}
.maze-move-pad::before { content: "↑  ↓"; inset: 8px 0 auto; word-spacing: 36px; }
.maze-move-pad::after { content: "←  →"; top: 48px; left: 8px; right: 8px; word-spacing: 45px; }
.maze-move-ring { position: absolute; inset: 29px; border: 1px solid rgba(17, 45, 80, .34); border-radius: 50%; pointer-events: none; }
.maze-move-knob {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 42px;
  height: 42px;
  transform: translate(-50%, -50%);
  background: rgba(17, 45, 80, .86);
  border: 2px solid rgba(255, 250, 242, .8);
  border-radius: 50%;
  box-shadow: 0 3px 8px rgba(5, 12, 20, .28);
  transition: transform 70ms linear;
}
.maze-move-pad.is-active .maze-move-knob { transition: none; }
.maze-move-pad small,
.maze-fire small {
  position: absolute;
  bottom: -18px;
  color: rgba(255, 250, 242, .9);
  font-size: 9px;
  font-weight: 900;
  letter-spacing: .16em;
  line-height: 1;
  text-shadow: 0 1px 3px rgba(5, 12, 20, .8);
  text-transform: uppercase;
}
.maze-fire {
  position: relative;
  width: 82px;
  height: 82px;
  color: #fffaf2;
  background: rgba(237, 85, 77, .92);
  border: 2px solid rgba(255, 250, 242, .9);
  border-radius: 50%;
  box-shadow: 0 0 0 2px rgba(206, 65, 60, .78), 0 8px 20px rgba(5, 12, 20, .3);
  backdrop-filter: blur(5px);
}
.maze-fire span { font: 800 2rem/1 var(--jp-serif); }
.maze-fire:active { transform: scale(.94); }

@media (max-width: 900px), (pointer: coarse) {
  body.is-wordmaze:not(.is-engaged) .maze-controller { display: flex; }
}

@media (max-width: 420px) {
  .maze-controller { right: 10px; bottom: 11px; left: 10px; }
  .maze-move-pad { width: 104px; height: 104px; }
  .maze-move-ring { inset: 27px; }
  .maze-fire { width: 74px; height: 74px; }
  .maze-fire span { font-size: 1.75rem; }
}

/* The grid. A sheet of words, each with its own boxes under it. Laid out as a
   wrapping row so ten short words sit on one line and four long ones do not. */
.grid-stage { display: none; flex-direction: column; gap: 16px; align-items: center; width: 100%; }
body.is-grid .grid-stage { display: flex; }
body.is-grid .flashcard { display: none; }
.grid-bar { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; width: 100%; }
.grid-progress { margin: 0; color: var(--green); font-size: 0.72rem; font-weight: 850; letter-spacing: 0.07em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
.grid-hint { margin: 0; color: var(--muted); font-size: 0.68rem; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; }
.grid-cards { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; width: 100%; }
.grid-card { display: flex; flex: 0 0 auto; flex-direction: column; gap: 8px; align-items: center; min-width: 96px; padding: 12px 12px 14px; background: linear-gradient(180deg, rgba(85, 164, 255, 0.16), rgba(85, 164, 255, 0.06)); border: 1px solid rgba(85, 164, 255, 0.4); transition: border-color 140ms ease, background 140ms ease; }
.grid-card.is-long { flex: 1 1 320px; max-width: 520px; }
.grid-card:focus-within { border-color: var(--line-lit); background: linear-gradient(180deg, rgba(85, 164, 255, 0.26), rgba(85, 164, 255, 0.1)); box-shadow: 0 0 0 1px var(--line-lit); }
/* Big enough to read without leaning in — the whole point of the sheet is that
   you recognise the word on sight. */
.grid-face { margin: 0; color: #f4f7ff; font-family: var(--jp-serif); font-size: 2.1rem; line-height: 1.15; text-align: center; }
.grid-card.is-long .grid-face { font-size: 1.15rem; }
.grid-box { display: flex; flex-direction: column; gap: 3px; width: 100%; }
.grid-box small { color: rgba(255, 255, 255, 0.6); font-size: 0.55rem; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; }
.grid-box input { width: 100%; min-width: 72px; padding: 7px 8px; color: var(--ink); background: rgba(5, 9, 17, 0.72); border: 1px solid var(--line); font: inherit; font-size: 0.9rem; }
.grid-box input:focus-visible { outline: none; border-color: var(--line-lit); box-shadow: 0 0 0 1px var(--line-lit); }
.grid-box input:disabled { opacity: 0.85; }
/* Marked on the way out of the box, so you know where you stand without having
   been shown the answer. */
.grid-box.is-right input { color: var(--green); border-color: rgba(79, 224, 166, 0.6); background: rgba(79, 224, 166, 0.1); }
.grid-box.is-wrong input { color: var(--red); border-color: rgba(255, 130, 114, 0.6); background: rgba(255, 130, 114, 0.1); }
/* Nothing is revealed until the sheet is finished, so the whole thing is one
   recall rather than a series of them with the answer handed over between. */
.grid-answer { color: var(--green); font-family: var(--jp-serif); font-size: 0.86rem; }
.grid-card.is-right { border-color: rgba(79, 224, 166, 0.55); background: linear-gradient(180deg, rgba(79, 224, 166, 0.16), rgba(79, 224, 166, 0.05)); }
.grid-card.is-wrong { border-color: rgba(255, 130, 114, 0.55); background: linear-gradient(180deg, rgba(255, 130, 114, 0.16), rgba(255, 130, 114, 0.05)); }
.grid-card.is-wrong .grid-answer { color: var(--red); }
.grid-finish { padding: 12px 30px; cursor: pointer; color: #04211a; background: linear-gradient(180deg, #6df0bd, var(--green)); border: 1px solid rgba(79, 224, 166, 0.7); box-shadow: 0 0 28px rgba(79, 224, 166, 0.3); font: inherit; font-size: 0.78rem; font-weight: 900; letter-spacing: 0.09em; text-transform: uppercase; }
.grid-finish:hover, .grid-finish:focus-visible { transform: translateY(-1px); outline: none; }

/* Snap. Two piles side by side, laid turn about: the computer's questions on
   the left and your answers on the right. The pile the last card landed on is
   the lit one, so whose turn it was is read off the table rather than said. */
/* The verdict on the last answer, over the middle of the screen. It takes no
   clicks and blocks nothing: the game underneath stays live throughout, so a
   fast player never waits on it and a slow one gets to read it. Fixed to the
   viewport rather than to the card, because the six games put their playing
   surface in six different places and the point of this is that it is always in
   the same one. */
/* Over the middle of the surface being played — the snap table, the flashcard,
   the room — rather than the middle of the window. Top and left are written by
   showVerdictFlash from that surface's own measurements; what is here is the
   centring on whatever point it is given. It lands over the thing it is about
   for the second it is up, which is the point: it is where your eye already is,
   so it is read without being looked for. */
.verdict-flash { position: fixed; z-index: 60; top: 50%; left: 50%; display: flex; flex-direction: column; align-items: center; transform: translate(-50%, -50%); text-align: center; pointer-events: none; }
.verdict-flash[hidden] { display: none; }
.verdict-word { margin: 0; font-size: clamp(2.2rem, 7vw, 4rem); font-weight: 900; letter-spacing: 0.04em; line-height: 1; text-transform: uppercase; }
/* What it should have been, under the word and only when it was not right, named
   as well as given: "Meaning: leave" rather than a bare "leave", which on a card
   with three boxes does not even say which question it answers.
   On its own dark plate, because it lands over whatever is being played and a
   line of text laid straight onto a kanji is a line nobody can read. The heading
   is small, spaced and quiet; the answer is the thing you are meant to take
   away, so it is set large and given the line to itself. */
.verdict-answer { display: flex; flex-direction: column; align-items: center; gap: 3px; max-width: min(90vw, 26rem); margin: 14px 0 0; padding: 9px 20px 11px; background: rgba(5, 9, 17, 0.88); border: 1px solid rgba(148, 198, 255, 0.34); border-top: 2px solid rgba(148, 198, 255, 0.5); box-shadow: 0 10px 26px rgba(0, 0, 0, 0.55); overflow-wrap: anywhere; }
.verdict-answer[hidden] { display: none; }
.verdict-answer-label { color: rgba(185, 214, 255, 0.82); font-size: clamp(0.58rem, 1.5vw, 0.68rem); font-weight: 850; letter-spacing: 0.14em; text-transform: uppercase; }
.verdict-answer-label:empty { display: none; }
.verdict-answer-value { color: #f4f7ff; font-size: clamp(1.15rem, 3.4vw, 1.75rem); font-weight: 800; line-height: 1.25; }
.verdict-answer-value[lang="ja"] { font-family: var(--jp-serif); font-size: clamp(1.35rem, 4vw, 2rem); }
.verdict-flash[data-tone="right"] .verdict-word { color: var(--green); text-shadow: 0 0 26px rgba(79, 224, 166, 0.65), 0 4px 14px #000; }
.verdict-flash[data-tone="near"] .verdict-word { color: var(--star); text-shadow: 0 0 26px rgba(255, 209, 102, 0.6), 0 4px 14px #000; }
.verdict-flash[data-tone="wrong"] .verdict-word { color: var(--red); text-shadow: 0 0 26px rgba(255, 130, 114, 0.6), 0 4px 14px #000; }
/* In and out in one movement: a quick punch out from nothing, a beat held at
   rest, then gone. Both stay dead centre the whole time — nothing drifts off the
   middle — and what separates them is the movement: right swells and settles,
   wrong shakes where it stands, so the two are not the same event glanced at
   sideways. */
/* The timings are matched to the two the script holds the flash for: 520ms for a
   right answer, which only has to be seen and is gone before the next card
   lands, and 2200ms for anything with a correction under it, which has to be
   read. An animation shorter than its hold would leave the verdict sitting there
   invisible. */
.verdict-flash.is-shown { animation: verdict-right 520ms cubic-bezier(0.2, 0.9, 0.3, 1) both; }
.verdict-flash[data-tone="near"].is-shown { animation: verdict-held 2200ms cubic-bezier(0.2, 0.9, 0.3, 1) both; }
.verdict-flash[data-tone="wrong"].is-shown { animation: verdict-wrong 2200ms cubic-bezier(0.2, 0.9, 0.3, 1) both; }
@keyframes verdict-held {
  0% { transform: translate(-50%, -50%) scale(0.55); opacity: 0; }
  7% { transform: translate(-50%, -50%) scale(1.14); opacity: 1; }
  13% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  88% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1.06); opacity: 0; }
}
/* Punched out and away again inside half a second: in by 90ms, settled by 150,
   and fading from two thirds of the way through. At this length the hold is the
   shortest part of it, which is the point — it is a stamp on the answer rather
   than a screen to be got past. */
@keyframes verdict-right {
  0% { transform: translate(-50%, -50%) scale(0.5); opacity: 0; }
  17% { transform: translate(-50%, -50%) scale(1.16); opacity: 1; }
  29% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  62% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(1.12); opacity: 0; }
}
@keyframes verdict-wrong {
  0% { transform: translate(-50%, -50%) scale(0.6); opacity: 0; }
  6% { transform: translate(-50%, -50%) scale(1.12); opacity: 1; }
  10% { transform: translate(-53%, -50%) scale(1.04); }
  14% { transform: translate(-47%, -50%) scale(1.02); }
  18% { transform: translate(-51%, -50%) scale(1); }
  22% { transform: translate(-50%, -50%) scale(1); }
  88% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
  100% { transform: translate(-50%, -50%) scale(0.96); opacity: 0; }
}
@media (prefers-reduced-motion: reduce) {
  .verdict-flash.is-shown { animation: verdict-still 520ms linear both; }
  .verdict-flash[data-tone="near"].is-shown,
  .verdict-flash[data-tone="wrong"].is-shown { animation: verdict-still 2200ms linear both; }
  @keyframes verdict-still {
    0%, 88% { opacity: 1; transform: translate(-50%, -50%); }
    100% { opacity: 0; transform: translate(-50%, -50%); }
  }
}

/* Flashcard. One card, turned over by a real rotation rather than a fade: what
   the game is called is what it does, and a card that dissolves into its answer
   is not a card being turned over. */
.flash-stage { display: none; flex-direction: column; gap: 18px; align-items: center; width: 100%; }
body.is-flashcard .flash-stage { display: flex; }
body.is-flashcard .flashcard { display: none; }
.flash-bar { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; width: 100%; }
.flash-progress { margin: 0; color: var(--green); font-size: 0.72rem; font-weight: 850; letter-spacing: 0.07em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
.flash-hint { margin: 0; color: var(--muted); font-size: 0.68rem; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; }
/* One face, opened a box at a time. The card was a 3D flip; a flip reveals
   everything at once, which is the one thing this must not do. */
.flash-card { position: relative; width: min(100%, 340px); min-height: 344px; margin-top: 10px; transition: border-color 220ms ease, background 220ms ease; background: linear-gradient(160deg, rgba(85, 164, 255, 0.16), rgba(85, 164, 255, 0.05)); border: 1px solid rgba(85, 164, 255, 0.42); box-shadow: var(--lift); }
/* Every box open: the card takes the settled colour the back used to have, so
   there is still something to see at a glance that says it is finished with. */
.flash-card.is-turned { background: linear-gradient(160deg, rgba(79, 224, 166, 0.14), rgba(79, 224, 166, 0.04)); border-color: rgba(79, 224, 166, 0.42); }
.flash-face { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 10px; min-height: 344px; padding: 26px 22px; text-align: center; }
.flash-card.has-mnemonic .flash-face { min-height: 430px; }
.flash-glyph { margin: 0; color: #f4f7ff; font-family: var(--jp-serif); font-size: 4.2rem; line-height: 1.05; }
.flash-card.is-long .flash-glyph { font-size: 1.3rem; line-height: 1.55; }
.flash-field { color: rgba(255, 255, 255, 0.66); font-size: 0.62rem; font-weight: 850; letter-spacing: 0.13em; text-transform: uppercase; }
/* The back carries every box, so the answer sizes itself to how many there are:
   one box gets the full display size it always had, and a five-box word card
   steps down rather than running off the card. */
.flash-answers { display: flex; flex-direction: column; align-items: center; gap: 10px; width: 100%; min-height: 0; overflow-y: auto; scrollbar-width: thin; scrollbar-color: rgba(255, 255, 255, 0.22) transparent; }
.flash-answer-row { display: flex; flex-direction: column; align-items: center; gap: 2px; width: 100%; }
.flash-answer { color: #f4f7ff; font-size: 2rem; font-weight: 700; line-height: 1.25; overflow-wrap: anywhere; }
.flash-answer[lang="ja"] { font-family: var(--jp-serif); font-size: 2.3rem; }
.flash-answers:has(.flash-answer-row:nth-child(2)) .flash-answer { font-size: 1.3rem; }
.flash-answers:has(.flash-answer-row:nth-child(2)) .flash-answer[lang="ja"] { font-size: 1.5rem; }
.flash-answers:has(.flash-answer-row:nth-child(4)) .flash-answer { font-size: 1.05rem; }
.flash-answers:has(.flash-answer-row:nth-child(4)) .flash-answer[lang="ja"] { font-size: 1.2rem; }
.flash-insitu { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 6px; margin-top: 10px; }
/* Sized against .kanji-svg, which is 1em and sits later in this file — inside a
   caption's font size that comes out at about 20px, far too small to see which
   strokes are picked out. */
.flash-insitu > .flash-insitu-svg { width: 92px; height: 92px; filter: none; }
.flash-family { flex: 0 0 auto; display: flex; flex-direction: column; align-items: center; gap: 4px; margin-top: 10px; width: 100%; }
.flash-family .radical-forms { justify-content: center; margin-top: 0; }
.flash-family .form-example { padding: 4px 7px; }
.flash-family .form-in { font-size: 0.62rem; }
.flash-listen { flex: 0 0 auto; margin-top: 8px; padding: 5px 12px; color: rgba(255, 255, 255, 0.82); font-size: 0.72rem; font-weight: 700; letter-spacing: 0.04em; background: rgba(255, 255, 255, 0.08); border: 1px solid rgba(255, 255, 255, 0.22); border-radius: 999px; cursor: pointer; }
.flash-listen:hover { background: rgba(255, 255, 255, 0.16); }
/* The picture is drawn on paper, so it keeps its own light ground rather than
   being tinted to match the card; the frame is what ties it to the face. The
   card is given room for it only when there is one, which keeps the flip the
   same size as before on every card that has none. */
/* An unopened box keeps its label and shows a placeholder, so the card is the
   same height throughout and opening one does not shove the rest around. */
.flash-answer-row:not(.is-open) .flash-answer { color: rgba(255, 255, 255, 0.3); letter-spacing: 0.22em; }
/* A kanji's card set can run to a dozen rows or more — 日 is fourteen — and one
   column of them runs off the card. Past six they sit side by side. */
.flash-answers:has(.flash-answer-row:nth-child(7)) { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); align-items: start; gap: 8px 8px; }
/* A picture row is the picture; a sound row is a button to hear it. */
.flash-picture { display: block; width: 80px; max-width: 100%; aspect-ratio: 1; background-color: #fffaf2; background-repeat: no-repeat; border-radius: 8px; box-shadow: 0 3px 10px rgba(0, 0, 0, 0.18); }
.flash-sound, .snap-sound { display: inline-grid; place-items: center; padding: 0; color: var(--ink, #17130d); background: #fffaf2; border: 2px solid rgba(23, 19, 13, 0.4); border-radius: 50%; cursor: pointer; }
.flash-sound { width: 46px; height: 46px; }
.flash-sound svg { width: 24px; height: 24px; fill: currentColor; }
.flash-sound:hover, .snap-sound:hover { border-color: rgba(23, 19, 13, 0.75); }
.flash-peek { flex: 0 0 auto; padding: 5px 12px; color: rgba(255, 255, 255, 0.72); font-size: 0.68rem; font-weight: 700; letter-spacing: 0.06em; text-transform: uppercase; background: transparent; border: 1px dashed rgba(255, 255, 255, 0.28); border-radius: 999px; cursor: pointer; }
.flash-peek:hover { color: #f4f7ff; border-color: rgba(255, 255, 255, 0.5); }
/* One cell of a sprite atlas rather than its own file: 2,317 mnemonic pictures
   were 2,317 of the deployment's files, and they pack into 37. background-size
   is the whole sheet scaled so that one cell fills the box, and the position is
   set per card in app.js. */
.flash-mnemonic { flex: 0 1 auto; height: 158px; min-height: 96px; max-height: 158px; width: auto; max-width: 62%; aspect-ratio: 1; margin-top: 4px; background-color: #fbfbf7; background-repeat: no-repeat; border: 1px solid rgba(255, 255, 255, 0.22); border-radius: 10px; box-shadow: 0 6px 18px rgba(0, 0, 0, 0.28); }
.flash-mnemonic[hidden] { display: none; }
.flash-mnemonic.is-radical-texture { position: relative; display: block; overflow: hidden; background: #fffaf2; }
.flash-mnemonic.is-radical-texture[hidden] { display: none; }
.flash-radical-texture-scene { position: absolute; inset: 0; background-repeat: no-repeat; }
.flash-radical-texture-glyph { position: absolute; right: 6px; bottom: 6px; display: grid; place-items: center; min-width: 32px; height: 32px; padding: 0 3px; color: #1b2f44; background: rgba(255, 250, 242, 0.94); border: 1px solid rgba(41, 63, 90, 0.28); border-radius: 5px; font-family: var(--jp-serif); font-size: 23px; font-weight: 700; line-height: 1; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.1); }
body.is-playing .flash-radical-texture-glyph { min-width: 26px; height: 26px; font-size: 19px; }
.flash-mnemonic.is-joyo-art { display: grid; flex: 0 0 auto; grid-template-columns: repeat(var(--art-count), minmax(0, 1fr)); gap: 6px; width: min(100%, 306px); max-width: 100%; height: auto; min-height: 0; max-height: none; aspect-ratio: auto; background: none; border: 0; border-radius: 0; box-shadow: none; }
.flash-mnemonic.is-joyo-art[hidden] { display: none; }
.flash-mnemonic.is-joyo-art.art-count-1 { width: 158px; }
.flash-mnemonic.is-joyo-art.art-count-2 { width: 250px; }
.flash-mnemonic-meaning { min-width: 0; margin: 0; }
.flash-mnemonic-picture { display: block; width: 100%; aspect-ratio: 1; background-color: #fffaf2; background-repeat: no-repeat; border-radius: 8px; }
.flash-mnemonic-meaning figcaption { margin-top: 3px; color: rgba(255, 255, 255, 0.86); font-size: 0.67rem; font-weight: 700; line-height: 1.2; text-align: center; }
.flash-answer-row.is-history { box-sizing: border-box; grid-column: 1 / -1; align-items: flex-start; width: min(100%, 430px); margin: 6px 0 0; padding: 12px 15px; color: var(--ink); background: #f6f0df; border: 1px solid rgba(17,45,80,.18); border-radius: 10px; font-size: .82rem; line-height: 1.45; text-align: left; }
.flash-answer-row.is-history .flash-field { color: var(--ink); }
.flash-answer-row.is-history p { margin: 4px 0; }
.flash-answer-row.is-history a { color: var(--blue); text-decoration: underline; text-underline-offset: 2px; }
body.is-playing .flash-mnemonic.is-joyo-art { height: auto; min-height: 0; max-height: none; }
body.is-playing .flash-mnemonic.is-joyo-art.art-count-1 { width: 110px; }
body.is-playing .flash-mnemonic.is-joyo-art.art-count-2 { width: 210px; }
.flash-card.has-mnemonic { min-height: 430px; }
.flash-controls { display: flex; gap: 12px; align-items: center; min-height: 52px; }
.flash-marks { display: flex; gap: 10px; }
.flash-marks[hidden] { display: none; }
.flash-turn { padding: 14px 40px; cursor: pointer; color: #2b0f05; background: linear-gradient(180deg, #ffc17a, var(--orange)); border: 1px solid rgba(255, 161, 77, 0.7); font: inherit; font-size: 0.86rem; font-weight: 900; letter-spacing: 0.11em; text-transform: uppercase; }
.flash-turn[hidden] { display: none; }
/* The study card's one button. Green rather than the orange of "show me the
   next box", because it is not revealing anything — it is agreeing that you are
   done reading and putting the word into play. */
.flash-deal { color: #04211a; background: linear-gradient(180deg, #7df0c4, var(--green)); border-color: rgba(79, 224, 166, 0.7); }
/* Three ways to mark it, coloured by what they mean rather than all alike: the
   middle one is not a lesser green, it is its own answer. */
.flash-mark { padding: 13px 20px; cursor: pointer; color: var(--ink); background: var(--panel); border: 1px solid var(--line); font: inherit; font-size: 0.74rem; font-weight: 850; letter-spacing: 0.07em; text-transform: uppercase; }
.flash-mark.is-had { color: var(--green); border-color: rgba(79, 224, 166, 0.55); }
.flash-mark.is-nearly { color: var(--star); border-color: rgba(255, 209, 102, 0.5); }
.flash-mark.is-missed { color: var(--red); border-color: rgba(255, 130, 114, 0.5); }
.flash-turn:hover, .flash-turn:focus-visible, .flash-mark:hover, .flash-mark:focus-visible { transform: translateY(-1px); border-color: var(--line-lit); outline: none; }
@media (prefers-reduced-motion: reduce) { .flash-card { transition: none; } }

.snap-stage { display: none; flex-direction: column; gap: 16px; align-items: center; width: 100%; }
body.is-snap .snap-stage { display: flex; }
body.is-snap .flashcard { display: none; }
.snap-bar { display: flex; align-items: center; justify-content: space-between; gap: 14px; flex-wrap: wrap; width: 100%; }
.snap-progress { margin: 0; color: var(--green); font-size: 0.72rem; font-weight: 850; letter-spacing: 0.07em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
.snap-hint { margin: 0; color: var(--muted); font-size: 0.68rem; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; }
/* A table rather than a page. Green felt inside a wooden rail, with the dealer's
   arc printed on it, and the two things standing on it are playing cards: ivory,
   pipped in the corners, and sitting at the small angle a dealt card lands at.
   Everything here is drawn rather than fetched, on the same terms as the range's
   surfaces — the one downloaded picture in the whole game is the card back on
   the shoe, and the suits are characters. */
.snap-table { position: relative; display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 26px; width: 100%; max-width: 720px; margin-top: 22px; padding: 30px 26px 24px; background: radial-gradient(120% 96% at 50% 6%, #1d7350 0%, #14563b 44%, #0c3826 100%); border: 9px solid #3a2418; border-radius: 26px; box-shadow: inset 0 0 70px rgba(0, 0, 0, 0.5), inset 0 2px 0 rgba(255, 255, 255, 0.07), 0 20px 44px rgba(0, 0, 0, 0.55); }
/* The arc a dealer's table has printed on it, and the felt's weave. Both are one
   pseudo-element so the table costs no extra markup. */
.snap-table::before { position: absolute; content: ""; inset: 10px; border-radius: 18px; border-top: 2px dashed rgba(255, 255, 255, 0.13); background-image: repeating-linear-gradient(45deg, rgba(255, 255, 255, 0.022) 0 2px, transparent 2px 4px), repeating-linear-gradient(-45deg, rgba(0, 0, 0, 0.03) 0 2px, transparent 2px 4px); pointer-events: none; }
/* The shoe, straddling the rail at the head of the table. Kenney's card back,
   with the shoulders of the cards under it drawn as shadows. */
.snap-deck { position: absolute; top: -20px; left: 50%; z-index: 2; width: 46px; height: 64px; transform: translateX(-50%) rotate(-4deg); background: #fdfbf5 url("assets/snap/card_back.png") center / 100% 100% no-repeat; border-radius: 5px; box-shadow: 3px 3px 0 -1px #e8e2d4, 3px 3px 0 0 rgba(20, 16, 12, 0.5), 6px 6px 0 -1px #e8e2d4, 6px 6px 0 0 rgba(20, 16, 12, 0.45), 0 10px 18px rgba(0, 0, 0, 0.5); }
.snap-deck[hidden] { display: none; }
.snap-pile { display: flex; flex-direction: column; gap: 8px; align-items: center; min-width: 0; }
.snap-pile-name { margin: 0; color: rgba(232, 226, 212, 0.62); font-size: 0.6rem; font-weight: 850; letter-spacing: 0.11em; text-transform: uppercase; }
.snap-pile-count { margin: 6px 0 0; color: rgba(232, 226, 212, 0.6); font-size: 0.6rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
/* An actual card: ivory stock, a hairline edge, the white margin a printed card
   has inside its border, and a shadow it casts onto the felt. */
.snap-card { position: relative; display: grid; place-content: center; gap: 6px; width: min(100%, 244px); min-height: 268px; padding: 26px 18px; text-align: center; color: #17130d; background: linear-gradient(158deg, #fffdf6 0%, #f6f1e4 62%, #ece5d4 100%); border: 1px solid rgba(23, 19, 13, 0.42); border-radius: 11px; box-shadow: inset 0 0 0 5px rgba(255, 255, 255, 0.75), inset 0 0 0 6px rgba(23, 19, 13, 0.11), 0 14px 26px rgba(0, 0, 0, 0.45); transform: rotate(-1.1deg); transition: border-color 140ms ease, box-shadow 140ms ease, transform 140ms ease; }
#snap-pile-answer .snap-card { transform: rotate(1.3deg); }
/* The corner index, twice, the second one upside down where the other corner of
   a real card carries it. The suit is a property so each side keeps its own. */
.snap-card::before, .snap-card::after { position: absolute; content: var(--pip, "\2660"); color: var(--pip-ink, #17130d); font-size: 0.95rem; line-height: 1; }
.snap-card::before { top: 11px; left: 13px; }
.snap-card::after { right: 13px; bottom: 11px; transform: rotate(180deg); }
#snap-face { --pip: "\2660"; }
#snap-answer { --pip: "\2665"; --pip-ink: #b3261e; }
.snap-pile.is-fresh .snap-card { box-shadow: inset 0 0 0 5px rgba(255, 255, 255, 0.75), inset 0 0 0 6px rgba(23, 19, 13, 0.11), 0 16px 30px rgba(0, 0, 0, 0.5), 0 0 0 2px rgba(255, 209, 102, 0.55); }
/* The place a card will land, before there is one. Printed on the felt rather
   than standing on it: a blank card is a card that has been dealt and says
   nothing, which is a different thing from not having asked for one yet. */
.snap-card.is-empty { background: rgba(0, 0, 0, 0.16); border: 2px dashed rgba(255, 255, 255, 0.22); box-shadow: inset 0 0 24px rgba(0, 0, 0, 0.25); }
.snap-card.is-empty::before, .snap-card.is-empty::after { content: none; }
/* Dealt: off the shoe at the head of the table and down onto the felt, landing
   with the small overshoot a card has when it is pushed rather than placed. The
   two sides come from the same place and so lean opposite ways, and each ends on
   exactly the angle its resting rule gives it. */
@keyframes snap-deal-face {
  0% { transform: translate(46%, -104%) rotate(15deg) scale(0.84); opacity: 0; }
  14% { opacity: 1; }
  72% { transform: translate(0, 0) rotate(-3.4deg) scale(1.015); }
  100% { transform: translate(0, 0) rotate(-1.1deg) scale(1); }
}
@keyframes snap-deal-answer {
  0% { transform: translate(-46%, -104%) rotate(-15deg) scale(0.84); opacity: 0; }
  14% { opacity: 1; }
  72% { transform: translate(0, 0) rotate(3.6deg) scale(1.015); }
  100% { transform: translate(0, 0) rotate(1.3deg) scale(1); }
}
#snap-face.is-dealt { animation: snap-deal-face 320ms cubic-bezier(0.19, 0.86, 0.32, 1) both; }
#snap-answer.is-dealt { animation: snap-deal-answer 300ms cubic-bezier(0.19, 0.86, 0.32, 1) both; }
/* Collected: swept back off the felt towards the shoe they came from. After the
   flight rules, so a card being taken away beats a card being dealt. */
@keyframes snap-collect-face {
  0% { transform: translate(0, 0) rotate(-1.1deg) scale(1); opacity: 1; }
  100% { transform: translate(52%, -110%) rotate(17deg) scale(0.78); opacity: 0; }
}
@keyframes snap-collect-answer {
  0% { transform: translate(0, 0) rotate(1.3deg) scale(1); opacity: 1; }
  100% { transform: translate(-52%, -110%) rotate(-17deg) scale(0.78); opacity: 0; }
}
#snap-face.is-collected { animation: snap-collect-face 280ms cubic-bezier(0.45, 0, 0.85, 0.4) both; }
#snap-answer.is-collected { animation: snap-collect-answer 280ms cubic-bezier(0.45, 0, 0.85, 0.4) both; }
/* The shoe gives a little as a card comes off it. */
.snap-table:has(.is-dealt) .snap-deck { animation: snap-shoe 300ms ease-out; }
@keyframes snap-shoe {
  0% { transform: translateX(-50%) rotate(-4deg) translateY(0); }
  35% { transform: translateX(-50%) rotate(-6deg) translateY(2px); }
  100% { transform: translateX(-50%) rotate(-4deg) translateY(0); }
}
@media (prefers-reduced-motion: reduce) {
  #snap-face.is-dealt, #snap-answer.is-dealt, .snap-table:has(.is-dealt) .snap-deck { animation: none; }
  #snap-face.is-collected, #snap-answer.is-collected { animation: none; opacity: 0; }
}
.snap-glyph { margin: 0; color: #17130d; font-family: var(--jp-serif); font-size: 3.4rem; line-height: 1.1; }
/* A sentence card's face is a sentence, and shrinking it to fit the tile a kanji
   sits in makes a card nobody can read. */
.snap-card.is-long .snap-glyph { font-size: 1.2rem; line-height: 1.55; }
.snap-field { color: rgba(23, 19, 13, 0.62); font-size: 0.58rem; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; }
/* On the word's own card the box name is the question being asked of it — every
   card in a shoe answers this one line — so it is set in the ink a card's suit
   is printed in rather than left grey. */
#snap-face .snap-field { margin-top: 8px; color: #a8321f; font-size: 0.66rem; letter-spacing: 0.13em; }
/* Read at a glance and against a glyph three times its size, so it is set large
   for an English word — the two sides of the table are one comparison. */
.snap-value { color: #17130d; font-size: 1.9rem; font-weight: 700; line-height: 1.25; overflow-wrap: anywhere; }
.snap-value[lang="ja"] { font-family: var(--jp-serif); font-size: 2.2rem; }
/* A picture dealt as an answer is the whole face of the card, and a sound is a
   button on it to hear it again — it plays once as it lands. */
#snap-answer.is-picture { padding: 16px; }
.snap-picture { display: block; width: 196px; max-width: 100%; aspect-ratio: 1; background-color: #fffaf2; background-repeat: no-repeat; border-radius: 8px; }
.snap-sound { width: 92px; height: 92px; color: #17130d; }
.snap-sound svg { width: 50px; height: 50px; fill: currentColor; }
.snap-verdict { min-height: 1.2em; margin: 0; color: var(--muted); font-size: 0.8rem; font-weight: 700; text-align: center; }
/* A finished round is marked on the card's edge and in a wash across its face.
   Printed stock does not change colour, so the tint stays light enough that what
   is written on the card is still the thing you are looking at. */
.snap-stage[data-outcome="won"] .snap-card { border-color: rgba(20, 120, 80, 0.7); background: linear-gradient(158deg, #f4fff8 0%, #e2f6ea 62%, #d2ecdd 100%); }
.snap-stage[data-outcome="won"] .snap-verdict { color: var(--green); }
/* A box taken with more of the word still to come — marked on the card that was
   dealt, because the word has not been won yet. */
.snap-stage[data-outcome="box"] #snap-answer { border-color: rgba(20, 120, 80, 0.7); background: linear-gradient(158deg, #f4fff8 0%, #e2f6ea 62%, #d2ecdd 100%); }
.snap-stage[data-outcome="box"] .snap-verdict { color: var(--green); }
.snap-stage[data-outcome="bust"] .snap-card,
.snap-stage[data-outcome="false"] .snap-card { border-color: rgba(168, 50, 31, 0.72); background: linear-gradient(158deg, #fff5f3 0%, #f8e3df 62%, #efd2cc 100%); }
.snap-stage[data-outcome="bust"] .snap-verdict,
.snap-stage[data-outcome="false"] .snap-verdict { color: var(--red); }
.snap-controls { display: flex; gap: 14px; align-items: center; }
/* Chips rather than buttons: rounded, edged, and with the dashed ring a casino
   chip has printed round its rim. Snap is the call you make and Hit is the one
   you fall back on, so they are not a pair of equals — the caller is the red
   chip and the other is a plaque off the same felt as the table. */
.snap-call, .snap-deal { position: relative; cursor: pointer; border-radius: 999px; font: inherit; text-transform: uppercase; transition: transform 120ms ease, box-shadow 120ms ease, filter 120ms ease; }
.snap-call::after, .snap-deal::after { position: absolute; content: ""; inset: 5px; border: 1px dashed rgba(255, 255, 255, 0.4); border-radius: 999px; pointer-events: none; }
.snap-call { padding: 15px 46px; color: #2b0f05; background: linear-gradient(180deg, #ffc17a, var(--orange)); border: 3px solid #7d2f14; box-shadow: 0 6px 0 -1px #7d2f14, 0 12px 22px rgba(0, 0, 0, 0.5), 0 0 30px rgba(255, 161, 77, 0.28); font-size: 0.9rem; font-weight: 900; letter-spacing: 0.12em; }
.snap-deal { padding: 15px 30px; color: #eaf3ec; background: linear-gradient(180deg, #1d7350, #12523a); border: 3px solid #0b3323; box-shadow: 0 6px 0 -1px #0b3323, 0 12px 22px rgba(0, 0, 0, 0.5); font-size: 0.78rem; font-weight: 850; letter-spacing: 0.09em; }
.snap-call:hover, .snap-call:focus-visible, .snap-deal:hover, .snap-deal:focus-visible { transform: translateY(-2px); filter: brightness(1.08); outline: none; }
.snap-call:active, .snap-deal:active { transform: translateY(3px); box-shadow: 0 2px 0 -1px rgba(0, 0, 0, 0.6), 0 6px 12px rgba(0, 0, 0, 0.45); }
@media (max-width: 620px) {
  .snap-table { grid-template-columns: minmax(0, 1fr); }
  .snap-card { min-height: 120px; }
  .snap-glyph { font-size: 2.6rem; }
}

/* The range is the viewport, not a card inside it. The ordinary session HUD is
   reduced to its pause button and floats over the canvas; all range information
   lives in the same frame as the action. */
.shooter-stage { display: none; width: 100%; }
body.is-shooter .shooter-stage { display: flex; flex: 1 1 auto; min-height: 0; }
body.is-shooter .flashcard { display: none; }
body.is-playing.is-shooter .shell { width: 100%; max-width: none; height: 100dvh; padding: 0; }
body.is-shooter .game { min-height: 0; padding: 0; overflow: hidden; background: #070b12; border: 0; box-shadow: none; }
body.is-shooter .hud { position: absolute; z-index: 24; top: 12px; right: 12px; width: auto; margin: 0; padding: 0; background: none; border: 0; }
body.is-shooter .hud-left,
body.is-shooter .hud-mid,
body.is-shooter .hud-right > :not(.hud-menu) { display: none; }
body.is-shooter .hud-right { display: block; flex: 0 0 auto; }
body.is-shooter .hud-menu-button { background: rgba(5, 9, 17, 0.82); border-color: rgba(148, 198, 255, 0.42); box-shadow: 0 0 18px rgba(85, 164, 255, 0.18); }
.shooter-view { --shooter-sky-x: 0px; --shooter-sky-y: 0px; position: relative; width: 100%; min-height: 0; flex: 1 1 auto; overflow: hidden; background-color: #26394b; background-image: url("assets/shooter/overcast-sky.png"); background-repeat: repeat-x; background-position: calc(50% + var(--shooter-sky-x)) calc(48% + var(--shooter-sky-y)); background-size: auto 118%; }
.shooter-bar { position: absolute; z-index: 6; top: 12px; right: 58px; left: 12px; display: flex; width: auto; align-items: center; justify-content: space-between; gap: 14px; padding: 8px 11px; pointer-events: none; background: linear-gradient(90deg, rgba(5, 9, 17, 0.84), rgba(5, 9, 17, 0.38), transparent); border-left: 2px solid rgba(79, 224, 166, 0.72); text-shadow: 0 2px 4px #000; }
.shooter-room { margin: 0; color: var(--green); font-size: 0.86rem; font-weight: 850; letter-spacing: 0.06em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
.shooter-pace { margin: 0; color: var(--green); font-size: 0.86rem; font-weight: 850; letter-spacing: 0.07em; text-transform: uppercase; }
.shooter-pace[data-slow="true"] { color: var(--red); }
#shooter-canvas { display: block; width: 100%; height: 100%; background: transparent; border: 0; cursor: crosshair; }
/* Thin, and never over the middle of the plaque you are trying to read. */
.shooter-cross { position: absolute; top: 50%; left: 50%; width: 18px; height: 18px; transform: translate(-50%, -50%); pointer-events: none; }
.shooter-cross::before, .shooter-cross::after { content: ""; position: absolute; background: rgba(255, 209, 102, 0.85); }
.shooter-cross::before { top: 0; left: 8px; width: 2px; height: 6px; box-shadow: 0 12px 0 rgba(255, 209, 102, 0.85); }
.shooter-cross::after { top: 8px; left: 0; width: 6px; height: 2px; box-shadow: 12px 0 0 rgba(255, 209, 102, 0.85); }
/* Where things are, without doing the looking for you: what you can see is
   drawn solid and what you cannot is drawn hollow. */
/* Top right, because bottom right is where the hand holding the kanji is, and a
   weapon behind a map is a weapon you cannot see. */
.shooter-map { position: absolute; z-index: 5; right: 12px; top: 58px; width: 156px; height: 156px; background: rgba(5, 9, 17, 0.72); border: 1px solid rgba(148, 198, 255, 0.35); box-shadow: 0 0 22px rgba(5, 9, 17, 0.7); pointer-events: none; }
/* What the aimed-at monster is asking, under the crosshair. On the HUD rather
   than out in the room for two reasons the room could not give it: it is always
   the same size, however far away the thing asking it is standing, and nothing
   you do with the mouse can turn it. Clear of the crosshair itself, because the
   crosshair is what you are pointing with. */
/* The same thing the plaques in the room are, in the place the HUD keeps for it.
   Those are drawn as a light projection — a hot centre, a pair of misregistered
   chromatic ghosts, a cyan throw and a dark outline to hold it against a lit
   wall — with deliberately no filled shape behind any of it. Moving the words to
   the HUD is not a reason to restyle them: it is the same question, still coming
   off the same monster, and a panel round it here would make it a different
   object that happened to say the same words.

   The colours and the offsets below are the ones in shooterPlaqueLabelTexture
   and shooterPlaqueAnswerTexture, scaled from the texture's 158px glyph to this
   one's. Shadows are listed outline first, then ghosts, then throw: CSS paints
   the earlier ones on top, which is the order the canvas draws them in. */
.shooter-readout { position: absolute; z-index: 9; top: calc(50% + 52px); left: 50%; display: flex; flex-direction: column; align-items: center; gap: 4px; max-width: min(76vw, 30rem); transform: translateX(-50%); text-align: center; pointer-events: none; }
.shooter-readout[hidden] { display: none; }
.shooter-readout-label { color: rgba(201, 248, 255, 0.92); font-family: system-ui, -apple-system, sans-serif; font-size: 0.82rem; font-weight: 800; text-transform: uppercase; text-shadow: 1px 1px 0 rgba(3, 12, 24, 0.92), -1px 1px 0 rgba(3, 12, 24, 0.92), 1px -1px 0 rgba(3, 12, 24, 0.92), -1px -1px 0 rgba(3, 12, 24, 0.92), 0 0 12px rgba(79, 224, 255, 0.95), 0 0 4px rgba(79, 224, 255, 0.8); }
.shooter-readout-text { color: rgba(226, 253, 255, 0.94); font-family: "Yu Mincho", "Hiragino Mincho ProN", "Noto Serif JP", serif; font-size: 2.5rem; font-weight: 700; line-height: 1.2; overflow-wrap: anywhere; text-shadow: 1px 1px 0 rgba(3, 12, 24, 0.94), -1px 1px 0 rgba(3, 12, 24, 0.94), 1px -1px 0 rgba(3, 12, 24, 0.94), -1px -1px 0 rgba(3, 12, 24, 0.94), -1px -1px 0 rgba(65, 170, 255, 0.4), 1px 1px 0 rgba(151, 246, 255, 0.45), 0 0 16px rgba(69, 224, 255, 0.95), 0 0 6px rgba(69, 224, 255, 0.7); }
/* Broken scan lines, none long enough to imply a panel — the same trick the
   textures use to say projection rather than sign. */
.shooter-readout::before, .shooter-readout::after { position: absolute; content: ""; height: 2px; background: rgba(86, 225, 255, 0.4); pointer-events: none; }
.shooter-readout::before { top: 6px; left: 8%; width: 14%; }
.shooter-readout::after { right: 12%; bottom: 7px; width: 11%; }
/* Arrived rather than appeared: it has just come off a monster, and a beat of
   movement is what says so. */
.shooter-readout.is-taken { animation: shooter-readout-taken 190ms cubic-bezier(0.2, 0.9, 0.3, 1) both; }
@keyframes shooter-readout-taken {
  0% { transform: translateX(-50%) translateY(-10px) scale(0.94); opacity: 0; }
  100% { transform: translateX(-50%) translateY(0) scale(1); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) { .shooter-readout.is-taken { animation: none; } }

/* The count-in, over the middle of the view. It sits above the crosshair rather
   than on it, because the three seconds are for looking at the room. */
.shooter-count { position: absolute; z-index: 10; top: 30%; left: 50%; display: flex; flex-direction: column; align-items: center; gap: 4px; transform: translate(-50%, -50%); pointer-events: none; text-align: center; }
.shooter-count[hidden] { display: none; }
.shooter-count-label { margin: 0; color: rgba(234, 240, 251, 0.86); font-size: 0.78rem; font-weight: 850; letter-spacing: 0.14em; text-transform: uppercase; text-shadow: 0 2px 6px #000; }
.shooter-count[data-go="true"] .shooter-count-label { visibility: hidden; }
.shooter-count-tick { margin: 0; color: var(--star); font-size: 4.6rem; font-weight: 900; line-height: 1; font-variant-numeric: tabular-nums; letter-spacing: 0.04em; text-shadow: 0 0 18px rgba(255, 209, 102, 0.55), 0 4px 12px #000; }
.shooter-count[data-go="true"] .shooter-count-tick { color: var(--green); text-shadow: 0 0 22px rgba(79, 224, 166, 0.6), 0 4px 12px #000; }
/* Each tick lands big and settles. The class is taken off and put back with a
   reflow between, which is what restarts it on the next number. */
.shooter-count-tick.is-tick { animation: shooter-count-tick 900ms cubic-bezier(0.16, 0.84, 0.32, 1) both; }
.shooter-count[data-go="true"] .shooter-count-tick.is-tick { animation: shooter-count-go 420ms cubic-bezier(0.2, 1.5, 0.4, 1) both; }
@keyframes shooter-count-tick {
  0% { transform: scale(2.7); opacity: 0; }
  18% { opacity: 1; }
  62% { transform: scale(1); opacity: 1; }
  100% { transform: scale(0.94); opacity: 0.28; }
}
@keyframes shooter-count-go {
  0% { transform: scale(0.45); opacity: 0; }
  45% { transform: scale(1.32); opacity: 1; }
  100% { transform: scale(1.12); opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .shooter-count-tick.is-tick,
  .shooter-count[data-go="true"] .shooter-count-tick.is-tick { animation: none; }
}
.shooter-say { position: absolute; z-index: 8; right: 22%; bottom: 78px; left: 22%; min-height: 1.2em; margin: 0; padding: 8px 12px; color: rgba(234, 240, 251, 0.9); background: rgba(5, 9, 17, 0.68); border-left: 2px solid rgba(148, 198, 255, 0.55); font-size: 0.95rem; font-weight: 800; text-align: center; text-shadow: 0 2px 4px #000; pointer-events: none; }
.shooter-say[data-right="true"] { color: var(--green); }
.shooter-say[data-right="false"] { color: var(--red); }
.shooter-guns { position: absolute; z-index: 9; bottom: 14px; left: 50%; display: flex; max-width: calc(100% - 360px); min-height: 52px; gap: 5px; justify-content: center; padding: 6px 8px; transform: translateX(-50%); background: rgba(5, 9, 17, 0.74); border: 1px solid rgba(148, 198, 255, 0.24); box-shadow: 0 0 24px rgba(0, 0, 0, 0.58); pointer-events: auto; }
.shooter-gun { position: relative; display: grid; width: 48px; height: 48px; flex: 0 0 auto; padding: 0; place-items: center; overflow: hidden; cursor: pointer; color: #f7feff; appearance: none; background: radial-gradient(circle at 50% 42%, rgba(93, 245, 255, 0.2), transparent 50%), linear-gradient(145deg, rgba(21, 43, 61, 0.98), rgba(5, 12, 20, 0.98)); border: 2px solid rgba(109, 232, 255, 0.8); border-radius: 4px; box-shadow: inset 0 0 0 2px #101a28, inset 0 0 15px rgba(85, 224, 255, 0.38), 0 0 10px rgba(85, 224, 255, 0.2); font: inherit; transition: filter 80ms ease, transform 80ms ease; touch-action: manipulation; }
.shooter-gun::after { position: absolute; inset: 5px; content: ""; border: 1px solid rgba(255, 209, 102, 0.42); pointer-events: none; }
.shooter-gun-face { position: relative; z-index: 1; padding-top: 7px; font-family: var(--jp-serif); font-size: 1.45rem; line-height: 1; text-shadow: 0 0 4px #fff, 0 0 10px #5ce6ff; }
/* The number is the trigger now, not a selection label. */
.shooter-gun-key { position: absolute; z-index: 2; top: 2px; left: 4px; color: rgba(185, 236, 255, 0.92); font-size: 0.65rem; font-weight: 900; font-variant-numeric: tabular-nums; letter-spacing: 0.02em; text-shadow: 0 0 6px rgba(85, 224, 255, 0.75); }
/* There is no persistent selected tile; only the weapon that actually fired flashes. */
.shooter-gun.is-firing { transform: translateY(-3px) scale(1.05); filter: brightness(1.65); box-shadow: inset 0 0 0 2px #101a28, inset 0 0 19px rgba(255, 209, 102, 0.72), 0 0 22px rgba(85, 224, 255, 0.72); }
.shooter-hint { position: absolute; z-index: 7; bottom: 18px; left: 16px; width: min(340px, 30vw); margin: 0; color: rgba(242, 247, 255, 0.74); font-size: 0.72rem; font-weight: 850; letter-spacing: 0.045em; line-height: 1.45; text-transform: uppercase; text-shadow: 0 2px 5px #000; pointer-events: none; }

/* Phone controls are floating rather than bolted to two circles. The left base
   appears at first contact and follows an overextended thumb. The right half is
   an invisible swipe surface, leaving its visible space to the weapon buttons. */
.shooter-touch { display: none; position: absolute; z-index: 8; inset: 0; pointer-events: none; }
.shooter-touch-zone { position: absolute; top: 0; bottom: 0; width: 50%; overflow: hidden; touch-action: none; user-select: none; -webkit-user-select: none; }
.shooter-touch-move { left: 0; }
.shooter-touch-look { right: 0; }
.shooter-stick { position: absolute; width: 116px; height: 116px; opacity: 0; transform: translate(-50%, -50%) scale(0.86); background: radial-gradient(circle, rgba(31, 79, 103, 0.32) 0 40%, rgba(5, 12, 20, 0.5) 42% 100%); border: 2px solid rgba(109, 232, 255, 0.62); border-radius: 50%; box-shadow: inset 0 0 24px rgba(85, 224, 255, 0.2), 0 0 22px rgba(0, 0, 0, 0.5); transition: opacity 90ms ease, transform 90ms ease; pointer-events: none; }
.shooter-stick.is-active { opacity: 0.9; transform: translate(-50%, -50%) scale(1); }
.shooter-stick span { position: absolute; top: 50%; left: 50%; width: 54px; height: 54px; transform: translate(-50%, -50%); background: radial-gradient(circle at 38% 32%, rgba(225, 253, 255, 0.86), rgba(59, 177, 205, 0.72) 32%, rgba(7, 25, 39, 0.92) 70%); border: 2px solid rgba(179, 249, 255, 0.92); border-radius: 50%; box-shadow: 0 0 16px rgba(85, 224, 255, 0.68); }
.shooter-touch-look .shooter-stick { display: none; }

/* A screen-space energy weapon: deliberately flat and chunky, like a sprite in
   a 1999 shooter. The card is the weapon and the short grip is the hand-held
   silhouette; glow, scanlines and recoil make it belong to the rendered scene. */
.shooter-held { --recoil: 0; position: absolute; z-index: 7; right: clamp(30px, 5vw, 78px); bottom: clamp(86px, 13vh, 132px); width: clamp(108px, 10vw, 148px); aspect-ratio: 1; transform: translateY(calc(var(--recoil) * 24px)); transform-origin: 50% 100%; filter: drop-shadow(0 0 9px rgba(85, 224, 255, 0.88)) drop-shadow(0 12px 13px rgba(0, 0, 0, 0.72)); pointer-events: none; transition: transform 45ms linear; }
.shooter-held[hidden] { display: none; }
.shooter-held::before { position: absolute; z-index: -1; right: -8%; bottom: -14%; width: 48%; height: 34%; content: ""; transform: rotate(-12deg); background: linear-gradient(145deg, #40506a, #111925 62%); border: 2px solid #080b10; border-radius: 45% 45% 18% 24%; box-shadow: inset 0 0 0 2px rgba(148, 198, 255, 0.14), 0 8px 12px rgba(0, 0, 0, 0.55); }
.shooter-held::after { position: absolute; top: -9%; left: 33%; width: 34%; aspect-ratio: 1; content: ""; opacity: 0; transform: rotate(45deg); background: #fff3bd; box-shadow: 0 0 12px #fff, 0 0 28px #ffd166, 0 0 54px #ff8a3d; }
.shooter-held[data-firing="true"]::after { opacity: 0.95; }
.shooter-held-core { position: absolute; inset: 0; display: grid; place-items: center; overflow: hidden; background: radial-gradient(circle at 50% 42%, rgba(93, 245, 255, 0.28), transparent 48%), linear-gradient(145deg, rgba(21, 43, 61, 0.98), rgba(5, 12, 20, 0.98)); border: 3px solid #6de8ff; border-radius: 6px; box-shadow: inset 0 0 0 5px #101a28, inset 0 0 26px rgba(85, 224, 255, 0.58), 0 0 28px rgba(85, 224, 255, 0.55); }
.shooter-held-core::before { position: absolute; inset: 0; content: ""; opacity: 0.3; background: repeating-linear-gradient(0deg, transparent 0 5px, rgba(160, 245, 255, 0.16) 6px); }
.shooter-held-core::after { position: absolute; inset: 8px; content: ""; border: 1px solid rgba(255, 209, 102, 0.68); border-radius: 2px; }
.shooter-held-core span { position: relative; z-index: 1; color: #f7feff; font-family: var(--jp-serif); font-size: clamp(3.8rem, 7vw, 6.8rem); font-weight: 700; line-height: 1; text-shadow: 0 0 7px #fff, 0 0 17px #5ce6ff, 0 0 34px rgba(85, 224, 255, 0.9); }
@media (max-width: 620px) {
  .shooter-map { width: 110px; height: 110px; }
  .shooter-gun { width: 36px; height: 36px; font-size: 1.2rem; }
  .shooter-guns { max-width: calc(100% - 24px); overflow: hidden; }
  .shooter-held { right: 18px; bottom: 88px; width: 94px; }
  .shooter-hint { display: none; }
  .shooter-say { right: 12px; bottom: 64px; left: 12px; }
}

@media (max-width: 900px) {
  .shooter-touch { display: block; }
  .shooter-touch-zone { pointer-events: auto; }
  .shooter-hint { display: none; }
  /* A compact last-shot receipt sits beside the direct-fire choices. */
  .shooter-held { right: 104px; bottom: max(10px, env(safe-area-inset-bottom)); width: 72px; }
  .shooter-guns { z-index: 11; right: max(8px, env(safe-area-inset-right)); bottom: max(8px, env(safe-area-inset-bottom)); left: auto; display: grid; grid-template-columns: repeat(2, 38px); max-width: none; max-height: calc(100% - 72px); min-height: 0; gap: 5px; padding: 6px; overflow-x: hidden; overflow-y: auto; transform: none; background: rgba(5, 9, 17, 0.5); scrollbar-width: none; }
  .shooter-guns::-webkit-scrollbar { display: none; }
  .shooter-gun { width: 38px; height: 38px; }
  .shooter-gun-face { padding-top: 6px; font-size: 1.2rem; }
  .shooter-gun-key { font-size: 0.56rem; }
  .shooter-say { right: 104px; bottom: 88px; left: 104px; }
  .shooter-count-tick { font-size: 3.2rem; }
}

/* Which game the list belongs to. It sits at the head of the list rather than
   beside Start, because changing it changes what every row is reporting — each
   game keeps its own record — and that is not something to discover afterwards
   from a control by the exit. */
.menu-head-side { display: flex; align-items: center; gap: 12px; flex: 0 0 auto; }
/* The head row does not shrink its right-hand side, so a long title used to
   push the game control and the back link off a narrow window — "Learning Path"
   over a named deck is simply the first title long enough to do it every time.
   Below the width where that starts to bite the row wraps instead, and the side
   sits under the title rather than past the edge of the page. */
@media (max-width: 620px) {
  .menu-head { flex-wrap: wrap; }
  .menu-head-side { flex: 1 1 100%; justify-content: flex-end; }
}
.game-select { display: inline-flex; align-items: center; gap: 8px; }
.game-select[hidden] { display: none; }
.game-select span { color: var(--muted); font-size: 0.62rem; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; }
.game-select select { padding: 8px 12px; cursor: pointer; color: var(--ink); background: rgba(255, 255, 255, 0.08); border: 1px solid var(--line-lit); font: inherit; font-size: 0.72rem; font-weight: 800; letter-spacing: 0.05em; }
.game-select select:hover, .game-select select:focus-visible { border-color: var(--line-lit); outline: none; }
.game-select select[data-type="wordmaze"] { color: var(--star); border-color: rgba(255, 209, 102, 0.5); }
.game-select select[data-type="grid"] { color: var(--green); border-color: rgba(79, 224, 166, 0.5); }


/* The two top-level lists. Tabs rather than another row in the menu, because
   neither is inside the other: the sets are what there is to learn, and the
   multidecks are what you have built out of them. */
.menu-tabs { display: flex; gap: 6px; margin-bottom: 16px; border-bottom: 1px solid rgba(88, 166, 255, 0.28); }
.play-mode-tabs { margin-bottom: 9px; }
.menu-tabs[hidden] { display: none; }
.menu-tab { position: relative; display: inline-flex; align-items: center; gap: 7px; padding: 10px 16px; bottom: -1px; cursor: pointer; color: var(--muted); background: transparent; border: 1px solid transparent; border-bottom-color: rgba(88, 166, 255, 0.28); font: inherit; font-size: 0.72rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; transition: color 140ms ease, border-color 140ms ease, background 140ms ease; }
.menu-tab:hover { color: var(--ink); }
.menu-tab.is-current { color: var(--ink); background: rgba(88, 166, 255, 0.16); border-color: var(--line-lit); border-bottom-color: var(--bg, #0d213e); box-shadow: inset 0 2px 0 #58a6ff; }
.menu-tab:focus-visible { outline: none; color: var(--ink); box-shadow: 0 0 0 1px var(--line-lit); }
.menu-tab-count { display: inline-grid; min-width: 19px; height: 19px; padding: 0 5px; place-items: center; color: #04211a; background: var(--green); border-radius: 999px; font-size: 0.62rem; font-weight: 900; font-variant-numeric: tabular-nums; }
.menu-tab-count[hidden] { display: none; }

/* Headings inside a list, dividing the pinned runs from the history behind
   them. Not a set name and not a row — a label on the group that follows. */
.menu-heading { margin: 4px 0 2px; color: var(--muted); font-size: 0.64rem; font-weight: 800; letter-spacing: 0.11em; text-transform: uppercase; }
.menu-heading:not(:first-child) { margin-top: 18px; }
.menu-empty { margin: 0; padding: 22px; color: var(--muted); background: rgba(255, 255, 255, 0.04); border: 1px dashed var(--line-lit); font-size: 0.84rem; line-height: 1.6; }

/* A remembered multideck. The name is the button that plays it; the pin and the
   rename sit outside that button, because a control inside a control cannot be
   pressed on its own. */
.merge-row { display: flex; gap: 8px; align-items: stretch; }
.merge-row .merge-play { flex: 1 1 auto; min-width: 0; }
.merge-row.is-pinned .merge-play { border-color: rgba(255, 209, 102, 0.4); background: linear-gradient(158deg, rgba(255, 209, 102, 0.1), rgba(255, 209, 102, 0.03)); }
.merge-actions { display: flex; flex: 0 0 auto; flex-direction: column; gap: 6px; justify-content: center; }
.merge-pin, .merge-rename { padding: 8px 12px; cursor: pointer; color: var(--muted); background: rgba(255, 255, 255, 0.05); border: 1px solid var(--line); font: inherit; font-size: 0.66rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; white-space: nowrap; transition: color 140ms ease, border-color 140ms ease, background 140ms ease; }
.merge-pin:hover, .merge-rename:hover, .merge-pin:focus-visible, .merge-rename:focus-visible { color: var(--ink); border-color: var(--line-lit); outline: none; }
.merge-pin[aria-pressed="true"] { color: var(--star); background: rgba(255, 209, 102, 0.12); border-color: rgba(255, 209, 102, 0.42); }
.merge-rename-input { width: 100%; padding: 4px 8px; color: var(--ink); background: rgba(5, 9, 17, 0.75); border: 1px solid var(--line-lit); font: inherit; font-size: 0.92rem; font-weight: 800; }
.merge-rename-input:focus-visible { outline: none; box-shadow: 0 0 0 1px var(--line-lit); }

/* What a deck has cost, beside what it has earned. Kept quieter than the
   mastered count: it is the price, not the point. */
.deck-misses { flex: 0 0 auto; color: var(--red); font-variant-numeric: tabular-nums; }

/* The card list. A record rather than a menu, so it is laid out as a table:
   eight facts about one card belong on one line, and eight stacked labels per
   row would turn ten thousand cards into a scroll nobody finishes. */
.card-list[hidden] { display: none; }
.card-list-tallies { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 14px; }
.card-list-tally { display: flex; flex: 1 1 120px; flex-direction: column; gap: 3px; padding: 9px 12px; background: rgba(255, 255, 255, 0.05); border: 1px solid var(--line); }
.card-list-tally b { font-size: 1.02rem; font-weight: 900; font-variant-numeric: tabular-nums; }
.card-list-tally small { color: var(--muted); font-size: 0.63rem; font-weight: 800; letter-spacing: 0.09em; text-transform: uppercase; }
.card-list-tally.is-done b { color: var(--green); }
.card-list-tally.is-going b { color: var(--blue); }
.card-list-tally.is-owed b { color: var(--orange); }
.card-list-tally.is-due b { color: var(--red); }
.card-list-tally.is-stars b { color: var(--star); }
.card-list-tally.is-misses b { color: var(--red); }

.card-list-controls { display: flex; flex-wrap: wrap; gap: 10px; margin-bottom: 12px; }
.card-list-search { flex: 1 1 260px; }
.card-list-search input, .card-list-filter select { width: 100%; padding: 10px 12px; color: var(--ink); background: rgba(255, 255, 255, 0.08); border: 1px solid var(--line-lit); font: inherit; font-size: 0.82rem; }
.card-list-search input:focus-visible, .card-list-filter select:focus-visible { outline: none; border-color: var(--line-lit); box-shadow: 0 0 0 1px var(--line-lit); }
.card-list-filter { flex: 0 1 220px; }

/* The table scrolls inside its own box rather than pushing the page sideways:
   the deck column is the one that runs long, and a window narrow enough to cut
   it should still be able to read the stars. */
.card-list-scroll { overflow-x: auto; border: 1px solid var(--line); }
.card-list-table { width: 100%; border-collapse: collapse; font-size: 0.78rem; }
.card-list-table th { position: sticky; top: 0; z-index: 1; padding: 9px 11px; color: var(--muted); background: rgba(255, 255, 255, 0.1); border-bottom: 1px solid var(--line-lit); font-size: 0.62rem; font-weight: 800; letter-spacing: 0.09em; text-align: left; text-transform: uppercase; white-space: nowrap; }
.card-list-table td { max-width: 22ch; padding: 8px 11px; overflow: hidden; border-bottom: 1px solid rgba(255, 255, 255, 0.05); text-overflow: ellipsis; white-space: nowrap; }
.card-list-row:hover td { background: rgba(255, 255, 255, 0.08); }
.card-list-num { color: var(--muted); font-variant-numeric: tabular-nums; }
.card-list-face { font-family: var(--jp-serif); font-size: 1.05rem; }
.card-list-reading { color: var(--blue-deep); }
.card-list-gloss { color: var(--ink); }
.card-list-deck { color: var(--muted); font-size: 0.7rem; }
.card-list-state { color: var(--muted); font-size: 0.68rem; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; white-space: nowrap; }
/* A card that has been fought and lost is the one state the old single bit
   could not show, so it is the one that gets a colour of its own. */
.card-list-row.state-1 .card-list-state { color: var(--orange); }
.card-list-row.state-4 .card-list-state { color: var(--green); }
.card-list-misses { color: rgba(255, 255, 255, 0.2); font-variant-numeric: tabular-nums; }
.card-list-misses.has-misses { color: var(--red); }
.card-list-next { color: var(--muted); font-variant-numeric: tabular-nums; white-space: nowrap; }
.card-list-next.is-due { color: var(--red); font-weight: 800; }

/* The unearned stars are drawn too, in the row's own dim, so a card at two
   stars visibly has three left to reach rather than simply lacking a mark. */
.card-list-stars { color: rgba(255, 255, 255, 0.16); font-size: 0.9rem; letter-spacing: 0.08em; white-space: nowrap; }
.card-list-stars[data-stars="1"], .card-list-stars[data-stars="2"] { color: var(--star-low); }
.card-list-stars[data-stars="3"], .card-list-stars[data-stars="4"] { color: var(--star); }
.card-list-stars[data-stars="5"] { color: var(--green); text-shadow: 0 0 12px rgba(79, 224, 166, 0.45); }

.card-list-pager { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 12px; }
.card-list-page { margin: 0; color: var(--muted); font-size: 0.72rem; font-weight: 800; letter-spacing: 0.05em; font-variant-numeric: tabular-nums; }
.card-list-pager .text-button { padding: 8px 13px; color: var(--ink); background: rgba(255, 255, 255, 0.07); border: 1px solid var(--line); font-size: 0.7rem; letter-spacing: 0.08em; text-transform: uppercase; }
.card-list-pager .text-button:disabled { opacity: 0.35; cursor: default; }

/* The bar of picked decks. Fixed to the foot of the window because the list it
   belongs to is longer than the window on every set worth picking from. */
.deck-launch { position: fixed; z-index: 15; right: 0; bottom: 0; left: 0; display: flex; align-items: center; justify-content: center; gap: 18px; flex-wrap: wrap; padding: 12px 18px calc(12px + env(safe-area-inset-bottom)); background: linear-gradient(180deg, rgba(255, 255, 255, 0.1), rgba(10, 20, 33, 0.94)); border-top: 1px solid var(--line-lit); box-shadow: 0 -18px 44px rgba(0, 0, 0, 0.45); -webkit-backdrop-filter: blur(6px); backdrop-filter: blur(6px); animation: launch-rise 180ms ease both; }
.deck-launch[hidden] { display: none; }
body.is-playing .deck-launch { display: none; }
/* The bar covers the foot of the list while it is up, so the list is given back
   the height the bar takes. */
body.has-picks .shell { padding-bottom: 96px; }
.deck-launch-summary { margin: 0; color: #d2ebff; font-size: 0.78rem; font-weight: 800; letter-spacing: 0.05em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
.deck-launch-actions { display: flex; align-items: center; gap: 10px; }
.deck-launch-clear { padding: 10px 14px; cursor: pointer; color: #d2ebff; background: rgba(255, 255, 255, 0.03); border: 1px solid var(--line); font: inherit; font-size: 0.7rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
.deck-launch-clear:hover, .deck-launch-clear:focus-visible { color: var(--ink); border-color: var(--line-lit); outline: none; }
.deck-launch-start { padding: 11px 26px; cursor: pointer; color: #05315a; background: linear-gradient(180deg, #5be6b2, #46d79e); border: 1px solid rgba(79, 224, 166, 0.7); box-shadow: 0 0 28px rgba(84, 222, 171, 0.35); font: inherit; font-size: 0.78rem; font-weight: 900; letter-spacing: 0.09em; text-transform: uppercase; transition: transform 140ms ease, box-shadow 140ms ease; }
.deck-launch-start:hover, .deck-launch-start:focus-visible { transform: translateY(-1px); box-shadow: 0 0 36px rgba(79, 224, 166, 0.5); outline: none; }

@keyframes launch-rise {
  from { opacity: 0; transform: translateY(100%); }
  to { opacity: 1; transform: none; }
}

.deck-context { position: fixed; z-index: 30; display: grid; min-width: 250px; padding: 5px; background: linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(24, 44, 67, 0.95)); border: 1px solid var(--line-lit); box-shadow: var(--shadow); }
.deck-context[hidden] { display: none; }
.deck-context-item { padding: 10px 12px; cursor: pointer; text-align: left; color: var(--ink); background: transparent; border: 0; font: inherit; font-size: 0.8rem; font-weight: 700; }
.deck-context-item:hover, .deck-context-item:focus-visible { background: rgba(85, 164, 255, 0.18); outline: none; }

/* The end of a run. Same weight as the per-card reward, because finishing the
   pile is the bigger of the two moments. */
.session-done { position: fixed; z-index: 25; inset: 0; display: grid; place-items: center; padding: 24px; background: rgba(3, 7, 14, 0.8); -webkit-backdrop-filter: blur(5px); backdrop-filter: blur(5px); }
.session-done[hidden] { display: none; }
.session-done-panel { width: min(100%, 520px); max-height: calc(100dvh - 48px); overflow-y: auto; padding: 30px; text-align: center; background: linear-gradient(180deg, #e8f4ff, #cfe4f9); border: 1px solid rgba(88, 166, 255, 0.35); box-shadow: 0 30px 90px rgba(0, 0, 0, 0.45), 0 0 70px rgba(88, 166, 255, 0.18); animation: pop-in 220ms cubic-bezier(0.2, 1.3, 0.5, 1) both; }
.session-done-panel:focus { outline: none; }
.session-done-panel h2 { margin: 4px 0 0; color: #2b7ad1; font-size: clamp(1.6rem, 3.4vw, 2.2rem); font-weight: 900; letter-spacing: -0.01em; text-shadow: 0 0 20px rgba(88, 166, 255, 0.3); }
.session-done-mark { margin: 6px 0 10px; color: var(--green); font-size: clamp(3rem, 12vw, 4.4rem); line-height: 1; text-shadow: 0 0 40px rgba(79, 224, 166, 0.45); }
.session-done-summary { margin: 0; color: #133a5f; font-size: 0.98rem; font-weight: 800; }
.session-done-decks { display: grid; gap: 7px; margin: 16px 0 0; padding: 0; list-style: none; }
.session-done-decks li { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 9px 12px; text-align: left; background: var(--well); border: 1px solid var(--line); font-size: 0.8rem; }
.session-done-decks li.is-complete { border-color: rgba(79, 224, 166, 0.4); }
.session-done-decks strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 800; }
.session-done-decks span { flex: 0 0 auto; color: var(--muted); font-weight: 800; font-variant-numeric: tabular-nums; }
.session-done-decks li.is-complete span { color: var(--green); }
.session-done-note { margin: 14px 0 0; color: var(--muted); font-size: 0.8rem; line-height: 1.5; }
.session-done-note[hidden] { display: none; }
.session-done-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-top: 22px; }
.session-done-button { padding: 12px 22px; cursor: pointer; color: #0d2548; background: linear-gradient(180deg, #ffffff, #dcedff); border: 1px solid rgba(88, 166, 255, 0.45); font: inherit; font-size: 0.74rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; transition: transform 140ms ease, background 140ms ease, border-color 140ms ease; }
.session-done-button:hover, .session-done-button:focus-visible { transform: translateY(-1px); border-color: var(--line-lit); background: rgba(85, 164, 255, 0.18); outline: none; }
.session-done-button.is-primary { color: #03385f; background: linear-gradient(180deg, #6df0bd, #59dca5); border-color: rgba(84, 222, 171, 0.75); box-shadow: 0 0 20px rgba(84, 222, 171, 0.32); }
.session-done-button.is-primary:hover, .session-done-button.is-primary:focus-visible { background: linear-gradient(180deg, #79f3c2, #6de9b2); }

.game { position: relative; padding: 16px; background: linear-gradient(180deg, rgba(255, 255, 255, 0.09), rgba(255, 255, 255, 0.035)); border: 1px solid var(--line-lit); box-shadow: var(--shadow); }

/* The HUD: which deck, and how far through it. Boxed and pinned to the top of
   the panel so it reads as instrumentation rather than as a heading. */
/* One bar at the foot of the frame: totals in the left corner, the deck in the
   middle, the card's state and the session controls on the right. The two sides
   are given equal flex so the middle sits on the centre of the frame rather than
   wherever the two corners happen to leave it. */
.hud { display: flex; align-items: center; gap: 14px; margin-top: 10px; padding: 8px 12px; background: linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.035)); border: 1px solid var(--line-lit); }
.hud-side { display: flex; flex: 1 1 0; min-width: 0; align-items: center; gap: 8px; }
.hud-right { justify-content: flex-end; }
.hud-mid { flex: 0 1 auto; min-width: 0; text-align: center; }
.hud-mid h2 { margin: 2px 0 0; font-size: 0.95rem; letter-spacing: -0.01em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.hud-status { margin: 0; min-width: 0; overflow: hidden; color: var(--muted); font-size: 0.72rem; line-height: 1.35; text-overflow: ellipsis; }
.progress { margin-top: 5px; color: var(--muted); font-size: 0.66rem; font-weight: 800; letter-spacing: 0.06em; text-transform: uppercase; font-variant-numeric: tabular-nums; }
.progress-track { width: min(240px, 28vw); margin-inline: auto; }

/* The session controls are one button until asked for. They are the rarest thing
   on this bar — chosen once a session, if at all — and at full width they were
   three shouting labels beside the thing you actually watch. */
.hud-menu { position: relative; flex: 0 0 auto; }
.hud-menu-button { display: grid; width: 34px; height: 34px; place-items: center; cursor: pointer; background: rgba(255, 255, 255, 0.12); border: 1px solid rgba(88, 166, 255, 0.35); transition: background 140ms ease, border-color 140ms ease; }
.hud-menu-button:hover, .hud-menu-button[aria-expanded="true"] { background: rgba(88, 166, 255, 0.2); border-color: var(--line-lit); }
/* The outer bars sit ±7px from the middle one, so the stack is 16px tall against
   16px wide. A square icon is what makes an equal inset read as an equal gap —
   at ±5 the stack was 12px tall, and the same inset left more air above it than
   beside it. */
.hamburger, .hamburger::before, .hamburger::after { display: block; width: 16px; height: 2px; background: var(--ink); }
.hamburger { position: relative; }
.hamburger::before, .hamburger::after { position: absolute; left: 0; content: ""; }
.hamburger::before { top: -7px; }
.hamburger::after { top: 7px; }
/* Opens upward: the bar is at the foot of the window, so downward has nowhere to
   go. Right-anchored so a long label grows into the frame, not off the edge. */
/* The pause screen. It takes the window rather than hanging off the button, and
   the ground behind it is only dimmed — the card stays visible underneath, so
   stopping to shuffle never loses your place on it. */
.hud-menu-panel { position: fixed; z-index: 30; display: grid; inset: 0; place-items: center; padding: 24px; background: rgba(6, 20, 37, 0.75); backdrop-filter: blur(7px); }
/* A class setting `display` outranks the user-agent's `[hidden] { display: none }`,
   so the attribute stops hiding anything and the panel sits open over the card.
   Every element styled with a display here has to say so itself. */
.hud-menu-panel[hidden] { display: none; }
/* No panel behind the choices — the dimmed window is the surface. A box here
   would be a second frame inside a frame the dimming already draws. */
.hud-menu-sheet { display: flex; width: min(100%, 300px); flex-direction: column; gap: 9px; text-align: center; }
.hud-menu-sheet .eyebrow { margin-bottom: 6px; }
.input-setting { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin: 3px 0 5px; padding: 0; border: 0; text-align: left; }
.input-setting legend { grid-column: 1 / -1; margin-bottom: 1px; color: var(--muted); font-size: 0.64rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
.input-setting label { display: flex; align-items: flex-start; gap: 7px; padding: 9px; cursor: pointer; background: rgba(255, 255, 255, 0.05); border: 1px solid var(--line); }
.input-setting label:has(input:checked) { background: rgba(85, 164, 255, 0.15); border-color: var(--blue); }
.input-setting input { flex: 0 0 auto; margin: 2px 0 0; accent-color: var(--blue); }
.input-setting span, .input-setting small { display: block; }
.input-setting span { color: var(--ink); font-size: 0.7rem; font-weight: 750; line-height: 1.25; }
.input-setting small { margin-top: 3px; color: var(--muted); font-size: 0.61rem; font-weight: 600; }
.hud-menu-panel .text-button { width: 100%; padding: 14px 18px; white-space: nowrap; font-size: 0.76rem; text-align: center; }
.hud-menu-dismiss { margin: 4px 0 0; color: var(--muted); font-size: 0.68rem; }
/* Resume is the way out, not a change to the session, so it reads as the lit
   one and the three that end the card sit behind it. */
.hud-menu-panel .text-button.is-primary { color: #08101d; background: var(--blue); border-color: var(--blue); }
.hud-menu-panel .text-button.is-primary:hover { background: var(--blue-deep); border-color: var(--blue-deep); }

.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }

/* Standing explanations, folded away. They are read once and then reread by
   nobody, but they cost their height on every card and they are the prose
   competing with the character for the eye. The toggle remembers itself, so
   opening it once keeps it open. */
/* Pinned to the corner of its panel rather than given a line of its own: it is a
   way in to something, not a step in reading the panel, and on its own line it
   cost the height the fold was meant to save. */
/* The corner button is out of flow, so each panel keeps a strip clear for it
   rather than letting a long label or a wide legend run underneath. */
.radical-pick, .radical-detail, .form-context, .answer-form { position: relative; }
.radical-detail, .form-context { padding-right: 32px; }
.answer-form { padding-top: 26px; }
.info-toggle { position: absolute; top: 6px; right: 6px; display: grid; width: 20px; height: 20px; place-items: center; cursor: pointer; color: var(--muted); background: rgba(255, 255, 255, 0.06); border: 1px solid var(--line); font-family: var(--ui); font-size: 0.68rem; font-weight: 900; font-style: italic; line-height: 1; transition: color 140ms ease, background 140ms ease, border-color 140ms ease; }
.info-toggle:hover, .info-toggle[aria-expanded="true"] { color: var(--ink); background: rgba(85, 164, 255, 0.18); border-color: var(--line-lit); }
/* Set apart as an aside rather than as more of the panel: italic, a rule down the
   side, and quieter than the thing it is explaining. It is a note about the
   card, not part of the question. */
.info-body { margin-top: 10px; padding-left: 9px; border-left: 2px solid var(--line-lit); font-style: italic; text-align: left; }
.info-body > p { margin: 0; }
.info-body > p + p { margin-top: 6px; }
.progress-track { height: 10px; margin-top: 8px; overflow: hidden; background: rgba(5, 9, 17, 0.75); border: 1px solid var(--line); box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.6); }
.progress-track i { display: block; width: 2%; height: 100%; background: linear-gradient(90deg, var(--green), var(--blue)); box-shadow: 0 0 14px rgba(85, 164, 255, 0.6); transition: width 220ms ease; }

.flashcard { width: 100%; }
/* The stage. Lit from the top so the character sits in a pool of light with the
   panel falling away below it. */
/* The stage packs from the top rather than centring its column. Centring stacked
   the prompt, the character and the console into a block in the middle of the
   frame with equal dead space above and below it; starting at the top gives the
   prompt its own place, lets the character take the room that opens up, and the
   console sits under it instead of being squeezed against it. */
.card-face { position: relative; display: flex; min-height: 340px; flex-direction: column; align-items: center; justify-content: space-between; gap: clamp(12px, 2.5vh, 36px); padding: 20px 22px 26px; background: radial-gradient(120% 86% at 50% -6%, rgba(85, 164, 255, 0.13), transparent 62%), var(--well); border: 1px solid var(--line); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.05), inset 0 -60px 90px rgba(0, 0, 0, 0.35); }
/* Real space between the question, the character and the boxes. These used to be
   three blocks pressed together with only their own margins between them, which
   read as one crowded lump in the middle of an otherwise empty frame. The gap
   scales with the window, so a tall screen spends its extra height on air rather
   than on a bigger void underneath. */
.card-stage { display: flex; width: 100%; min-width: 0; flex: 1 1 auto; flex-direction: column; align-items: center; justify-content: space-evenly; gap: clamp(14px, 4vh, 52px); }
/* Set larger than the words around it, in the serif stack the kanji use, and
   with the prompt's wide tracking cancelled — that tracking is for uppercase
   Latin and puts a gap after a character that has no gap in it. */
/* The same glow the character on the stage carries, so the one named in the
   question reads as the same object rather than as a letter in the sentence. */
.prompt-kanji { color: var(--ink); font-family: var(--jp-serif); font-size: 1.75em; font-weight: 700; letter-spacing: 0; line-height: 1; vertical-align: -0.16em; text-shadow: 0 0 18px rgba(148, 198, 255, 0.4), 0 2px 4px rgba(5, 9, 17, 0.85); }

/* Centred, because on a narrow window the question wraps to two lines and a
   ragged left edge inside a centred box reads as a mistake. */
.prompt { padding: 0; color: var(--muted); background: none; border: 0; font-size: clamp(0.95rem, 1.5vw, 1.25rem); font-weight: 800; letter-spacing: 0.09em; line-height: 1.45; text-align: center; text-transform: uppercase; }
/* Sized against the window's height as well as its width. A card has to hold a
   character, a legend and five boxes at once, so on a short window the glyph is
   the thing that gives way — it is still the biggest thing on screen. */
/* The character is the thing being learned and the only detailed thing on the
   screen, so it takes what the frame can give it. The vh terms rose with the
   header gone and the console hidden while picking — there is simply more stage
   than there was. The clamp floors are what a short window shrinks to, set in
   the short-viewport blocks above. */
/* Height is what actually limits the character; the vw term is only here to stop
   it running off the sides. A lone kanji is one em wide, so that limit can be
   generous — it used to be 30vw, which shrank the character on a narrow window
   that had all the vertical room in the world. Word cards keep a tighter limit
   below, because their glyph carries okurigana either side and is several em
   wide rather than one. */
.glyph { display: inline-flex; align-items: baseline; margin: 0; color: #fff; font-family: var(--jp-serif); font-size: clamp(5rem, min(58vw, 44vh), 22rem); line-height: 1; text-shadow: 0 0 46px rgba(148, 198, 255, 0.28); }
.kanji-glyph { position: relative; display: inline-block; line-height: 1; }
.word-affix { align-self: flex-end; padding-bottom: 0.05em; color: rgba(234, 240, 251, 0.72); font-size: 0.52em; line-height: 1; }
.word-prefix { margin-right: 0.03em; }
.word-suffix { margin-left: 0.03em; }
.flashcard.is-form-card .glyph { margin: 0; font-size: clamp(4.4rem, min(30vw, 36vh), 18rem); }
/* Larger than a word card's glyph, not smaller: the arrow round asks you to
   read the parts of one character, and the boxes have to be told apart. */
.flashcard.is-partial-card .glyph { margin: 0; font-size: clamp(4.8rem, min(54vw, 39vh), 19rem); }
/* Colouring a component, not boxing it.
   The kanji is drawn from its own stroke paths rather than set as text, so a
   component can be coloured exactly. Clipping a text glyph to a rectangle cut
   strokes in half wherever a component reached across the dividing line, which
   it does constantly — 包's strokes inside 泡 run left past the midpoint. */
/* Exactly one of the two renderings is visible, chosen by the parent's class.
   Neither child carries a `hidden` attribute, because that attribute behaves
   differently on an HTML span and an SVG root. */
.kanji-glyph.shows-strokes > #kanji-text { display: none; }
.kanji-glyph:not(.shows-strokes) > .kanji-svg { display: none; }
.kanji-svg {
  display: block;
  width: 1em;
  height: 1em;
  overflow: visible;
  filter: drop-shadow(0 0 22px rgba(0, 0, 0, 0.5));
}
.kanji-svg path {
  fill: none;
  stroke-width: 4.6;
  stroke-linecap: round;
  stroke-linejoin: round;
  transition: stroke 180ms ease;
}
/* While the choice is open both parts are tinted, differently, so the split is
   visible without either being marked as the answer. Every stroke colour here
   is a light on a dark ground now, so all of them are the bright end of their
   hue rather than the ink end. */
.kanji-svg path.s-one { stroke: #7fb2ff; }
.kanji-svg path.s-two { stroke: #e0b880; }
.kanji-svg path.s-target { stroke: var(--orange); }
.kanji-svg path.s-rest { stroke: #5b687f; }

/* Parts are numbered rather than pointed at, so a kanji made of five pieces
   asks its question the same way one made of two does. The legend draws each
   part as just its own strokes, in the same box the whole character occupies,
   so both its shape and where it sits are visible at a glance. */
/* No trailing margin: it used to hold the legend off the help text, and with the
   help text folded away it was 11px hanging under the chips, sitting them above
   the middle of their own box. Whatever follows brings its own top margin. */
/* Six parts is the most any card here splits into, and they stay on one row:
   min-width lets each chip give way as the row fills rather than wrapping, which
   is what keeps the numbers in a line you can read across. */
.part-legend { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; margin: 0; }
.part-legend .part-key { flex: 0 1 auto; min-width: 0; }
/* The chips are the answer, so they are sized to be aimed at rather than read:
   a bigger drawing of the part and a bigger target around it. The part sits
   above its number rather than beside it — the drawing is what you are choosing
   between, and the number is only how you say which one. */
/* No padding on the chip itself: the coloured number is a band across the foot
   of it, so any padding here would inset that band and leave a frame of chip
   showing around three of its sides. The drawing carries its own margin
   instead. */
.part-key { position: relative; display: inline-flex; align-items: center; gap: 0; flex-direction: column; padding: 0; overflow: hidden; cursor: pointer; color: inherit; background: rgba(255, 255, 255, 0.06); border: 1px solid var(--line); border-bottom: 3px solid var(--part); transition: transform 120ms ease, background 120ms ease, border-color 120ms ease; }
.part-key:hover { transform: translateY(-2px); background: rgba(255, 255, 255, 0.11); border-color: var(--line-lit); }
.part-key:active { transform: translateY(0) scale(0.97); }
/* A band across the whole foot of the chip rather than a badge floating inside
   it — full width, flush to all three edges, so it reads as the chip's label and
   stays legible when a crowded row squashes the drawing above it. Kept slim: it
   names the key to press, so it has to be readable and nothing more. The part
   above is the thing being chosen and should carry the weight. */
/* Tucked into the top-right corner, out of the drawing's way. It is a key hint
   rather than a caption, so it sits where an index number sits: findable without
   being looked at, and never competing with the part below it. Absolute, so it
   costs the chip no height and the drawing keeps the whole face. */
.part-key b { position: absolute; top: 4px; right: 5px; color: var(--part); font-family: var(--ui); font-size: 0.62rem; font-weight: 900; line-height: 1; letter-spacing: 0.04em; }
/* The drawing is what gives when the row runs out of room: min-width lets it
   shrink below its ideal size, and the aspect ratio keeps it square while it
   does, so six parts squash rather than wrap into an unreadable stack. The
   margin is the chip's padding, carried here so the number band below can still
   reach the edges. */
/* With the number lifted into the corner the drawing has the chip's whole face,
   so it takes it: the part is what is actually being chosen between, and at the
   old size two similar components were told apart by squinting. */
.part-key svg { display: block; width: clamp(52px, 7vh, 96px); min-width: 0; height: auto; margin: 7px 9px 6px; aspect-ratio: 1; }
.part-key svg path { fill: none; stroke-width: 5.5; stroke-linecap: round; stroke-linejoin: round; }
/* The rest of the character behind the part: thinner as well as fainter, so it
   reads as the outline of where the part sits and never as a candidate itself. */
.part-key svg path.s-ghost { stroke: var(--muted); stroke-width: 4; opacity: 0.22; }

/* One colour per part, shared by the legend chip and the strokes it names. */
/* Each part's colour named once, then spent in three small places rather than as
   a filled block: the strokes, the digit, and a rule across the foot of the
   chip. A solid band of it under every part was more colour than the job needs —
   the tie between chip and strokes only has to be recognised, not announced. */
.s-p1 { --part: #6ea9ff; }
.s-p2 { --part: #4fd7ad; }
.s-p3 { --part: #b892ff; }
.s-p4 { --part: #f0b45c; }
.s-p5 { --part: #86d46a; }
.s-p6 { --part: #ff8fb0; }
.part-key svg path, .kanji-svg path.s-p1, .kanji-svg path.s-p2, .kanji-svg path.s-p3,
.kanji-svg path.s-p4, .kanji-svg path.s-p5, .kanji-svg path.s-p6 { stroke: var(--part); }

.radical-pick { width: min(100%, 780px); margin: 0; padding: 20px 22px; text-align: center; background: rgba(85, 164, 255, 0.07); border: 1px solid rgba(85, 164, 255, 0.26); }
.radical-pick-help { margin: 0; color: var(--muted); font-size: 0.8rem; line-height: 1.6; }
.radical-pick-help em { display: block; margin-top: 3px; color: #7f8ca2; font-size: 0.92em; font-style: normal; }
.radical-pick kbd { display: inline-block; min-width: 1.4em; color: var(--blue-deep); font-family: inherit; font-weight: 700; text-align: center; }
.radical-pick-note { margin: 8px 0 0; color: #d8b880; font-size: 0.75rem; line-height: 1.45; }
.radical-pick-note[hidden] { display: none; }
/* The reserved line kept the box from jumping when a verdict arrived, but it is
   empty on every pick that goes right — a blank row under the chips on the
   common case to spare a nudge on the rare one. It collapses when there is
   nothing in it, and the stage re-centres the box if it grows. */
.radical-pick-feedback { min-height: 1.2em; margin: 10px 0 0; font-size: 0.79rem; font-weight: 800; }
.radical-pick-feedback:empty { min-height: 0; margin: 0; }
.radical-pick-feedback.incorrect { color: var(--red); }

.radical-detail { width: min(100%, 620px); margin: 8px 0 10px; padding: 13px 15px; text-align: left; background: rgba(255, 161, 77, 0.06); border: 1px solid rgba(255, 161, 77, 0.24); }
.radical-forms { display: flex; flex-wrap: wrap; gap: 7px; margin-top: 8px; }
.radical-forms .form-example.is-this-form { border-color: var(--orange); box-shadow: 0 0 0 1px rgba(255, 161, 77, 0.35), 0 0 18px rgba(255, 161, 77, 0.2); }
.form-context { width: min(100%, 560px); margin: 8px 0 10px; padding: 13px 15px; text-align: left; background: rgba(255, 161, 77, 0.06); border: 1px solid rgba(255, 161, 77, 0.24); }
.form-context-title { margin: 0; color: var(--orange); font-size: 0.65rem; font-weight: 850; letter-spacing: 0.1em; text-transform: uppercase; }
.form-parent { margin: 6px 0 9px; color: var(--muted); font-family: var(--jp-serif); font-size: 0.83rem; }
.form-examples { display: flex; flex-wrap: wrap; gap: 7px; }
.form-example { display: inline-flex; align-items: center; gap: 6px; padding: 6px 9px; color: var(--ink); background: rgba(255, 255, 255, 0.055); border: 1px solid var(--line); font-family: var(--jp-serif); }
.form-fragment { display: inline-grid; min-width: 1.5em; place-items: center; color: var(--orange); background: var(--orange-pale); font-size: 1.07rem; font-weight: 800; }
.form-in { color: var(--muted); font-family: var(--ui); font-size: 0.72rem; }
.form-kanji { font-size: 1.25rem; }
.form-context-note { margin: 9px 0 0; color: var(--muted); font-size: 0.71rem; line-height: 1.4; }

/* The console the answers are typed into: a darker well under the lit stage, so
   the character is what the eye lands on and the boxes are where the hands go. */
.answer-form { width: min(100%, 780px); margin-top: 0; padding: 18px 20px 16px; background: rgba(5, 9, 17, 0.5); border: 1px solid var(--line); }
.flashcard.is-form-card .answer-form { width: min(100%, 1040px); }
.flashcard.is-partial-card .answer-form { width: min(100%, 1040px); }
/* Wrapping and centred rather than a fixed three columns. A card asks for two
   fields, three or five depending on its deck, and hidden fields are removed
   from layout — so a fixed grid left the two-field cards sitting in columns one
   and two with a dead column beside them, and the last two of five hanging off
   the left. Centring makes every count sit under the middle of the card. */
.answer-fields { display: flex; flex-wrap: wrap; justify-content: center; gap: 13px; }
.answer-field { display: flex; min-width: 0; flex: 1 1 clamp(160px, 29%, 300px); flex-direction: column; text-align: left; }
.answer-field[hidden], .field-answer[hidden] { display: none !important; }
.answer-field label { display: flex; min-height: 3.2em; align-items: flex-end; margin-bottom: 8px; color: var(--muted); font-size: 0.67rem; font-weight: 800; letter-spacing: 0.075em; text-transform: uppercase; }
.answer-field input { width: 100%; min-width: 0; height: 56px; padding: 10px 14px; color: var(--ink); background: rgba(255, 255, 255, 0.06); border: 1px solid rgba(255, 255, 255, 0.16); font-family: var(--jp-sans); font-size: 1.25rem; transition: border-color 140ms ease, box-shadow 140ms ease, background 140ms ease; }
.answer-field input:focus { outline: none; background: rgba(255, 255, 255, 0.1); border-color: var(--blue); box-shadow: 0 0 0 3px rgba(85, 164, 255, 0.2), 0 0 22px rgba(85, 164, 255, 0.22); }
.answer-field input:disabled { opacity: 0.5; }
/* The box itself answers back. The verdict already prints under the field, but
   a line of text below the cursor is not where the eye is — the border is. */
.answer-field:has(.field-feedback.correct) input { border-color: rgba(79, 224, 166, 0.75); box-shadow: 0 0 0 3px rgba(79, 224, 166, 0.15), 0 0 20px rgba(79, 224, 166, 0.2); }
.answer-field:has(.field-feedback.incorrect) input { border-color: rgba(255, 130, 114, 0.75); box-shadow: 0 0 0 3px rgba(255, 130, 114, 0.15); animation: shake 240ms ease; }

@keyframes shake {
  20% { transform: translateX(-5px); }
  50% { transform: translateX(4px); }
  80% { transform: translateX(-2px); }
}
/* The "empty if none" placeholder shows on every word card, never only on the
   rootless ones — a hint that appeared just for those would give the answer
   away before it was recalled. It lives in the input rather than the label
   because the label is a flex row, where a second child stacks beside the
   text instead of running on from it. */
.answer-field input::placeholder { color: #6c7a90; font-size: 0.78rem; }

.field-feedback { min-height: 1.2em; margin: 8px 0 0; font-size: 0.76rem; line-height: 1.35; }
/* Folded behind the ⓘ, so it needs no reserved line of its own — the info body
   already spaces it, and a min-height here would pad the open panel. */
.kana-help { margin: 0; color: var(--muted); font-size: 0.76rem; line-height: 1.35; }
.field-feedback { font-weight: 800; }
.field-feedback.correct { color: var(--green); }
.field-feedback.incorrect { color: var(--red); }
.field-answer { margin-top: 8px; padding: 9px 11px; background: rgba(255, 161, 77, 0.09); border: 1px solid rgba(255, 161, 77, 0.28); }
.field-answer span, .field-answer strong { display: block; }
.field-answer span { color: var(--muted); font-size: 0.63rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; }
.field-answer strong { margin-top: 3px; color: #ffd0a0; font-family: var(--jp-sans); font-size: 1rem; }

/* Three lamps, not a sentence: how many clean passes this card has banked is
   the one number worth reading mid-card, and dots are read without stopping. */
.streak-status { display: inline-flex; align-items: center; gap: 10px; margin: 0; padding: 7px 14px; color: var(--green); background: rgba(79, 224, 166, 0.09); border: 1px solid rgba(79, 224, 166, 0.28); font-size: 0.68rem; font-weight: 850; letter-spacing: 0.1em; text-transform: uppercase; }
.streak-dots { display: inline-flex; gap: 5px; }
.streak-dots i { display: block; width: 9px; height: 9px; background: rgba(255, 255, 255, 0.13); box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.12); transition: background 180ms ease, box-shadow 180ms ease; }
.streak-dots i.is-lit { background: var(--green); box-shadow: 0 0 11px rgba(79, 224, 166, 0.75); }

/* What the card has earned for good, beside what it owes this session. Styled
   as the quieter of the two: the clean passes are what the next few minutes are
   about, and the stars only move on the day they move. */
.card-stars { display: inline-flex; align-items: center; gap: 10px; margin: 8px 0 0; padding: 6px 14px; color: var(--star); background: rgba(255, 209, 102, 0.08); border: 1px solid rgba(255, 209, 102, 0.26); font-size: 0.68rem; font-weight: 850; letter-spacing: 0.1em; text-transform: uppercase; }
.card-stars:empty { display: none; }
.card-stars .star-marks { font-size: 0.85rem; letter-spacing: 0.1em; }
.card-stars[data-stars="0"] { color: var(--muted); background: rgba(255, 255, 255, 0.04); border-color: var(--line); }
.card-stars[data-stars="5"] { color: var(--green); background: rgba(79, 224, 166, 0.09); border-color: rgba(79, 224, 166, 0.28); }

.stat { display: inline-flex; flex: 0 0 auto; align-items: baseline; gap: 6px; padding: 5px 10px; background: rgba(255, 255, 255, 0.05); border: 1px solid var(--line); font-size: 0.66rem; font-weight: 800; letter-spacing: 0.07em; text-transform: uppercase; white-space: nowrap; }
.stat b { font-size: 0.95rem; font-variant-numeric: tabular-nums; }
#got-it-count { color: var(--green); }
#again-count { color: var(--red); }

/* Controls are pressable things, not links in a paragraph. The back button in
   the menu head is the same object, so leaving a level and leaving a deck look
   and land the same. */
.text-button { padding: 0; cursor: pointer; color: var(--blue); background: transparent; border: 0; font-size: inherit; font-weight: 800; }
.hud-menu-panel .text-button, .menu-head .text-button { padding: 8px 13px; color: #0b2b4b; background: linear-gradient(180deg, rgba(255, 255, 255, 0.98), rgba(255, 255, 255, 0.92)); border: 1px solid rgba(88, 166, 255, 0.6); font-size: 0.7rem; font-weight: 800; letter-spacing: 0.08em; text-transform: uppercase; transition: background 140ms ease, border-color 140ms ease, transform 140ms ease; }
.hud-menu-panel .text-button:hover, .menu-head .text-button:hover { transform: translateY(-1px); background: linear-gradient(180deg, rgba(255, 255, 255, 1), rgba(226, 242, 255, 0.97)); border-color: var(--line-lit); }

.completion-overlay { position: fixed; z-index: 20; inset: 0; display: grid; place-items: center; padding: 24px; background: rgba(3, 7, 14, 0.64); -webkit-backdrop-filter: blur(4px); backdrop-filter: blur(4px); }
.completion-overlay[hidden] { display: none; }
/* The reward beat. It arrives with a snap rather than a fade, because it is the
   answer to the thing you just did. */
.completion-popup { width: min(100%, 560px); max-height: calc(100vh - 48px); overflow-y: auto; padding: 30px; text-align: center; background: linear-gradient(180deg, #e5f2ff, #c7e4ff); border: 1px solid rgba(88, 166, 255, 0.4); color: #0d213e; box-shadow: 0 30px 90px rgba(0, 0, 0, 0.45), 0 0 70px rgba(88, 166, 255, 0.2); animation: pop-in 220ms cubic-bezier(0.2, 1.3, 0.5, 1) both; }
.completion-popup:focus { outline: none; }
.completion-popup h2 { color: #1a6ab8; font-size: clamp(1.6rem, 3.4vw, 2.2rem); font-weight: 900; letter-spacing: -0.01em; text-shadow: 0 0 26px rgba(88, 166, 255, 0.45); }
.completion-glyph { min-height: 1em; margin: 8px 0 12px; color: #165f9e; font-family: var(--jp-serif); font-size: clamp(4.2rem, 17vw, 6.1rem); line-height: 1; text-shadow: 0 0 34px rgba(88, 166, 255, 0.45); }
.completion-answer { margin: 0; color: #0d213e; font-size: 1.05rem; font-weight: 800; }
.completion-note { margin: 14px auto 0; color: var(--muted); font-size: 0.9rem; line-height: 1.5; }
.completion-source { display: inline-block; margin-top: 12px; color: var(--blue); font-size: 0.76rem; font-weight: 800; }
.completion-source[hidden] { display: none; }
.listen-replay { margin-top: -4px; padding: 10px 16px; color: #123f68; background: rgba(88, 166, 255, 0.18); border: 1px solid rgba(88, 166, 255, 0.35); font: inherit; font-size: 0.82rem; font-weight: 800; cursor: pointer; }
.listen-replay:hover, .listen-replay:focus-visible { background: rgba(85, 164, 255, 0.22); border-color: var(--blue); outline: none; }
.listen-replay[hidden] { display: none; }
.completion-radical, .completion-root { margin: 12px 0 0; padding: 10px 12px; font-size: 0.88rem; line-height: 1.45; }
.completion-radical { color: #ffc79a; background: var(--orange-pale); border: 1px solid rgba(255, 161, 77, 0.3); }
.completion-root { color: var(--blue-deep); background: rgba(85, 164, 255, 0.1); border: 1px solid rgba(85, 164, 255, 0.3); }
.completion-help { margin: 19px 0 0; color: var(--muted); font-size: 0.78rem; line-height: 1.45; }
.completion-help strong { color: var(--blue-deep); }

@keyframes pop-in {
  from { opacity: 0; transform: scale(0.9) translateY(10px); }
  to { opacity: 1; transform: none; }
}

@media (max-width: 900px) and (min-width: 621px) {
  .answer-field { flex-basis: 44%; }
}

@media (max-width: 620px) {
  .shell { width: min(100% - 20px, 1400px); padding-top: 18px; }
  .hero { padding: 12px 14px; }
  .active-profile-copy { display: none; }
  .active-profile { right: 8px; padding: 4px; }
  .profile-gate { padding: 12px; }
  .profile-panel { width: calc(100vw - 24px); padding: 22px 16px; }
  .profile-choices { gap: 10px; }
  .profile-choice { padding: 10px; }
  .profile-choice img { width: 100%; max-width: 140px; }
  .start-menu, .game { padding: 14px; }
  .card-face { min-height: 320px; padding: 18px 14px; }
  .answer-form { padding: 13px; }
  .answer-fields { gap: 9px; }
  .answer-field { flex-basis: 100%; }
  .answer-field label { min-height: 0; }
  /* Too narrow for three columns. The running totals and the status line are the
     parts that can be read again at any time, so the bar keeps what changes:
     the deck, the streak and the menu.

     The progress track becomes a hairline along the bottom edge of the whole
     bar. In the flow it was a stub of a rule under the deck name with nothing
     to align to, which read as a stray mark; pinned to the edge it is a
     progress bar for the bar itself and costs no height at all. The three lamps
     drop their label, because "CLEAN PASSES" beside them was most of the width
     spent on a caption for three dots that are already lit or not. */
  .hud { position: relative; gap: 10px; padding: 9px 12px 11px; }
  .hud-left { display: none; }
  .hud-mid { flex: 1 1 auto; min-width: 0; text-align: left; }
  .hud-mid h2 { margin: 0; font-size: 0.88rem; }
  .progress { margin-top: 2px; font-size: 0.6rem; }
  .progress-track { position: absolute; right: 0; bottom: 0; left: 0; width: auto; height: 3px; margin: 0; border: 0; box-shadow: none; }
  .hud-right { flex: 0 0 auto; gap: 8px; }
  .streak-label { display: none; }
  .streak-status { padding: 7px 10px; }
}

/* Any window too thin to hold the bar's three columns comfortably — a phone, or
   a narrow desktop window — lifts the menu out of the bar and floats it at the
   top-right of the screen, where a thumb reaches it and where it stops competing
   with the deck name for the width. It lives inside the game section, so it
   appears only while a card is up. */
@media (max-width: 860px) {
  /* The masthead is hidden while playing, so the shell's top padding and the
     game panel's own were 30px of nothing above the card on the screen with the
     least height to spare. Trimmed to the width of the frame's edge. */
  body.is-playing .shell { padding-top: 6px; }
  body.is-playing .game { padding: 8px; }
  /* Into the corner of the window. Just the icon — no border, no fill, nothing
     for it to sit inside. The bars carry a shadow so they hold up over a stroke
     of the kanji, which is the only thing that ever passes behind them. */
  /* A little further in from the right than from the top: the window's right
     edge has the frame's border running down it, and sitting level with that
     border reads as touching it. */
  .hud-menu { position: fixed; top: 5px; right: 11px; z-index: 20; }
  .hud-menu-button {
    width: 46px;
    height: 46px;
    /* Faint by default. It is reached for perhaps once a session, so it only has
       to be findable, not read — and full-strength ink in the corner of a screen
       whose whole point is the character in the middle is one bright thing too
       many. It comes up to full on hover, focus, and while open. */
    color: var(--ink);
    opacity: 0.34;
    background: none;
    border: 0;
    box-shadow: none;
    filter: drop-shadow(0 1px 3px rgba(5, 9, 17, 0.9));
    transition: opacity 160ms ease;
  }
  .hud-menu-button:hover,
  .hud-menu-button:focus-visible,
  .hud-menu-button[aria-expanded="true"] { background: none; opacity: 1; }
}

@media (max-width: 410px) {
  .streak-status { padding: 5px 9px; font-size: 0.62rem; }
  .hud-mid h2 { font-size: 0.85rem; }
}

@media (prefers-reduced-motion: reduce) {
  /* A field of things sliding across the whole viewport is the first thing to go
     when less motion has been asked for. Stopping the animation alone would park
     every glyph at its start position, off the bottom of the screen, so they are
     laid out down the page instead and simply hold still. */
  .kanji-drift span { top: var(--drift-top); bottom: auto; animation: none; }
  .deck-progress b, .progress-track i, .kanji-svg path, .streak-dots i { transition: none; }
  .deck-option, .deck-go, .deck-icon, .answer-field input, .text-button, .part-key { transition: none; }
  .part-key:hover, .part-key:active { transform: none; }
  .answer-field:has(.field-feedback.incorrect) input { animation: none; }
  .deck-option:hover { transform: none; }
  .deck-option:hover .deck-go, .deck-option:hover .deck-icon { transform: none; }
  .hud-menu-panel .text-button:hover, .menu-head .text-button:hover { transform: none; }
  .completion-popup { animation: none; }
}

.reference-libraries{position:relative;z-index:1;display:flex;align-items:center;justify-content:center;flex-wrap:wrap;gap:10px 20px;max-width:1120px;margin:24px auto 38px;padding:16px 20px;border:1px solid var(--line);border-radius:14px;background:#102a47;color:var(--ink);font:700 13px var(--ui)}
.reference-libraries strong{margin-right:auto}
.reference-libraries a{padding:8px 13px;border:1px solid #a9d3ed66;border-radius:9px;color:#e6f5ff;text-decoration:none}
.reference-libraries a:hover,.reference-libraries a:focus-visible{border-color:#f5d39a;color:#fff}
body.is-playing .reference-libraries,body.profile-locked .reference-libraries{display:none}
@media(max-width:600px){.reference-libraries{margin:16px 12px 25px}.reference-libraries strong{flex-basis:100%}.reference-libraries a{flex:1;text-align:center}}
