/* Light theme — modelled on the VS Code "Quiet Light" theme
   (bg #f5f5f5, text #333, green #2f9e44, red #aa3731). This is the
   default; the dark theme applies via prefers-color-scheme or an
   explicit [data-theme="dark"] set by the toggle. */
:root {
  color-scheme: light;
  --bg: #ece8e1;
  --card-bg: #f8f6f1;
  --text: #333230;
  --muted: #8c887f;
  --accent: #2f9e44;
  /* A second, equally neutral colour — the singular/plural (or similar
     two-way) split in a reference-panel case pair needs its own colours
     that don't read as "right" (--accent, used everywhere else for a
     correct answer) versus "wrong" (--bad). Skin-independent on purpose,
     same as --bad: the pairing only has to stay legible against itself,
     not retune with the seven skins' accents. Orange specifically, not
     blue: two of the seven skins (bubbles, ice) already use a blue/teal
     --accent, and a blue --accent2 sat on top of a blue --accent was
     indistinguishable from it ("dem" and "den" both rendering the same
     colour) — orange is the one hue none of the seven skins' accents or
     --bad go anywhere near. */
  --bad: #aa3731;
  --border: #ddd8cf;

  /* Skin knobs — the defaults below reproduce the plain "leaf" look exactly.
     Each skin (data-skin) overrides some of these to restyle shape, surface
     and background without ever touching the layout.

     Typography is deliberately NOT among them: --font-body / --font-head /
     --head-weight belong to the typeface axis alone (data-font, further down).
     Skins used to set a face each, which meant liking a skin cost you the face
     you had chosen — the one setting on this page you actually stare at for an
     hour. The two axes are now independent, like skin and light/dark. */
  --radius: 20px;        /* card corners */
  --radius-sm: 14px;     /* buttons / controls */
  --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --font-head: var(--font-body);   /* the headword */
  --head-weight: 700;              /* headword weight (the typeface may lighten) */
  --card-tint: var(--card-bg);     /* card surface (skins may tint / go translucent) */
  --card-blur: 0px;                /* frosted-glass backdrop for translucent skins */
  --card-border-w: 1px;
  --card-shadow: 0 8px 30px rgba(0, 0, 0, 0.08);
  --page-bg: var(--bg);            /* body backdrop (skins may paint a gradient) */
}

/* Dark theme values, shared by system-preference and manual selection. */
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    color-scheme: dark;
    --bg: #141517;
    --card-bg: #282a2f;
    --text: #ececec;
    --muted: #9a9a9a;
    --accent: #2e8b3e;
    --bad: #e07070;
    --border: #40434a;
  }
}

:root[data-theme="dark"] {
  color-scheme: dark;
  --bg: #141517;
  --card-bg: #282a2f;
  --text: #ececec;
  --muted: #9a9a9a;
  --accent: #2e8b3e;
  --bad: #e07070;
  --border: #40434a;
}

/* ----------------------------------------------------------------- design --
   A fourth independent axis alongside skin (colour), font and light/dark:
   `data-design` on <html>, same JS pattern as `data-skin` (see DESIGNS /
   initDesignPicker in app-src/13-controls-marks-wiring.js). The plain :root
   defaults right below are the bare fallback (no data-design attribute) —
   no longer a selectable Design itself, just the base every real Design's
   own [data-design="x"] block builds on top of.

   A future design restyles the same knobs a skin restyles (--radius,
   --card-tint/--card-blur/--card-border-w, --card-shadow, --page-bg) plus
   whatever structural chrome its own look calls for (border weight/style,
   decorative overlays, panel chrome), and — like a skin — MUST NOT set
   --font-body/--font-head/--head-weight. Typography stays the reader's own
   axis regardless of which design or skin is active; a design that wants a
   signature face offers it as an ordinary addition to the Font picker's own
   list (see FONTS in 13-controls-marks-wiring.js), never as something
   switching Design silently changes underneath the reader.

   A design's own accent colour(s), if it needs more than the shared
   --accent/--accent2 pair, get their own --accent-3/--accent-4/--accent-5
   custom properties scoped inside that design's own [data-design="x"] block
   — introduced by the design that first needs them, not pre-declared here
   for designs that don't exist in this file yet. */

/* The 17 named "skins" that used to live here (each its own accent PLUS its
   own shape/surface/backdrop personality) are retired — every Design now
   uses the same shared accent-slot picker (see DESIGNS in
   13-controls-marks-wiring.js). The bare fallback (no data-design, no
   data-skin) keeps one fixed shape/surface look of its own — the plain
   :root defaults right below, exactly what "leaf" already rendered before
   this. Their 17 accent colours live on as ordinary swatches in the shared
   reuses the very same hex values and translated names). */

/* ---- designs --------------------------------------------------------
   A third axis alongside font and light/dark (skin, formerly a fourth, is
   retired — see the comment just above): restyles shape/surface/backdrop,
   mixing against the shared --bg/--card-bg rather than replacing them so
   it reads in both light and dark for free, and never touches typography.

   Colour is a separate axis again, not bundled into which design you
   picked: every design shares ONE swatch-to-colour mapping and ONE actual
   selection — `data-a1`/`data-a2` on <html> carry whichever shared swatch
   is currently picked for the Primary/Secondary slot, the block right
   below maps every swatch key to --accent/--accent2 unscoped by
   data-design, and switching design on its own never touches either
   attribute (see applyDesign(), 13-controls-marks-wiring.js — it changes
   shape, not colour). A design's own block underneath only ever needs its
   shape/surface/backdrop knobs plus which two swatch keys it *defaults*
   to the first time anyone ever opens it — never a fresh set of colour
   rules. Most swatches here are simply the 17 former skins' own accents
   (already translated as themeCoral, themeIce, etc.) plus the newer pastel
   additions (palettePeach etc.) — reusing rather than duplicating either
   set. */
:root[data-a1="coral"]    { --accent: #ff6b4a; }
:root[data-a1="sunset"]   { --accent: #e8590c; }
:root[data-a1="amber"]    { --accent: #f08c00; }
:root[data-a1="leaf"]     { --accent: #2f9e44; }
:root[data-a1="aurora"]   { --accent: #0ca678; }
:root[data-a1="mint"]     { --accent: #12b886; }
:root[data-a1="ice"]      { --accent: #1098ad; }
:root[data-a1="lagoon"]   { --accent: #0c8599; }
:root[data-a1="bubbles"]  { --accent: #228be6; }
:root[data-a1="sapphire"] { --accent: #1c7ed6; }
:root[data-a1="indigo"]   { --accent: #3b5bdb; }
:root[data-a1="crystal"]  { --accent: #7950f2; }
:root[data-a1="disco"]    { --accent: #be4bdb; }
:root[data-a1="plum"]     { --accent: #9c36b5; }
:root[data-a1="wine"]     { --accent: #c2255c; }
:root[data-a1="sakura"]   { --accent: #e64980; }
:root[data-a1="slate"]    { --accent: #495057; }
:root[data-a1="peach"]    { --accent: #f2a65a; }
:root[data-a1="apricot"]  { --accent: #f6c36b; }
:root[data-a1="blossom"]  { --accent: #e8879e; }
:root[data-a1="orchid"]   { --accent: #b98bd4; }
:root[data-a1="lilac"]    { --accent: #c9b6e4; }
:root[data-a1="sky"]      { --accent: #8fb8e0; }
:root[data-a1="butter"]   { --accent: #f3d98b; }
:root[data-a1="cherry"]    { --accent: #e0263f; }
:root[data-a1="terracotta"] { --accent: #c1440e; }
:root[data-a1="tangelo"]   { --accent: #ff9248; }
:root[data-a1="mango"]     { --accent: #ffb347; }
:root[data-a1="marigold"]  { --accent: #f5a623; }
:root[data-a1="honey"]     { --accent: #e0a72e; }
:root[data-a1="cream"]     { --accent: #f8e3a3; }
:root[data-a1="fern"]      { --accent: #4f9d3d; }
:root[data-a1="sage"]      { --accent: #87a878; }
:root[data-a1="emerald"]   { --accent: #0d9b6c; }
:root[data-a1="turquoise"] { --accent: #0fb5ae; }
:root[data-a1="teal"]      { --accent: #087f8c; }
:root[data-a1="azure"]     { --accent: #339af0; }
:root[data-a1="cerulean"]  { --accent: #1971c2; }
:root[data-a1="powder"]    { --accent: #a7d8f0; }
:root[data-a1="cobalt"]    { --accent: #2b4bd1; }
:root[data-a1="periwinkle"] { --accent: #8c9eff; }
:root[data-a1="amethyst"]  { --accent: #8e5fd6; }
:root[data-a1="lavender"]  { --accent: #d8cbf0; }
:root[data-a1="magenta"]   { --accent: #d6249a; }
:root[data-a1="mauve"]     { --accent: #a374a8; }
:root[data-a1="fuchsia"]   { --accent: #d6336c; }
:root[data-a1="rose"]      { --accent: #f2739e; }
:root[data-a1="petal"]     { --accent: #f6c6d6; }
:root[data-a1="fog"]       { --accent: #868e96; }
/* Dark-mode variants — same hue/saturation as the light-mode swatch above,
   own lightness. Same convention as the skins' own
   [data-theme="dark"][data-skin="x"] blocks above (plus the matching
   prefers-color-scheme block for a device that hasn't been explicitly
   toggled). Current lightness values are a flat ~28% relative HSL-L cut off
   an earlier, brighter generation of these same variants (user feedback,
   2026-08-25: "toooooo bright" against the dark background) — a saturated
   hue at this L still contrasts fine against the near-black page, so this
   was free to drop below the light-mode swatch's own L where the earlier
   generation had stayed above it for headroom that turned out not to be
   needed. */
:root[data-theme="dark"][data-a1="coral"]    { --accent: #ff5e5e; }
:root[data-theme="dark"][data-a1="sunset"]   { --accent: #d76800; }
:root[data-theme="dark"][data-a1="amber"]    { --accent: #ef9000; }
:root[data-theme="dark"][data-a1="leaf"]     { --accent: #2e8b3e; }
:root[data-theme="dark"][data-a1="aurora"]   { --accent: #20a57d; }
:root[data-theme="dark"][data-a1="mint"]     { --accent: #21cc98; }
:root[data-theme="dark"][data-a1="ice"]      { --accent: #1f9aa9; }
:root[data-theme="dark"][data-a1="lagoon"]   { --accent: #188495; }
:root[data-theme="dark"][data-a1="bubbles"]  { --accent: #0a80df; }
:root[data-theme="dark"][data-a1="sapphire"] { --accent: #0e71c3; }
:root[data-theme="dark"][data-a1="indigo"]   { --accent: #0f3efa; }
:root[data-theme="dark"][data-a1="crystal"]  { --accent: #4c12f6; }
:root[data-theme="dark"][data-a1="disco"]    { --accent: #c11be9; }
:root[data-theme="dark"][data-a1="plum"]     { --accent: #a91dcd; }
:root[data-theme="dark"][data-a1="wine"]     { --accent: #e0165c; }
:root[data-theme="dark"][data-a1="sakura"]   { --accent: #f12069; }
:root[data-theme="dark"][data-a1="slate"]    { --accent: #758290; }
:root[data-theme="dark"][data-a1="peach"]    { --accent: #ee8717; }
:root[data-theme="dark"][data-a1="apricot"]  { --accent: #f2a724; }
:root[data-theme="dark"][data-a1="blossom"]  { --accent: #e13f68; }
:root[data-theme="dark"][data-a1="orchid"]   { --accent: #9952c5; }
:root[data-theme="dark"][data-a1="lilac"]    { --accent: #946bce; }
:root[data-theme="dark"][data-a1="sky"]      { --accent: #4b90d5; }
:root[data-theme="dark"][data-a1="butter"]   { --accent: #ecc13b; }
:root[data-theme="dark"][data-a1="cherry"] { --accent: #b31a2e; }
:root[data-theme="dark"][data-a1="terracotta"] { --accent: #97350b; }
:root[data-theme="dark"][data-a1="tangelo"] { --accent: #ff6700; }
:root[data-theme="dark"][data-a1="mango"] { --accent: #fe9500; }
:root[data-theme="dark"][data-a1="marigold"] { --accent: #d18609; }
:root[data-theme="dark"][data-a1="honey"] { --accent: #b7851b; }
:root[data-theme="dark"][data-a1="cream"] { --accent: #f2c94f; }
:root[data-theme="dark"][data-a1="fern"] { --accent: #3e7a30; }
:root[data-theme="dark"][data-a1="sage"] { --accent: #678958; }
:root[data-theme="dark"][data-a1="emerald"] { --accent: #0c8d62; }
:root[data-theme="dark"][data-a1="turquoise"] { --accent: #0c8d88; }
:root[data-theme="dark"][data-a1="teal"] { --accent: #088391; }
:root[data-theme="dark"][data-a1="azure"] { --accent: #107ad3; }
:root[data-theme="dark"][data-a1="cerulean"] { --accent: #135897; }
:root[data-theme="dark"][data-a1="powder"] { --accent: #5ab6e3; }
:root[data-theme="dark"][data-a1="cobalt"] { --accent: #223aa3; }
:root[data-theme="dark"][data-a1="periwinkle"] { --accent: #3555ff; }
:root[data-theme="dark"][data-a1="amethyst"] { --accent: #6a31c0; }
:root[data-theme="dark"][data-a1="lavender"] { --accent: #9f7fda; }
:root[data-theme="dark"][data-a1="magenta"] { --accent: #a71c78; }
:root[data-theme="dark"][data-a1="mauve"] { --accent: #835588; }
:root[data-theme="dark"][data-a1="fuchsia"] { --accent: #ac2353; }
:root[data-theme="dark"][data-a1="rose"] { --accent: #eb2b6c; }
:root[data-theme="dark"][data-a1="petal"] { --accent: #e97299; }
:root[data-theme="dark"][data-a1="fog"] { --accent: #676f77; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="coral"]    { --accent: #ff5e5e; }
  :root:not([data-theme])[data-a1="sunset"]   { --accent: #d76800; }
  :root:not([data-theme])[data-a1="amber"]    { --accent: #ef9000; }
  :root:not([data-theme])[data-a1="leaf"]     { --accent: #2e8b3e; }
  :root:not([data-theme])[data-a1="aurora"]   { --accent: #20a57d; }
  :root:not([data-theme])[data-a1="mint"]     { --accent: #21cc98; }
  :root:not([data-theme])[data-a1="ice"]      { --accent: #1f9aa9; }
  :root:not([data-theme])[data-a1="lagoon"]   { --accent: #188495; }
  :root:not([data-theme])[data-a1="bubbles"]  { --accent: #0a80df; }
  :root:not([data-theme])[data-a1="sapphire"] { --accent: #0e71c3; }
  :root:not([data-theme])[data-a1="indigo"]   { --accent: #0f3efa; }
  :root:not([data-theme])[data-a1="crystal"]  { --accent: #4c12f6; }
  :root:not([data-theme])[data-a1="disco"]    { --accent: #c11be9; }
  :root:not([data-theme])[data-a1="plum"]     { --accent: #a91dcd; }
  :root:not([data-theme])[data-a1="wine"]     { --accent: #e0165c; }
  :root:not([data-theme])[data-a1="sakura"]   { --accent: #f12069; }
  :root:not([data-theme])[data-a1="slate"]    { --accent: #758290; }
  :root:not([data-theme])[data-a1="peach"]    { --accent: #ee8717; }
  :root:not([data-theme])[data-a1="apricot"]  { --accent: #f2a724; }
  :root:not([data-theme])[data-a1="blossom"]  { --accent: #e13f68; }
  :root:not([data-theme])[data-a1="orchid"]   { --accent: #9952c5; }
  :root:not([data-theme])[data-a1="lilac"]    { --accent: #946bce; }
  :root:not([data-theme])[data-a1="sky"]      { --accent: #4b90d5; }
  :root:not([data-theme])[data-a1="butter"]   { --accent: #ecc13b; }
  :root:not([data-theme])[data-a1="cherry"] { --accent: #b31a2e; }
  :root:not([data-theme])[data-a1="terracotta"] { --accent: #97350b; }
  :root:not([data-theme])[data-a1="tangelo"] { --accent: #ff6700; }
  :root:not([data-theme])[data-a1="mango"] { --accent: #fe9500; }
  :root:not([data-theme])[data-a1="marigold"] { --accent: #d18609; }
  :root:not([data-theme])[data-a1="honey"] { --accent: #b7851b; }
  :root:not([data-theme])[data-a1="cream"] { --accent: #f2c94f; }
  :root:not([data-theme])[data-a1="fern"] { --accent: #3e7a30; }
  :root:not([data-theme])[data-a1="sage"] { --accent: #678958; }
  :root:not([data-theme])[data-a1="emerald"] { --accent: #0c8d62; }
  :root:not([data-theme])[data-a1="turquoise"] { --accent: #0c8d88; }
  :root:not([data-theme])[data-a1="teal"] { --accent: #088391; }
  :root:not([data-theme])[data-a1="azure"] { --accent: #107ad3; }
  :root:not([data-theme])[data-a1="cerulean"] { --accent: #135897; }
  :root:not([data-theme])[data-a1="powder"] { --accent: #5ab6e3; }
  :root:not([data-theme])[data-a1="cobalt"] { --accent: #223aa3; }
  :root:not([data-theme])[data-a1="periwinkle"] { --accent: #3555ff; }
  :root:not([data-theme])[data-a1="amethyst"] { --accent: #6a31c0; }
  :root:not([data-theme])[data-a1="lavender"] { --accent: #9f7fda; }
  :root:not([data-theme])[data-a1="magenta"] { --accent: #a71c78; }
  :root:not([data-theme])[data-a1="mauve"] { --accent: #835588; }
  :root:not([data-theme])[data-a1="fuchsia"] { --accent: #ac2353; }
  :root:not([data-theme])[data-a1="rose"] { --accent: #eb2b6c; }
  :root:not([data-theme])[data-a1="petal"] { --accent: #e97299; }
  :root:not([data-theme])[data-a1="fog"] { --accent: #676f77; }
}

/* Every design below sets --font-head (its signature display face for the
   headword and, where a pseudo-element title bar is drawn, that bar's own
   label) and sometimes --font-body too, when the face is still comfortably
   legible at paragraph length. The fallback chain after the webfont name is
   always a full system stack: a browser substitutes only the individual
   *glyphs* a decorative face can't render, so a Cyrillic/Greek/CJK/Thai
   headword quietly drops to that fallback per character while a Latin one
   keeps the full display face — no target language is blocked from any
   Design. This is the same reasoning the typeface section below already
   documents for --font-face's own Noto chains, just simpler (Designs don't
   need per-script genre matching the way a body reading face does). --font-
   body is left alone entirely wherever the display face would hurt reading
   speed at length. Placed before
   the typeface section on purpose — see that section's own comment for why
   an explicit Font pick there always wins over whatever a Design suggests. */
:root {
  --sys-sans: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif;
  --sys-serif: Charter, "Iowan Old Style", Georgia, Cambria, "Times New Roman", serif;
  --sys-mono: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
}

/* Every design below also sets its OWN --bg / --card-bg / --text / --muted /
   --border — a full base palette, not just an accent layered on the bare
   fallback's shared grey. This is the one deliberate break from how the 17
   skins work (they only ever retune shape + accent, by design, so liking a
   skin never costs you the base you started from) — a Design is a different
   visual world, not a recolour of this one, and reusing the bare fallback's
   own bg/card/ink underneath every one of them is exactly what made them all
   read as "the same, just a different shade" despite the shape and accent
   work. Each
   gets an explicit light block (below) and a paired dark block —
   :root[data-theme="dark"][data-design="x"] plus the matching
   prefers-color-scheme media query for a device that hasn't been
   explicitly toggled — same convention the skins' own dark accent
   overrides already use one section down. */

/* Pastel Sky — a soft gradient wash between the two accents themselves, so
   Colour genuinely restyles the whole backdrop, not just the cards. Baloo 2:
   chunky and rounded, matches the pill-shaped, weightless mood.
   --card-shadow here (and Ember's own, just below) used to tint its glow
   from color-mix(var(--accent...)), which put a soft accent-coloured halo
   around every single card/panel -- the same "background one colour, edge
   another" look the outline-only-on-Herbarium pass (user feedback) was
   about, just blurred instead of a hard line. First neutralised to a
   plain rgba(0,0,0,...) shadow, then dropped to "none" outright on both
   (user feedback: no shadowing at all outside Herbarium) -- matching
   Herbarium's own already-"none" value exactly. --page-bg's own
   accent-tinted wash is left alone -- that is the whole-canvas backdrop,
   not a per-component edge. */
:root[data-design="pastelsky"] {
  --bg: #eef1fb; --card-bg: #ffffff; --text: #33324a; --muted: #83829c; --border: rgba(255, 255, 255, 0.6);
  --radius: 30px; --radius-sm: 20px;
  --card-tint: color-mix(in srgb, var(--card-bg) 72%, transparent);
  --card-blur: 20px;
  --card-shadow: none;
  --page-bg: linear-gradient(135deg,
    color-mix(in srgb, var(--accent) 26%, var(--bg)),
    color-mix(in srgb, var(--accent) 22%, var(--bg)) 60%,
    var(--bg));
  --font-body: "Baloo 2", var(--sys-sans);
  --font-head: var(--font-body);
  --head-weight: 700;
}
:root[data-theme="dark"][data-design="pastelsky"] {
  --bg: #20213e; --card-bg: #262236; --text: #f1eef6; --muted: #b3aec4; --border: rgba(255, 255, 255, 0.12);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-design="pastelsky"] {
    --bg: #20213e; --card-bg: #262236; --text: #f1eef6; --muted: #b3aec4; --border: rgba(255, 255, 255, 0.12);
  }
}

/* Ember Pulse — a warm glow rising from below rather than from the top
   corners, fitness-tracker energy. Sora: tight, bold, high-energy. The
   primary verdict button carries the reference's gradient bar directly. */
:root[data-design="ember"] {
  --bg: #fbf2f4; --card-bg: #ffffff; --text: #2a1620; --muted: #8a7580; --border: #f0dce2;
  --radius: 16px; --radius-sm: 10px;
  --card-tint: var(--card-bg);
  --card-blur: 0px;
  --card-shadow: none;
  --page-bg: radial-gradient(900px 520px at 50% 112%, color-mix(in srgb, var(--accent) 10%, var(--bg)), var(--bg));
  --font-body: "Sora", var(--sys-sans);
  --font-head: var(--font-body);
  --head-weight: 700;
}
:root[data-theme="dark"][data-design="ember"] {
  --bg: #120a14; --card-bg: #1c1220; --text: #f5eef0; --muted: #b39fae;
  --border: rgba(255, 255, 255, 0.14);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-design="ember"] {
    --bg: #120a14; --card-bg: #1c1220; --text: #f5eef0; --muted: #b39fae;
    --border: rgba(255, 255, 255, 0.14);
  }
}
[data-design="ember"] #btnKnown {
  background: linear-gradient(135deg, var(--accent), var(--accent)); border-color: transparent; color: #fff;
}

/* Editorial Botanica — quiet print-magazine restraint: no shadow at all,
   near-square corners. Cormorant Garamond on the headword (the reference's
   own display serif) paired with Lora for reading text — already loaded in
   this file for target-phrase italics, so this just extends an existing
   face to the rest of the page instead of introducing a second serif. */
:root[data-design="editorial"] {
  --bg: #fbf9f4; --card-bg: #ffffff; --text: #1c1b19; --muted: #8b8578; --border: rgba(28, 27, 25, 0.16);
  --radius: 2px; --radius-sm: 2px;
  --card-tint: var(--card-bg);
  --card-blur: 0px;
  --card-shadow: none;
  --page-bg: var(--bg);
  --font-body: "Lora", var(--sys-serif);
  --font-head: "Cormorant Garamond", var(--font-body);
  --head-weight: 600;
}
:root[data-theme="dark"][data-design="editorial"] {
  --bg: #1b1a17; --card-bg: #232220; --text: #f3f0e7; --muted: #a39c8c; --border: rgba(243, 240, 231, 0.14);
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-design="editorial"] {
    --bg: #1b1a17; --card-bg: #232220; --text: #f3f0e7; --muted: #a39c8c; --border: rgba(243, 240, 231, 0.14);
  }
}
[data-design="editorial"] .gTopic h2 {
  text-transform: uppercase; letter-spacing: 0.10em;
}
/* Herbarium's "near-square corners" (--radius/--radius-sm above) only
   reaches the handful of controls that actually consume those two
   variables -- the great majority of pills/chips/icon-circles/panels in
   this file hardcode their own border-radius (999px, 50%, or a fixed px)
   entirely independent of the design system, so the variable alone left
   dozens of round shapes untouched. One blanket reset, rather than
   hunting down and re-deriving every one of them individually (and
   missing some, the way the variable already did) -- !important because
   plenty of those hardcoded rules (IDs, :hover/.open states) outrank a
   plain attribute-plus-universal selector on specificity alone. */
[data-design="editorial"] * {
  border-radius: 0 !important;
}
/* The outline/framing look (a visible border line at all, any colour) is
   Herbarium's own signature -- every other design should show NONE,
   full stop. Recolouring --border to a neutral tone (see the design
   blocks below) still left a visible line, just a quieter one -- that
   was never the actual ask. Same blanket-reset idiom as the border-
   radius rule just above, inverted: every design EXCEPT editorial gets
   its border-color killed outright, on every element, everywhere
   (cards, buttons, dropdowns, chips, panels, table rules -- all of it).
   border-color, not border/border-width, so the box model (and anything
   sized or positioned against a border's width) doesn't shift -- the
   line itself just becomes invisible. !important for the same reason
   the radius reset needs it: plenty of the individual border rules this
   overrides (IDs, :hover/.open states, table cells) outrank a plain
   :not()-plus-universal selector on specificity alone. */
:root:not([data-design="editorial"]) * {
  border-color: transparent !important;
}


/* ---- background patterns -------------------------------------------------
   The Background picker's 35 options — a fourth axis, orthogonal to Design.
   Every rule below targets #bgPattern (the fixed layer that is <body>'s
   first child, see index.html) via `:root[data-bgpattern="key"]`, plus a
   second, independent rule targeting `[data-bgswatch="key"]` — the picker's
   own icon-only buttons, each showing its OWN pattern regardless of which
   one is currently applied (custom properties inherit from :root, a plain
   attribute match on the button itself does not, so the two never collide).
   Two constructions:
     - A plain CSS gradient (dots, grids, stripes, checks) painted straight
       onto background-image, tinted inline via color-mix(var(--accent)).
     - An inline SVG used as a `mask-image` over a solid
       `background-color: color-mix(var(--accent) …)` — the only way an
       arbitrary vector motif (a leaf, a star, a snowflake) can still be
       tinted live from the current accent, since a data-URI SVG is its own
       isolated document and cannot itself read a CSS custom property. Every
       mask SVG is drawn in flat `white` (full reveal).
   The swatch versions of both constructions reuse the exact same gradient
   or SVG, just stronger and tighter-tiled, so a 30px button still reads at
   a glance; the live page versions stay closer to Blueprint's original
   dot-grid subtlety. */
.ldSwatchBtn.bgSwatchBtn { width: 22px; height: 22px; border-radius: 6px; box-shadow: none; }
.bgSwatchBtn.bgSwatchNone {
  background-image: linear-gradient(45deg, transparent 46%, var(--muted) 46% 54%, transparent 54%);
}

/* Grids & Lines — plain CSS gradients, no image data at all. */
:root[data-bgpattern="dotgrid"] #bgPattern {
  background-image: radial-gradient(circle, color-mix(in srgb, var(--accent) 14%, transparent) 1.3px, transparent 1.3px);
  background-size: 22px 22px;
}
[data-bgswatch="dotgrid"] {
  background-image: radial-gradient(circle, color-mix(in srgb, var(--accent) 55%, transparent) 1.6px, transparent 1.6px);
  background-size: 12px 12px;
}
:root[data-bgpattern="graphpaper"] #bgPattern {
  background-image:
    repeating-linear-gradient(0deg, color-mix(in srgb, var(--accent) 10%, transparent) 0 1px, transparent 1px 26px),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--accent) 10%, transparent) 0 1px, transparent 1px 26px);
  background-size: 26px 26px;
}
[data-bgswatch="graphpaper"] {
  background-image:
    repeating-linear-gradient(0deg, color-mix(in srgb, var(--accent) 45%, transparent) 0 1.2px, transparent 1.2px 14px),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--accent) 45%, transparent) 0 1.2px, transparent 1.2px 14px);
  background-size: 14px 14px;
}
:root[data-bgpattern="diagonallines"] #bgPattern {
  background-image: repeating-linear-gradient(45deg, color-mix(in srgb, var(--accent) 12%, transparent) 0 2px, transparent 2px 14px);
  background-size: 14px 14px;
}
[data-bgswatch="diagonallines"] {
  background-image: repeating-linear-gradient(45deg, color-mix(in srgb, var(--accent) 55%, transparent) 0 2px, transparent 2px 9px);
  background-size: 9px 9px;
}
:root[data-bgpattern="crosshatch"] #bgPattern {
  background-image:
    repeating-linear-gradient(45deg, color-mix(in srgb, var(--accent) 11%, transparent) 0 1.4px, transparent 1.4px 16px),
    repeating-linear-gradient(-45deg, color-mix(in srgb, var(--accent) 11%, transparent) 0 1.4px, transparent 1.4px 16px);
  background-size: 16px 16px;
}
[data-bgswatch="crosshatch"] {
  background-image:
    repeating-linear-gradient(45deg, color-mix(in srgb, var(--accent) 50%, transparent) 0 1.4px, transparent 1.4px 10px),
    repeating-linear-gradient(-45deg, color-mix(in srgb, var(--accent) 50%, transparent) 0 1.4px, transparent 1.4px 10px);
  background-size: 10px 10px;
}
:root[data-bgpattern="checkerboard"] #bgPattern {
  background-image: conic-gradient(from 0deg, color-mix(in srgb, var(--accent) 8%, transparent) 90deg, transparent 90deg 180deg, color-mix(in srgb, var(--accent) 8%, transparent) 180deg 270deg, transparent 270deg);
  background-size: 24px 24px;
}
[data-bgswatch="checkerboard"] {
  background-image: conic-gradient(from 0deg, color-mix(in srgb, var(--accent) 45%, transparent) 90deg, transparent 90deg 180deg, color-mix(in srgb, var(--accent) 45%, transparent) 180deg 270deg, transparent 270deg);
  background-size: 14px 14px;
}
:root[data-bgpattern="windowpane"] #bgPattern {
  background-image:
    repeating-linear-gradient(0deg, color-mix(in srgb, var(--accent) 9%, transparent) 0 3px, transparent 3px 40px),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--accent) 9%, transparent) 0 3px, transparent 3px 40px);
  background-size: 40px 40px;
}
[data-bgswatch="windowpane"] {
  background-image:
    repeating-linear-gradient(0deg, color-mix(in srgb, var(--accent) 45%, transparent) 0 2px, transparent 2px 16px),
    repeating-linear-gradient(90deg, color-mix(in srgb, var(--accent) 45%, transparent) 0 2px, transparent 2px 16px);
  background-size: 16px 16px;
}
:root[data-bgpattern="houndstooth"] #bgPattern {
  background-image:
    conic-gradient(from 45deg, color-mix(in srgb, var(--accent) 8%, transparent) 25%, transparent 0 50%, color-mix(in srgb, var(--accent) 8%, transparent) 0 75%, transparent 0),
    conic-gradient(from 45deg, transparent 25%, color-mix(in srgb, var(--accent) 8%, transparent) 0 50%, transparent 0 75%, color-mix(in srgb, var(--accent) 8%, transparent) 0);
  background-size: 16px 16px, 16px 16px;
  background-position: 0 0, 8px 8px;
}
[data-bgswatch="houndstooth"] {
  background-image:
    conic-gradient(from 45deg, color-mix(in srgb, var(--accent) 45%, transparent) 25%, transparent 0 50%, color-mix(in srgb, var(--accent) 45%, transparent) 0 75%, transparent 0),
    conic-gradient(from 45deg, transparent 25%, color-mix(in srgb, var(--accent) 45%, transparent) 0 50%, transparent 0 75%, color-mix(in srgb, var(--accent) 45%, transparent) 0);
  background-size: 12px 12px, 12px 12px;
  background-position: 0 0, 6px 6px;
}
:root[data-bgpattern="stripesvertical"] #bgPattern {
  background-image: repeating-linear-gradient(90deg, color-mix(in srgb, var(--accent) 11%, transparent) 0 3px, transparent 3px 14px);
  background-size: 14px 14px;
}
[data-bgswatch="stripesvertical"] {
  background-image: repeating-linear-gradient(90deg, color-mix(in srgb, var(--accent) 50%, transparent) 0 2.5px, transparent 2.5px 9px);
  background-size: 9px 9px;
}
:root[data-bgpattern="stripesdiagonal"] #bgPattern {
  background-image: repeating-linear-gradient(45deg, color-mix(in srgb, var(--accent) 10%, transparent) 0 8px, transparent 8px 20px);
  background-size: 20px 20px;
}
[data-bgswatch="stripesdiagonal"] {
  background-image: repeating-linear-gradient(45deg, color-mix(in srgb, var(--accent) 45%, transparent) 0 5px, transparent 5px 13px);
  background-size: 13px 13px;
}
:root[data-bgpattern="polkadot"] #bgPattern {
  background-image:
    radial-gradient(circle, color-mix(in srgb, var(--accent) 13%, transparent) 4px, transparent 4.2px),
    radial-gradient(circle, color-mix(in srgb, var(--accent) 13%, transparent) 4px, transparent 4.2px);
  background-size: 32px 32px, 32px 32px;
  background-position: 0 0, 16px 16px;
}
[data-bgswatch="polkadot"] {
  background-image:
    radial-gradient(circle, color-mix(in srgb, var(--accent) 55%, transparent) 2.4px, transparent 2.6px),
    radial-gradient(circle, color-mix(in srgb, var(--accent) 55%, transparent) 2.4px, transparent 2.6px);
  background-size: 16px 16px, 16px 16px;
  background-position: 0 0, 8px 8px;
}
:root[data-bgpattern="diamondgrid"] #bgPattern {
  background-image:
    repeating-linear-gradient(45deg, color-mix(in srgb, var(--accent) 10%, transparent) 0 1.6px, transparent 1.6px 30px),
    repeating-linear-gradient(-45deg, color-mix(in srgb, var(--accent) 10%, transparent) 0 1.6px, transparent 1.6px 30px);
  background-size: 30px 30px;
}
[data-bgswatch="diamondgrid"] {
  background-image:
    repeating-linear-gradient(45deg, color-mix(in srgb, var(--accent) 48%, transparent) 0 1.6px, transparent 1.6px 15px),
    repeating-linear-gradient(-45deg, color-mix(in srgb, var(--accent) 48%, transparent) 0 1.6px, transparent 1.6px 15px);
  background-size: 15px 15px;
}
:root[data-bgpattern="bricks"] #bgPattern {
  background-image:
    linear-gradient(color-mix(in srgb, var(--accent) 11%, transparent) 2px, transparent 2px),
    linear-gradient(90deg, color-mix(in srgb, var(--accent) 11%, transparent) 2px, transparent 2px),
    linear-gradient(90deg, color-mix(in srgb, var(--accent) 11%, transparent) 2px, transparent 2px);
  background-size: 100% 18px, 36px 36px, 36px 36px;
  background-position: 0 0, 0 0, 18px 18px;
}
[data-bgswatch="bricks"] {
  background-image:
    linear-gradient(color-mix(in srgb, var(--accent) 50%, transparent) 1.6px, transparent 1.6px),
    linear-gradient(90deg, color-mix(in srgb, var(--accent) 50%, transparent) 1.6px, transparent 1.6px),
    linear-gradient(90deg, color-mix(in srgb, var(--accent) 50%, transparent) 1.6px, transparent 1.6px);
  background-size: 100% 9px, 18px 18px, 18px 18px;
  background-position: 0 0, 0 0, 9px 9px;
}
:root[data-bgpattern="confetti"] #bgPattern {
  background-image:
    radial-gradient(circle at 15% 22%, color-mix(in srgb, var(--accent) 16%, transparent) 1.8px, transparent 2.2px),
    radial-gradient(circle at 62% 12%, color-mix(in srgb, var(--accent) 16%, transparent) 1.4px, transparent 1.8px),
    radial-gradient(circle at 82% 58%, color-mix(in srgb, var(--accent) 16%, transparent) 2px, transparent 2.4px),
    radial-gradient(circle at 32% 72%, color-mix(in srgb, var(--accent) 16%, transparent) 1.5px, transparent 1.9px),
    radial-gradient(circle at 55% 88%, color-mix(in srgb, var(--accent) 16%, transparent) 1.8px, transparent 2.2px);
  background-size: 34px 34px;
}
[data-bgswatch="confetti"] {
  background-image:
    radial-gradient(circle at 15% 22%, color-mix(in srgb, var(--accent) 60%, transparent) 1.6px, transparent 2px),
    radial-gradient(circle at 62% 12%, color-mix(in srgb, var(--accent) 60%, transparent) 1.2px, transparent 1.6px),
    radial-gradient(circle at 82% 58%, color-mix(in srgb, var(--accent) 60%, transparent) 1.8px, transparent 2.2px),
    radial-gradient(circle at 32% 72%, color-mix(in srgb, var(--accent) 60%, transparent) 1.3px, transparent 1.7px),
    radial-gradient(circle at 55% 88%, color-mix(in srgb, var(--accent) 60%, transparent) 1.6px, transparent 2px);
  background-size: 17px 17px;
}
:root[data-bgpattern="ripples"] #bgPattern {
  background-image: repeating-radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--accent) 12%, transparent) 0 1.6px, transparent 1.6px 12px);
  background-size: 48px 48px;
}
[data-bgswatch="ripples"] {
  background-image: repeating-radial-gradient(circle at 50% 50%, color-mix(in srgb, var(--accent) 50%, transparent) 0 1.4px, transparent 1.4px 6px);
  background-size: 24px 24px;
}

/* Scattered Motifs, Woven & Tessellated, Bands & Waves — SVG masks. Each
   pair below shares one data-URI (the shape never changes, only mask-size
   and the revealing background-color do), so the SVG only appears once. */
:root[data-bgpattern="circuit"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M4 4H20V20H36' stroke='white' stroke-width='1.6' fill='none'/%3E%3Ccircle cx='4' cy='4' r='2' fill='white'/%3E%3Ccircle cx='36' cy='4' r='1.6' fill='white'/%3E%3Ccircle cx='36' cy='20' r='2' fill='white'/%3E%3Cpath d='M36 4V16H24' stroke='white' stroke-width='1.6' fill='none'/%3E%3Ccircle cx='24' cy='16' r='1.6' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M4 4H20V20H36' stroke='white' stroke-width='1.6' fill='none'/%3E%3Ccircle cx='4' cy='4' r='2' fill='white'/%3E%3Ccircle cx='36' cy='4' r='1.6' fill='white'/%3E%3Ccircle cx='36' cy='20' r='2' fill='white'/%3E%3Cpath d='M36 4V16H24' stroke='white' stroke-width='1.6' fill='none'/%3E%3Ccircle cx='24' cy='16' r='1.6' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 40px 40px;
          mask-size: 40px 40px;
}
[data-bgswatch="circuit"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M4 4H20V20H36' stroke='white' stroke-width='1.6' fill='none'/%3E%3Ccircle cx='4' cy='4' r='2' fill='white'/%3E%3Ccircle cx='36' cy='4' r='1.6' fill='white'/%3E%3Ccircle cx='36' cy='20' r='2' fill='white'/%3E%3Cpath d='M36 4V16H24' stroke='white' stroke-width='1.6' fill='none'/%3E%3Ccircle cx='24' cy='16' r='1.6' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M4 4H20V20H36' stroke='white' stroke-width='1.6' fill='none'/%3E%3Ccircle cx='4' cy='4' r='2' fill='white'/%3E%3Ccircle cx='36' cy='4' r='1.6' fill='white'/%3E%3Ccircle cx='36' cy='20' r='2' fill='white'/%3E%3Cpath d='M36 4V16H24' stroke='white' stroke-width='1.6' fill='none'/%3E%3Ccircle cx='24' cy='16' r='1.6' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 22px 22px;
          mask-size: 22px 22px;
}
:root[data-bgpattern="bubbles"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 15%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56'%3E%3Ccircle cx='10' cy='12' r='6' stroke='white' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='34' cy='8' r='3' stroke='white' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='44' cy='30' r='8' stroke='white' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='16' cy='40' r='4.5' stroke='white' stroke-width='1.4' fill='none'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56'%3E%3Ccircle cx='10' cy='12' r='6' stroke='white' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='34' cy='8' r='3' stroke='white' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='44' cy='30' r='8' stroke='white' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='16' cy='40' r='4.5' stroke='white' stroke-width='1.4' fill='none'/%3E%3C/svg%3E");
  -webkit-mask-size: 56px 56px;
          mask-size: 56px 56px;
}
[data-bgswatch="bubbles"] {
  background-color: color-mix(in srgb, var(--accent) 62%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56'%3E%3Ccircle cx='10' cy='12' r='6' stroke='white' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='34' cy='8' r='3' stroke='white' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='44' cy='30' r='8' stroke='white' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='16' cy='40' r='4.5' stroke='white' stroke-width='1.4' fill='none'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='56' height='56'%3E%3Ccircle cx='10' cy='12' r='6' stroke='white' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='34' cy='8' r='3' stroke='white' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='44' cy='30' r='8' stroke='white' stroke-width='1.4' fill='none'/%3E%3Ccircle cx='16' cy='40' r='4.5' stroke='white' stroke-width='1.4' fill='none'/%3E%3C/svg%3E");
  -webkit-mask-size: 30px 30px;
          mask-size: 30px 30px;
}
:root[data-bgpattern="stars"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 16%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M10,4 L11.68,8.32 L16,10 L11.68,11.68 L10,16 L8.32,11.68 L4,10 L8.32,8.32 Z' fill='white'/%3E%3Cpath d='M32,24 L33.12,25.88 L36,28 L33.12,30.12 L32,32 L30.88,30.12 L28,28 L30.88,25.88 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M10,4 L11.68,8.32 L16,10 L11.68,11.68 L10,16 L8.32,11.68 L4,10 L8.32,8.32 Z' fill='white'/%3E%3Cpath d='M32,24 L33.12,25.88 L36,28 L33.12,30.12 L32,32 L30.88,30.12 L28,28 L30.88,25.88 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 44px 44px;
          mask-size: 44px 44px;
}
[data-bgswatch="stars"] {
  background-color: color-mix(in srgb, var(--accent) 62%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M10,4 L11.68,8.32 L16,10 L11.68,11.68 L10,16 L8.32,11.68 L4,10 L8.32,8.32 Z' fill='white'/%3E%3Cpath d='M32,24 L33.12,25.88 L36,28 L33.12,30.12 L32,32 L30.88,30.12 L28,28 L30.88,25.88 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M10,4 L11.68,8.32 L16,10 L11.68,11.68 L10,16 L8.32,11.68 L4,10 L8.32,8.32 Z' fill='white'/%3E%3Cpath d='M32,24 L33.12,25.88 L36,28 L33.12,30.12 L32,32 L30.88,30.12 L28,28 L30.88,25.88 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 24px 24px;
          mask-size: 24px 24px;
}
:root[data-bgpattern="snowflakes"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 15%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg stroke='white' stroke-width='1.2' stroke-linecap='round'%3E%3Cpath d='M4,10H16M7,4.8L13,15.2M13,4.8L7,15.2'/%3E%3Cpath d='M24,26H32M25.9,22.5L30.1,29.5M30.1,22.5L25.9,29.5'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg stroke='white' stroke-width='1.2' stroke-linecap='round'%3E%3Cpath d='M4,10H16M7,4.8L13,15.2M13,4.8L7,15.2'/%3E%3Cpath d='M24,26H32M25.9,22.5L30.1,29.5M30.1,22.5L25.9,29.5'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 40px 40px;
          mask-size: 40px 40px;
}
[data-bgswatch="snowflakes"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg stroke='white' stroke-width='1.2' stroke-linecap='round'%3E%3Cpath d='M4,10H16M7,4.8L13,15.2M13,4.8L7,15.2'/%3E%3Cpath d='M24,26H32M25.9,22.5L30.1,29.5M30.1,22.5L25.9,29.5'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg stroke='white' stroke-width='1.2' stroke-linecap='round'%3E%3Cpath d='M4,10H16M7,4.8L13,15.2M13,4.8L7,15.2'/%3E%3Cpath d='M24,26H32M25.9,22.5L30.1,29.5M30.1,22.5L25.9,29.5'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 22px 22px;
          mask-size: 22px 22px;
}
:root[data-bgpattern="frostcrystal"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg stroke='white' stroke-width='1.1' stroke-linecap='round'%3E%3Cpath d='M20,20L34,20M20,20L27,32.12M20,20L13,32.12M20,20L6,20M20,20L13,7.88M20,20L27,7.88'/%3E%3Cpath d='M27.7,20L30.98,22.30M27.7,20L30.98,17.70M23.85,26.67L23.50,30.66M23.85,26.67L27.48,28.36M16.15,26.67L12.52,28.36M16.15,26.67L16.50,30.66M12.3,20L9.02,17.70M12.3,20L9.02,22.30M16.15,13.33L16.50,9.34M16.15,13.33L12.52,11.64M23.85,13.33L27.48,11.64M23.85,13.33L23.50,9.34'/%3E%3C/g%3E%3Ccircle cx='20' cy='20' r='1.4' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg stroke='white' stroke-width='1.1' stroke-linecap='round'%3E%3Cpath d='M20,20L34,20M20,20L27,32.12M20,20L13,32.12M20,20L6,20M20,20L13,7.88M20,20L27,7.88'/%3E%3Cpath d='M27.7,20L30.98,22.30M27.7,20L30.98,17.70M23.85,26.67L23.50,30.66M23.85,26.67L27.48,28.36M16.15,26.67L12.52,28.36M16.15,26.67L16.50,30.66M12.3,20L9.02,17.70M12.3,20L9.02,22.30M16.15,13.33L16.50,9.34M16.15,13.33L12.52,11.64M23.85,13.33L27.48,11.64M23.85,13.33L23.50,9.34'/%3E%3C/g%3E%3Ccircle cx='20' cy='20' r='1.4' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 40px 40px;
          mask-size: 40px 40px;
}
[data-bgswatch="frostcrystal"] {
  background-color: color-mix(in srgb, var(--accent) 58%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg stroke='white' stroke-width='1.1' stroke-linecap='round'%3E%3Cpath d='M20,20L34,20M20,20L27,32.12M20,20L13,32.12M20,20L6,20M20,20L13,7.88M20,20L27,7.88'/%3E%3Cpath d='M27.7,20L30.98,22.30M27.7,20L30.98,17.70M23.85,26.67L23.50,30.66M23.85,26.67L27.48,28.36M16.15,26.67L12.52,28.36M16.15,26.67L16.50,30.66M12.3,20L9.02,17.70M12.3,20L9.02,22.30M16.15,13.33L16.50,9.34M16.15,13.33L12.52,11.64M23.85,13.33L27.48,11.64M23.85,13.33L23.50,9.34'/%3E%3C/g%3E%3Ccircle cx='20' cy='20' r='1.4' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg stroke='white' stroke-width='1.1' stroke-linecap='round'%3E%3Cpath d='M20,20L34,20M20,20L27,32.12M20,20L13,32.12M20,20L6,20M20,20L13,7.88M20,20L27,7.88'/%3E%3Cpath d='M27.7,20L30.98,22.30M27.7,20L30.98,17.70M23.85,26.67L23.50,30.66M23.85,26.67L27.48,28.36M16.15,26.67L12.52,28.36M16.15,26.67L16.50,30.66M12.3,20L9.02,17.70M12.3,20L9.02,22.30M16.15,13.33L16.50,9.34M16.15,13.33L12.52,11.64M23.85,13.33L27.48,11.64M23.85,13.33L23.50,9.34'/%3E%3C/g%3E%3Ccircle cx='20' cy='20' r='1.4' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 24px 24px;
          mask-size: 24px 24px;
}
:root[data-bgpattern="snowfall"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 15%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg stroke='white' stroke-width='1' stroke-linecap='round'%3E%3Cpath d='M7,10H13M8.5,7.4L11.5,12.6M11.5,7.4L8.5,12.6'/%3E%3Cpath d='M27.8,26H32.2M28.9,24.1L31.1,27.9M31.1,24.1L28.9,27.9'/%3E%3C/g%3E%3Cg fill='white'%3E%3Ccircle cx='24' cy='6' r='1.3'/%3E%3Ccircle cx='38' cy='14' r='1'/%3E%3Ccircle cx='6' cy='32' r='1.4'/%3E%3Ccircle cx='36' cy='38' r='1'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg stroke='white' stroke-width='1' stroke-linecap='round'%3E%3Cpath d='M7,10H13M8.5,7.4L11.5,12.6M11.5,7.4L8.5,12.6'/%3E%3Cpath d='M27.8,26H32.2M28.9,24.1L31.1,27.9M31.1,24.1L28.9,27.9'/%3E%3C/g%3E%3Cg fill='white'%3E%3Ccircle cx='24' cy='6' r='1.3'/%3E%3Ccircle cx='38' cy='14' r='1'/%3E%3Ccircle cx='6' cy='32' r='1.4'/%3E%3Ccircle cx='36' cy='38' r='1'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 44px 44px;
          mask-size: 44px 44px;
}
[data-bgswatch="snowfall"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg stroke='white' stroke-width='1' stroke-linecap='round'%3E%3Cpath d='M7,10H13M8.5,7.4L11.5,12.6M11.5,7.4L8.5,12.6'/%3E%3Cpath d='M27.8,26H32.2M28.9,24.1L31.1,27.9M31.1,24.1L28.9,27.9'/%3E%3C/g%3E%3Cg fill='white'%3E%3Ccircle cx='24' cy='6' r='1.3'/%3E%3Ccircle cx='38' cy='14' r='1'/%3E%3Ccircle cx='6' cy='32' r='1.4'/%3E%3Ccircle cx='36' cy='38' r='1'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg stroke='white' stroke-width='1' stroke-linecap='round'%3E%3Cpath d='M7,10H13M8.5,7.4L11.5,12.6M11.5,7.4L8.5,12.6'/%3E%3Cpath d='M27.8,26H32.2M28.9,24.1L31.1,27.9M31.1,24.1L28.9,27.9'/%3E%3C/g%3E%3Cg fill='white'%3E%3Ccircle cx='24' cy='6' r='1.3'/%3E%3Ccircle cx='38' cy='14' r='1'/%3E%3Ccircle cx='6' cy='32' r='1.4'/%3E%3Ccircle cx='36' cy='38' r='1'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 26px 26px;
          mask-size: 26px 26px;
}
:root[data-bgpattern="plusmarks"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 15%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Cpath d='M13,7V19M7,13H19' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Cpath d='M13,7V19M7,13H19' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  -webkit-mask-size: 26px 26px;
          mask-size: 26px 26px;
}
[data-bgswatch="plusmarks"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Cpath d='M13,7V19M7,13H19' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='26'%3E%3Cpath d='M13,7V19M7,13H19' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  -webkit-mask-size: 15px 15px;
          mask-size: 15px 15px;
}
:root[data-bgpattern="leaves"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M12 6 C17 9 17 19 12 22 C7 19 7 9 12 6 Z' fill='white' transform='rotate(-20 12 14)'/%3E%3Cpath d='M32 27 C35.5 29 35.5 35 32 37 C28.5 35 28.5 29 32 27 Z' fill='white' transform='rotate(35 32 32)'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M12 6 C17 9 17 19 12 22 C7 19 7 9 12 6 Z' fill='white' transform='rotate(-20 12 14)'/%3E%3Cpath d='M32 27 C35.5 29 35.5 35 32 37 C28.5 35 28.5 29 32 27 Z' fill='white' transform='rotate(35 32 32)'/%3E%3C/svg%3E");
  -webkit-mask-size: 44px 44px;
          mask-size: 44px 44px;
}
[data-bgswatch="leaves"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M12 6 C17 9 17 19 12 22 C7 19 7 9 12 6 Z' fill='white' transform='rotate(-20 12 14)'/%3E%3Cpath d='M32 27 C35.5 29 35.5 35 32 37 C28.5 35 28.5 29 32 27 Z' fill='white' transform='rotate(35 32 32)'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M12 6 C17 9 17 19 12 22 C7 19 7 9 12 6 Z' fill='white' transform='rotate(-20 12 14)'/%3E%3Cpath d='M32 27 C35.5 29 35.5 35 32 37 C28.5 35 28.5 29 32 27 Z' fill='white' transform='rotate(35 32 32)'/%3E%3C/svg%3E");
  -webkit-mask-size: 24px 24px;
          mask-size: 24px 24px;
}
:root[data-bgpattern="branch"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 13%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M4,40 L34,6' stroke='white' stroke-width='1.3' fill='none'/%3E%3Cg fill='white'%3E%3Cg transform='translate(10,33.2) rotate(40) scale(0.9)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(16,26.4) rotate(-140) scale(0.9)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(22,19.6) rotate(40) scale(0.8)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(28,12.8) rotate(-140) scale(0.8)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(34,6) rotate(-40) scale(0.6)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M4,40 L34,6' stroke='white' stroke-width='1.3' fill='none'/%3E%3Cg fill='white'%3E%3Cg transform='translate(10,33.2) rotate(40) scale(0.9)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(16,26.4) rotate(-140) scale(0.9)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(22,19.6) rotate(40) scale(0.8)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(28,12.8) rotate(-140) scale(0.8)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(34,6) rotate(-40) scale(0.6)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 44px 44px;
          mask-size: 44px 44px;
}
[data-bgswatch="branch"] {
  background-color: color-mix(in srgb, var(--accent) 58%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M4,40 L34,6' stroke='white' stroke-width='1.3' fill='none'/%3E%3Cg fill='white'%3E%3Cg transform='translate(10,33.2) rotate(40) scale(0.9)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(16,26.4) rotate(-140) scale(0.9)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(22,19.6) rotate(40) scale(0.8)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(28,12.8) rotate(-140) scale(0.8)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(34,6) rotate(-40) scale(0.6)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M4,40 L34,6' stroke='white' stroke-width='1.3' fill='none'/%3E%3Cg fill='white'%3E%3Cg transform='translate(10,33.2) rotate(40) scale(0.9)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(16,26.4) rotate(-140) scale(0.9)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(22,19.6) rotate(40) scale(0.8)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(28,12.8) rotate(-140) scale(0.8)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3Cg transform='translate(34,6) rotate(-40) scale(0.6)'%3E%3Cpath d='M0,-6 C3,-4 3,4 0,6 C-3,4 -3,-4 0,-6 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 24px 24px;
          mask-size: 24px 24px;
}
:root[data-bgpattern="fern"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg stroke='white' stroke-width='1.3' stroke-linecap='round'%3E%3Cpath d='M20,4V36'/%3E%3Cpath d='M20,10L13,6M20,10L27,6M20,16L12,11M20,16L28,11M20,22L13,18M20,22L27,18M20,28L14,25M20,28L26,25'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg stroke='white' stroke-width='1.3' stroke-linecap='round'%3E%3Cpath d='M20,4V36'/%3E%3Cpath d='M20,10L13,6M20,10L27,6M20,16L12,11M20,16L28,11M20,22L13,18M20,22L27,18M20,28L14,25M20,28L26,25'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 40px 40px;
          mask-size: 40px 40px;
}
[data-bgswatch="fern"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg stroke='white' stroke-width='1.3' stroke-linecap='round'%3E%3Cpath d='M20,4V36'/%3E%3Cpath d='M20,10L13,6M20,10L27,6M20,16L12,11M20,16L28,11M20,22L13,18M20,22L27,18M20,28L14,25M20,28L26,25'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg stroke='white' stroke-width='1.3' stroke-linecap='round'%3E%3Cpath d='M20,4V36'/%3E%3Cpath d='M20,10L13,6M20,10L27,6M20,16L12,11M20,16L28,11M20,22L13,18M20,22L27,18M20,28L14,25M20,28L26,25'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 22px 22px;
          mask-size: 22px 22px;
}
:root[data-bgpattern="clover"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 13%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36'%3E%3Cg fill='white'%3E%3Ccircle cx='18' cy='10' r='6'/%3E%3Ccircle cx='12.8' cy='19' r='6'/%3E%3Ccircle cx='23.2' cy='19' r='6'/%3E%3C/g%3E%3Cpath d='M18,23V32' stroke='white' stroke-width='1.6'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36'%3E%3Cg fill='white'%3E%3Ccircle cx='18' cy='10' r='6'/%3E%3Ccircle cx='12.8' cy='19' r='6'/%3E%3Ccircle cx='23.2' cy='19' r='6'/%3E%3C/g%3E%3Cpath d='M18,23V32' stroke='white' stroke-width='1.6'/%3E%3C/svg%3E");
  -webkit-mask-size: 36px 36px;
          mask-size: 36px 36px;
}
[data-bgswatch="clover"] {
  background-color: color-mix(in srgb, var(--accent) 58%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36'%3E%3Cg fill='white'%3E%3Ccircle cx='18' cy='10' r='6'/%3E%3Ccircle cx='12.8' cy='19' r='6'/%3E%3Ccircle cx='23.2' cy='19' r='6'/%3E%3C/g%3E%3Cpath d='M18,23V32' stroke='white' stroke-width='1.6'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36'%3E%3Cg fill='white'%3E%3Ccircle cx='18' cy='10' r='6'/%3E%3Ccircle cx='12.8' cy='19' r='6'/%3E%3Ccircle cx='23.2' cy='19' r='6'/%3E%3C/g%3E%3Cpath d='M18,23V32' stroke='white' stroke-width='1.6'/%3E%3C/svg%3E");
  -webkit-mask-size: 20px 20px;
          mask-size: 20px 20px;
}
:root[data-bgpattern="daisy"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='34' height='34'%3E%3Cg fill='none' stroke='white' stroke-width='1.3'%3E%3Cellipse cx='17' cy='10' rx='3' ry='6'/%3E%3Cellipse cx='23.06' cy='13.5' rx='3' ry='6' transform='rotate(60 23.06 13.5)'/%3E%3Cellipse cx='23.06' cy='20.5' rx='3' ry='6' transform='rotate(120 23.06 20.5)'/%3E%3Cellipse cx='17' cy='24' rx='3' ry='6' transform='rotate(180 17 24)'/%3E%3Cellipse cx='10.94' cy='20.5' rx='3' ry='6' transform='rotate(240 10.94 20.5)'/%3E%3Cellipse cx='10.94' cy='13.5' rx='3' ry='6' transform='rotate(300 10.94 13.5)'/%3E%3C/g%3E%3Ccircle cx='17' cy='17' r='2.3' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='34' height='34'%3E%3Cg fill='none' stroke='white' stroke-width='1.3'%3E%3Cellipse cx='17' cy='10' rx='3' ry='6'/%3E%3Cellipse cx='23.06' cy='13.5' rx='3' ry='6' transform='rotate(60 23.06 13.5)'/%3E%3Cellipse cx='23.06' cy='20.5' rx='3' ry='6' transform='rotate(120 23.06 20.5)'/%3E%3Cellipse cx='17' cy='24' rx='3' ry='6' transform='rotate(180 17 24)'/%3E%3Cellipse cx='10.94' cy='20.5' rx='3' ry='6' transform='rotate(240 10.94 20.5)'/%3E%3Cellipse cx='10.94' cy='13.5' rx='3' ry='6' transform='rotate(300 10.94 13.5)'/%3E%3C/g%3E%3Ccircle cx='17' cy='17' r='2.3' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 34px 34px;
          mask-size: 34px 34px;
}
[data-bgswatch="daisy"] {
  background-color: color-mix(in srgb, var(--accent) 58%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='34' height='34'%3E%3Cg fill='none' stroke='white' stroke-width='1.3'%3E%3Cellipse cx='17' cy='10' rx='3' ry='6'/%3E%3Cellipse cx='23.06' cy='13.5' rx='3' ry='6' transform='rotate(60 23.06 13.5)'/%3E%3Cellipse cx='23.06' cy='20.5' rx='3' ry='6' transform='rotate(120 23.06 20.5)'/%3E%3Cellipse cx='17' cy='24' rx='3' ry='6' transform='rotate(180 17 24)'/%3E%3Cellipse cx='10.94' cy='20.5' rx='3' ry='6' transform='rotate(240 10.94 20.5)'/%3E%3Cellipse cx='10.94' cy='13.5' rx='3' ry='6' transform='rotate(300 10.94 13.5)'/%3E%3C/g%3E%3Ccircle cx='17' cy='17' r='2.3' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='34' height='34'%3E%3Cg fill='none' stroke='white' stroke-width='1.3'%3E%3Cellipse cx='17' cy='10' rx='3' ry='6'/%3E%3Cellipse cx='23.06' cy='13.5' rx='3' ry='6' transform='rotate(60 23.06 13.5)'/%3E%3Cellipse cx='23.06' cy='20.5' rx='3' ry='6' transform='rotate(120 23.06 20.5)'/%3E%3Cellipse cx='17' cy='24' rx='3' ry='6' transform='rotate(180 17 24)'/%3E%3Cellipse cx='10.94' cy='20.5' rx='3' ry='6' transform='rotate(240 10.94 20.5)'/%3E%3Cellipse cx='10.94' cy='13.5' rx='3' ry='6' transform='rotate(300 10.94 13.5)'/%3E%3C/g%3E%3Ccircle cx='17' cy='17' r='2.3' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 19px 19px;
          mask-size: 19px 19px;
}
:root[data-bgpattern="blossomsprig"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 13%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg fill='white'%3E%3Ccircle cx='12' cy='6.4' r='3.4'/%3E%3Ccircle cx='12' cy='13.6' r='3.4'/%3E%3Ccircle cx='8.4' cy='10' r='3.4'/%3E%3Ccircle cx='15.6' cy='10' r='3.4'/%3E%3C/g%3E%3Cpath d='M12,14V24' stroke='white' stroke-width='1.3'/%3E%3Cpath d='M12,20 C15,20.5 15,24.5 12,25.5 C10.5,24 10.5,21 12,20 Z' fill='white'/%3E%3Cg fill='white'%3E%3Ccircle cx='32' cy='27.8' r='2'/%3E%3Ccircle cx='32' cy='32.2' r='2'/%3E%3Ccircle cx='29.8' cy='30' r='2'/%3E%3Ccircle cx='34.2' cy='30' r='2'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg fill='white'%3E%3Ccircle cx='12' cy='6.4' r='3.4'/%3E%3Ccircle cx='12' cy='13.6' r='3.4'/%3E%3Ccircle cx='8.4' cy='10' r='3.4'/%3E%3Ccircle cx='15.6' cy='10' r='3.4'/%3E%3C/g%3E%3Cpath d='M12,14V24' stroke='white' stroke-width='1.3'/%3E%3Cpath d='M12,20 C15,20.5 15,24.5 12,25.5 C10.5,24 10.5,21 12,20 Z' fill='white'/%3E%3Cg fill='white'%3E%3Ccircle cx='32' cy='27.8' r='2'/%3E%3Ccircle cx='32' cy='32.2' r='2'/%3E%3Ccircle cx='29.8' cy='30' r='2'/%3E%3Ccircle cx='34.2' cy='30' r='2'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 44px 44px;
          mask-size: 44px 44px;
}
[data-bgswatch="blossomsprig"] {
  background-color: color-mix(in srgb, var(--accent) 58%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg fill='white'%3E%3Ccircle cx='12' cy='6.4' r='3.4'/%3E%3Ccircle cx='12' cy='13.6' r='3.4'/%3E%3Ccircle cx='8.4' cy='10' r='3.4'/%3E%3Ccircle cx='15.6' cy='10' r='3.4'/%3E%3C/g%3E%3Cpath d='M12,14V24' stroke='white' stroke-width='1.3'/%3E%3Cpath d='M12,20 C15,20.5 15,24.5 12,25.5 C10.5,24 10.5,21 12,20 Z' fill='white'/%3E%3Cg fill='white'%3E%3Ccircle cx='32' cy='27.8' r='2'/%3E%3Ccircle cx='32' cy='32.2' r='2'/%3E%3Ccircle cx='29.8' cy='30' r='2'/%3E%3Ccircle cx='34.2' cy='30' r='2'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg fill='white'%3E%3Ccircle cx='12' cy='6.4' r='3.4'/%3E%3Ccircle cx='12' cy='13.6' r='3.4'/%3E%3Ccircle cx='8.4' cy='10' r='3.4'/%3E%3Ccircle cx='15.6' cy='10' r='3.4'/%3E%3C/g%3E%3Cpath d='M12,14V24' stroke='white' stroke-width='1.3'/%3E%3Cpath d='M12,20 C15,20.5 15,24.5 12,25.5 C10.5,24 10.5,21 12,20 Z' fill='white'/%3E%3Cg fill='white'%3E%3Ccircle cx='32' cy='27.8' r='2'/%3E%3Ccircle cx='32' cy='32.2' r='2'/%3E%3Ccircle cx='29.8' cy='30' r='2'/%3E%3Ccircle cx='34.2' cy='30' r='2'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 24px 24px;
          mask-size: 24px 24px;
}
:root[data-bgpattern="herringbone"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 12%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='16'%3E%3Cg fill='white'%3E%3Crect x='-4' y='5' width='24' height='6' transform='rotate(45 8 8)'/%3E%3Crect x='12' y='5' width='24' height='6' transform='rotate(-45 24 8)'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='16'%3E%3Cg fill='white'%3E%3Crect x='-4' y='5' width='24' height='6' transform='rotate(45 8 8)'/%3E%3Crect x='12' y='5' width='24' height='6' transform='rotate(-45 24 8)'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 32px 16px;
          mask-size: 32px 16px;
}
[data-bgswatch="herringbone"] {
  background-color: color-mix(in srgb, var(--accent) 55%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='16'%3E%3Cg fill='white'%3E%3Crect x='-4' y='5' width='24' height='6' transform='rotate(45 8 8)'/%3E%3Crect x='12' y='5' width='24' height='6' transform='rotate(-45 24 8)'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='16'%3E%3Cg fill='white'%3E%3Crect x='-4' y='5' width='24' height='6' transform='rotate(45 8 8)'/%3E%3Crect x='12' y='5' width='24' height='6' transform='rotate(-45 24 8)'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 20px 10px;
          mask-size: 20px 10px;
}
:root[data-bgpattern="chevron"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 15%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath d='M0,15 L5,5 L10,15 L15,5 L20,15' stroke='white' stroke-width='2' fill='none' stroke-linejoin='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath d='M0,15 L5,5 L10,15 L15,5 L20,15' stroke='white' stroke-width='2' fill='none' stroke-linejoin='round'/%3E%3C/svg%3E");
  -webkit-mask-size: 20px 20px;
          mask-size: 20px 20px;
}
[data-bgswatch="chevron"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath d='M0,15 L5,5 L10,15 L15,5 L20,15' stroke='white' stroke-width='2' fill='none' stroke-linejoin='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath d='M0,15 L5,5 L10,15 L15,5 L20,15' stroke='white' stroke-width='2' fill='none' stroke-linejoin='round'/%3E%3C/svg%3E");
  -webkit-mask-size: 12px 12px;
          mask-size: 12px 12px;
}
:root[data-bgpattern="triangles"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 10%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath d='M0,0 L20,0 L0,20 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath d='M0,0 L20,0 L0,20 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 20px 20px;
          mask-size: 20px 20px;
}
[data-bgswatch="triangles"] {
  background-color: color-mix(in srgb, var(--accent) 50%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath d='M0,0 L20,0 L0,20 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath d='M0,0 L20,0 L0,20 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 12px 12px;
          mask-size: 12px 12px;
}
:root[data-bgpattern="lattice"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 13%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='24'%3E%3Cpolygon points='7,0 21,0 28,12 21,24 7,24 0,12' fill='none' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='24'%3E%3Cpolygon points='7,0 21,0 28,12 21,24 7,24 0,12' fill='none' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E");
  -webkit-mask-size: 28px 24px;
          mask-size: 28px 24px;
}
[data-bgswatch="lattice"] {
  background-color: color-mix(in srgb, var(--accent) 58%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='24'%3E%3Cpolygon points='7,0 21,0 28,12 21,24 7,24 0,12' fill='none' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='24'%3E%3Cpolygon points='7,0 21,0 28,12 21,24 7,24 0,12' fill='none' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E");
  -webkit-mask-size: 16px 14px;
          mask-size: 16px 14px;
}
:root[data-bgpattern="scallop"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 13%, transparent);
  -webkit-mask-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Cpath d='M0,10 A10,10 0 0 1 20,10' fill='none' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Cpath d='M0,10 A10,10 0 0 1 20,10' fill='none' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E");
          mask-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Cpath d='M0,10 A10,10 0 0 1 20,10' fill='none' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Cpath d='M0,10 A10,10 0 0 1 20,10' fill='none' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E");
  -webkit-mask-size: 20px 10px, 20px 10px;
          mask-size: 20px 10px, 20px 10px;
  -webkit-mask-position: 0 0, 10px 5px;
          mask-position: 0 0, 10px 5px;
}
[data-bgswatch="scallop"] {
  background-color: color-mix(in srgb, var(--accent) 58%, transparent);
  -webkit-mask-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Cpath d='M0,10 A10,10 0 0 1 20,10' fill='none' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Cpath d='M0,10 A10,10 0 0 1 20,10' fill='none' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E");
          mask-image:
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Cpath d='M0,10 A10,10 0 0 1 20,10' fill='none' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E"),
    url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='10'%3E%3Cpath d='M0,10 A10,10 0 0 1 20,10' fill='none' stroke='white' stroke-width='1.5'/%3E%3C/svg%3E");
  -webkit-mask-size: 12px 6px, 12px 6px;
          mask-size: 12px 6px, 12px 6px;
  -webkit-mask-position: 0 0, 6px 3px;
          mask-position: 0 0, 6px 3px;
}
:root[data-bgpattern="waves"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 15%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='12'%3E%3Cpath d='M0,6 Q6,0 12,6 T24,6' fill='none' stroke='white' stroke-width='1.6'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='12'%3E%3Cpath d='M0,6 Q6,0 12,6 T24,6' fill='none' stroke='white' stroke-width='1.6'/%3E%3C/svg%3E");
  -webkit-mask-size: 24px 12px;
          mask-size: 24px 12px;
}
[data-bgswatch="waves"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='12'%3E%3Cpath d='M0,6 Q6,0 12,6 T24,6' fill='none' stroke='white' stroke-width='1.6'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='24' height='12'%3E%3Cpath d='M0,6 Q6,0 12,6 T24,6' fill='none' stroke='white' stroke-width='1.6'/%3E%3C/svg%3E");
  -webkit-mask-size: 14px 7px;
          mask-size: 14px 7px;
}
:root[data-bgpattern="topographic"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 12%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Cpath d='M30,4 C46,4 56,16 56,30 C56,46 44,56 30,56 C14,56 4,44 4,30 C4,14 16,4 30,4 Z' fill='none' stroke='white' stroke-width='1.2'/%3E%3Cpath d='M30,12.8 C41.2,12.8 48.4,21.2 48.4,30 C48.4,41.2 40,48.4 30,48.4 C18.8,48.4 11.6,40 11.6,30 C11.6,18.8 20,11.6 30,11.6 Z' fill='none' stroke='white' stroke-width='1.2'/%3E%3Cpath d='M30,19.6 C36.4,19.6 40.6,23.8 40.6,30 C40.6,36.4 35.2,40.6 30,40.6 C23.6,40.6 19.4,36.2 19.4,30 C19.4,23.6 24.8,19.4 30,19.4 Z' fill='none' stroke='white' stroke-width='1.2'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Cpath d='M30,4 C46,4 56,16 56,30 C56,46 44,56 30,56 C14,56 4,44 4,30 C4,14 16,4 30,4 Z' fill='none' stroke='white' stroke-width='1.2'/%3E%3Cpath d='M30,12.8 C41.2,12.8 48.4,21.2 48.4,30 C48.4,41.2 40,48.4 30,48.4 C18.8,48.4 11.6,40 11.6,30 C11.6,18.8 20,11.6 30,11.6 Z' fill='none' stroke='white' stroke-width='1.2'/%3E%3Cpath d='M30,19.6 C36.4,19.6 40.6,23.8 40.6,30 C40.6,36.4 35.2,40.6 30,40.6 C23.6,40.6 19.4,36.2 19.4,30 C19.4,23.6 24.8,19.4 30,19.4 Z' fill='none' stroke='white' stroke-width='1.2'/%3E%3C/svg%3E");
  -webkit-mask-size: 60px 60px;
          mask-size: 60px 60px;
}
[data-bgswatch="topographic"] {
  background-color: color-mix(in srgb, var(--accent) 55%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Cpath d='M30,4 C46,4 56,16 56,30 C56,46 44,56 30,56 C14,56 4,44 4,30 C4,14 16,4 30,4 Z' fill='none' stroke='white' stroke-width='1.2'/%3E%3Cpath d='M30,12.8 C41.2,12.8 48.4,21.2 48.4,30 C48.4,41.2 40,48.4 30,48.4 C18.8,48.4 11.6,40 11.6,30 C11.6,18.8 20,11.6 30,11.6 Z' fill='none' stroke='white' stroke-width='1.2'/%3E%3Cpath d='M30,19.6 C36.4,19.6 40.6,23.8 40.6,30 C40.6,36.4 35.2,40.6 30,40.6 C23.6,40.6 19.4,36.2 19.4,30 C19.4,23.6 24.8,19.4 30,19.4 Z' fill='none' stroke='white' stroke-width='1.2'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='60' height='60'%3E%3Cpath d='M30,4 C46,4 56,16 56,30 C56,46 44,56 30,56 C14,56 4,44 4,30 C4,14 16,4 30,4 Z' fill='none' stroke='white' stroke-width='1.2'/%3E%3Cpath d='M30,12.8 C41.2,12.8 48.4,21.2 48.4,30 C48.4,41.2 40,48.4 30,48.4 C18.8,48.4 11.6,40 11.6,30 C11.6,18.8 20,11.6 30,11.6 Z' fill='none' stroke='white' stroke-width='1.2'/%3E%3Cpath d='M30,19.6 C36.4,19.6 40.6,23.8 40.6,30 C40.6,36.4 35.2,40.6 30,40.6 C23.6,40.6 19.4,36.2 19.4,30 C19.4,23.6 24.8,19.4 30,19.4 Z' fill='none' stroke='white' stroke-width='1.2'/%3E%3C/svg%3E");
  -webkit-mask-size: 30px 30px;
          mask-size: 30px 30px;
}
:root[data-bgpattern="eightpointstar"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 15%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M20,4 L22.5,14 L31.3,8.7 L26,17.5 L36,20 L26,22.5 L31.3,31.3 L22.5,26 L20,36 L17.5,26 L8.7,31.3 L14,22.5 L4,20 L14,17.5 L8.7,8.7 L17.5,14 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M20,4 L22.5,14 L31.3,8.7 L26,17.5 L36,20 L26,22.5 L31.3,31.3 L22.5,26 L20,36 L17.5,26 L8.7,31.3 L14,22.5 L4,20 L14,17.5 L8.7,8.7 L17.5,14 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 40px 40px;
          mask-size: 40px 40px;
}
[data-bgswatch="eightpointstar"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M20,4 L22.5,14 L31.3,8.7 L26,17.5 L36,20 L26,22.5 L31.3,31.3 L22.5,26 L20,36 L17.5,26 L8.7,31.3 L14,22.5 L4,20 L14,17.5 L8.7,8.7 L17.5,14 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M20,4 L22.5,14 L31.3,8.7 L26,17.5 L36,20 L26,22.5 L31.3,31.3 L22.5,26 L20,36 L17.5,26 L8.7,31.3 L14,22.5 L4,20 L14,17.5 L8.7,8.7 L17.5,14 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 22px 22px;
          mask-size: 22px 22px;
}
:root[data-bgpattern="girih"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 12%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Cg stroke='white' fill='none'%3E%3Cpath d='M0,0 L32,32' stroke-width='3.4'/%3E%3Cpath d='M32,0 L0,32' stroke-width='3.4'/%3E%3Cpolygon points='16,10 22,16 16,22 10,16' stroke-width='1.4'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Cg stroke='white' fill='none'%3E%3Cpath d='M0,0 L32,32' stroke-width='3.4'/%3E%3Cpath d='M32,0 L0,32' stroke-width='3.4'/%3E%3Cpolygon points='16,10 22,16 16,22 10,16' stroke-width='1.4'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 32px 32px;
          mask-size: 32px 32px;
}
[data-bgswatch="girih"] {
  background-color: color-mix(in srgb, var(--accent) 55%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Cg stroke='white' fill='none'%3E%3Cpath d='M0,0 L32,32' stroke-width='3.4'/%3E%3Cpath d='M32,0 L0,32' stroke-width='3.4'/%3E%3Cpolygon points='16,10 22,16 16,22 10,16' stroke-width='1.4'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='32'%3E%3Cg stroke='white' fill='none'%3E%3Cpath d='M0,0 L32,32' stroke-width='3.4'/%3E%3Cpath d='M32,0 L0,32' stroke-width='3.4'/%3E%3Cpolygon points='16,10 22,16 16,22 10,16' stroke-width='1.4'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 18px 18px;
          mask-size: 18px 18px;
}
:root[data-bgpattern="dragon"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M4,34 C6,28 10,26 8,20 C6,15 10,10 16,12 L18,7 L20,13 C24,11 28,13 26,18 L31,15 L29,20 C33,20 36,24 34,28 C31,33 25,32 22,28 C18,24 12,24 10,29 C9,32 6,35 4,34 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M4,34 C6,28 10,26 8,20 C6,15 10,10 16,12 L18,7 L20,13 C24,11 28,13 26,18 L31,15 L29,20 C33,20 36,24 34,28 C31,33 25,32 22,28 C18,24 12,24 10,29 C9,32 6,35 4,34 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 44px 44px;
          mask-size: 44px 44px;
}
[data-bgswatch="dragon"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M4,34 C6,28 10,26 8,20 C6,15 10,10 16,12 L18,7 L20,13 C24,11 28,13 26,18 L31,15 L29,20 C33,20 36,24 34,28 C31,33 25,32 22,28 C18,24 12,24 10,29 C9,32 6,35 4,34 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M4,34 C6,28 10,26 8,20 C6,15 10,10 16,12 L18,7 L20,13 C24,11 28,13 26,18 L31,15 L29,20 C33,20 36,24 34,28 C31,33 25,32 22,28 C18,24 12,24 10,29 C9,32 6,35 4,34 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 24px 24px;
          mask-size: 24px 24px;
}
:root[data-bgpattern="shield"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='40'%3E%3Cpath d='M4,4 L28,4 L28,18 C28,28 20,34 16,36 C12,34 4,28 4,18 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='40'%3E%3Cpath d='M4,4 L28,4 L28,18 C28,28 20,34 16,36 C12,34 4,28 4,18 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 32px 40px;
          mask-size: 32px 40px;
}
[data-bgswatch="shield"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='40'%3E%3Cpath d='M4,4 L28,4 L28,18 C28,28 20,34 16,36 C12,34 4,28 4,18 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='32' height='40'%3E%3Cpath d='M4,4 L28,4 L28,18 C28,28 20,34 16,36 C12,34 4,28 4,18 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 18px 22px;
          mask-size: 18px 22px;
}
:root[data-bgpattern="wing"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg fill='white' transform='translate(14,34)'%3E%3Cg transform='rotate(-35) scale(0.9)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(-15)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(5) scale(1.05)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(25) scale(0.95)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(45) scale(0.8)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg fill='white' transform='translate(14,34)'%3E%3Cg transform='rotate(-35) scale(0.9)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(-15)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(5) scale(1.05)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(25) scale(0.95)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(45) scale(0.8)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 44px 44px;
          mask-size: 44px 44px;
}
[data-bgswatch="wing"] {
  background-color: color-mix(in srgb, var(--accent) 58%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg fill='white' transform='translate(14,34)'%3E%3Cg transform='rotate(-35) scale(0.9)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(-15)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(5) scale(1.05)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(25) scale(0.95)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(45) scale(0.8)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cg fill='white' transform='translate(14,34)'%3E%3Cg transform='rotate(-35) scale(0.9)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(-15)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(5) scale(1.05)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(25) scale(0.95)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3Cg transform='rotate(45) scale(0.8)'%3E%3Cpath d='M0,0 C3,-9 3,-19 0,-24 C-3,-19 -3,-9 0,0 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 24px 24px;
          mask-size: 24px 24px;
}
:root[data-bgpattern="penrose"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 15%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg fill='white' transform='translate(20,20)'%3E%3Cg transform='rotate(0)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(72)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(144)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(216)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(288)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg fill='white' transform='translate(20,20)'%3E%3Cg transform='rotate(0)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(72)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(144)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(216)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(288)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 40px 40px;
          mask-size: 40px 40px;
}
[data-bgswatch="penrose"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg fill='white' transform='translate(20,20)'%3E%3Cg transform='rotate(0)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(72)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(144)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(216)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(288)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cg fill='white' transform='translate(20,20)'%3E%3Cg transform='rotate(0)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(72)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(144)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(216)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3Cg transform='rotate(288)'%3E%3Cpath d='M0,0 L6,-16 L0,-20 L-6,-16 Z'/%3E%3C/g%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 22px 22px;
          mask-size: 22px 22px;
}
:root[data-bgpattern="celticknot"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 13%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36'%3E%3Cg fill='none' stroke='white' stroke-width='1.6'%3E%3Ccircle cx='18' cy='11' r='8'/%3E%3Ccircle cx='11.6' cy='21' r='8'/%3E%3Ccircle cx='24.4' cy='21' r='8'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36'%3E%3Cg fill='none' stroke='white' stroke-width='1.6'%3E%3Ccircle cx='18' cy='11' r='8'/%3E%3Ccircle cx='11.6' cy='21' r='8'/%3E%3Ccircle cx='24.4' cy='21' r='8'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 36px 36px;
          mask-size: 36px 36px;
}
[data-bgswatch="celticknot"] {
  background-color: color-mix(in srgb, var(--accent) 55%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36'%3E%3Cg fill='none' stroke='white' stroke-width='1.6'%3E%3Ccircle cx='18' cy='11' r='8'/%3E%3Ccircle cx='11.6' cy='21' r='8'/%3E%3Ccircle cx='24.4' cy='21' r='8'/%3E%3C/g%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='36' height='36'%3E%3Cg fill='none' stroke='white' stroke-width='1.6'%3E%3Ccircle cx='18' cy='11' r='8'/%3E%3Ccircle cx='11.6' cy='21' r='8'/%3E%3Ccircle cx='24.4' cy='21' r='8'/%3E%3C/g%3E%3C/svg%3E");
  -webkit-mask-size: 20px 20px;
          mask-size: 20px 20px;
}

/* ---- typeface -----------------------------------------------------------
   Its own axis, independent of the skin the way light/dark is, and the only
   place --font-body / --font-head / --head-weight are set. No `data-font` at
   all means the app default at the top of this file. Declared after the skins
   so it still wins on source order if one of them ever grows a font again — the
   selectors have equal specificity.

   The first six are system stacks: whatever the machine already has, costing
   nothing and working with the network off. The fifteen below them are webfonts
   (see "webfont faces"), which is what a real choice of face requires — no
   laptop ships twenty reading faces covering Cyrillic and Greek. */
:root[data-font="serif"] {
  --font-body: Charter, "Iowan Old Style", Georgia, Cambria, "Times New Roman", serif;
  --font-head: var(--font-body);
}
:root[data-font="rounded"] {
  --font-body: ui-rounded, "SF Pro Rounded", "Nunito", -apple-system, sans-serif;
  --font-head: var(--font-body);
}
:root[data-font="humanist"] {
  --font-body: Optima, "Avenir Next", "Gill Sans", "Segoe UI", sans-serif;
  --font-head: var(--font-body);
}
:root[data-font="book"] {
  --font-body: Baskerville, "Hoefler Text", "Palatino", "Book Antiqua", Georgia, serif;
  --font-head: var(--font-body);
}
:root[data-font="mono"] {
  --font-body: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  --font-head: var(--font-body);
}
:root[data-font="typewriter"] {
  --font-body: "American Typewriter", "Courier New", ui-monospace, monospace;
  --font-head: var(--font-body);
}

/* ---- webfont faces ------------------------------------------------------
   The fifteen faces chosen off tools/fonts/index.html, where every candidate
   was shown setting real beeins text in every script the app has to render, so
   the choice was made against the sentences rather than against a name.
   app.js holds the matching table and requests the stylesheet for a face the
   moment it is applied; nothing here downloads until then.

   No face outside the Noto project covers Arabic, Devanagari, Ethiopic, Thai
   and CJK, so a typeface in beeins is a chain: the chosen face for Latin,
   Cyrillic, Greek and Vietnamese, and a genre-matched Noto for the scripts it
   does not reach. Genre-matched so a serif never falls back to a gothic —
   Persian under Chiron Sung HK gets Naskh, not the sans. The chosen face is
   always first, so the three faces that do cover a CJK script themselves
   (Chiron Sung HK, LXGW WenKai TC, Zen Maru Gothic) set it in their own hand.

   data-font-genre carries the genre, set by app.js beside data-font, so the
   chain is written five times instead of fifteen. */
:root[data-font-genre] {
  --font-body: var(--font-face), var(--noto), var(--cjk-zh), var(--cjk-ja), var(--cjk-ko), var(--font-generic);
  --font-head: var(--font-body);
}

/* Han is the one script whose fallback cannot be a single font: the same
   character is drawn differently in Chinese, Japanese and Korean, and a learner
   reading Japanese should not be shown the Chinese shapes. The chain above is
   the default order; these put the language of the text first wherever the app
   has said what that language is. */
:root[data-font-genre] [lang|="ja"] {
  font-family: var(--font-face), var(--noto), var(--cjk-ja), var(--cjk-zh), var(--cjk-ko), var(--font-generic);
}
:root[data-font-genre] [lang|="ko"] {
  font-family: var(--font-face), var(--noto), var(--cjk-ko), var(--cjk-zh), var(--cjk-ja), var(--font-generic);
}
:root[data-font-genre] [lang|="zh"] {
  font-family: var(--font-face), var(--noto), var(--cjk-zh), var(--cjk-ja), var(--cjk-ko), var(--font-generic);
}

:root[data-font-genre="sans"],
:root[data-font-genre="display"] {
  --noto: "Noto Sans Arabic", "Noto Nastaliq Urdu", "Noto Sans Devanagari", "Noto Sans Ethiopic", "Noto Sans Thai";
  --cjk-zh: "Noto Sans SC"; --cjk-ja: "Noto Sans JP"; --cjk-ko: "Noto Sans KR";
  --font-generic: sans-serif;
}
/* The monos borrow the sans chain: Noto ships no monospaced Arabic or Thai, and
   a fixed pitch is not worth tofu. Only the last resort differs. */
:root[data-font-genre="mono"] {
  --noto: "Noto Sans Arabic", "Noto Nastaliq Urdu", "Noto Sans Devanagari", "Noto Sans Ethiopic", "Noto Sans Thai";
  --cjk-zh: "Noto Sans SC"; --cjk-ja: "Noto Sans JP"; --cjk-ko: "Noto Sans KR";
  --font-generic: monospace;
}
:root[data-font-genre="serif"] {
  --noto: "Noto Naskh Arabic", "Noto Nastaliq Urdu", "Noto Serif Devanagari", "Noto Serif Ethiopic", "Noto Serif Thai";
  --cjk-zh: "Noto Serif SC"; --cjk-ja: "Noto Serif JP"; --cjk-ko: "Noto Serif KR";
  --font-generic: serif;
}
/* Thai Looped is the rounded Thai — the same softening the Latin has. M PLUS
   Rounded 1c is the only rounded CJK face there is, so it takes Japanese. */
:root[data-font-genre="rounded"] {
  --noto: "Noto Sans Arabic", "Noto Nastaliq Urdu", "Noto Sans Devanagari", "Noto Sans Ethiopic", "Noto Sans Thai Looped";
  --cjk-zh: "Noto Sans SC"; --cjk-ja: "M PLUS Rounded 1c"; --cjk-ko: "Noto Sans KR";
  --font-generic: sans-serif;
}

/* sans */
:root[data-font="roboto"]          { --font-face: "Roboto"; }
:root[data-font="source-sans-3"]   { --font-face: "Source Sans 3"; }
:root[data-font="geologica"]       { --font-face: "Geologica"; }
:root[data-font="alegreya-sans"]   { --font-face: "Alegreya Sans"; }
:root[data-font="libertinus-sans"] { --font-face: "Libertinus Sans"; }
/* Gidole and Didact Gothic ship a 400 and nothing else. Left at the default
   --head-weight the browser would smear one into a fake bold, which on a
   single-weight geometric face looks like a rendering fault. */
:root[data-font="gidole"]          { --font-face: "Gidole"; --head-weight: 400; }
:root[data-font="didact-gothic"]   { --font-face: "Didact Gothic"; --head-weight: 400; }

/* serif */
:root[data-font="chiron-sung-hk"]  { --font-face: "Chiron Sung HK"; }

/* mono */
:root[data-font="source-code-pro"] { --font-face: "Source Code Pro"; }
:root[data-font="victor-mono"]     { --font-face: "Victor Mono"; }

/* rounded */
:root[data-font="comfortaa"]       { --font-face: "Comfortaa"; }
:root[data-font="zen-maru-gothic"] { --font-face: "Zen Maru Gothic"; }
:root[data-font="playpen-sans"]    { --font-face: "Playpen Sans"; }

/* display */
:root[data-font="jura"]            { --font-face: "Jura"; }
:root[data-font="lxgw-wenkai-tc"]  { --font-face: "LXGW WenKai TC"; }

* { box-sizing: border-box; }

/* No visible scrollbars anywhere — vertical or horizontal. Everything still
   scrolls (wheel, trackpad, touch, keyboard); only the bars are hidden. */
* {
  scrollbar-width: none;        /* Firefox */
  -ms-overflow-style: none;     /* old Edge/IE */
}
*::-webkit-scrollbar { width: 0; height: 0; }   /* WebKit / Blink */

html, body {
  height: 100%;
  margin: 0;
}

body {
  font-family: var(--font-body);
  background: var(--page-bg);
  background-attachment: fixed;
  color: var(--text);
  display: flex;
  flex-direction: column;
  min-height: 100vh;
  /* Every page here is content to study, not to copy out — words, examples,
     grammar prose, media notes. Nothing in the app reads a selection back
     (no getSelection() anywhere), so there is no feature cost to turning it
     off wholesale rather than chasing it section by section. -webkit-
     touch-callout also suppresses iOS's long-press copy/lookup menu, which
     a plain user-select: none does not reach on its own. Re-enabled below
     for the two real text inputs, where select-to-edit still has to work. */
  -webkit-user-select: none;
  user-select: none;
  -webkit-touch-callout: none;
}

/* The chosen Background pattern's own fixed layer — see the comment above
   it in index.html for why it is a dedicated element rather than another
   `--page-bg` gradient layer (SVG-motif patterns use `mask-image` +
   `background-color`, which cannot compose inside the same `background:`
   shorthand a plain gradient layer would). `z-index: -1` is load-bearing,
   not decorative: a `position: fixed` element with `z-index: auto` still
   paints ABOVE every plain in-flow (static) descendant in its stacking
   context regardless of DOM order — being body's first child was not
   enough on its own, and without this the pattern rendered in front of
   every ordinarily-positioned control (search inputs, ghost tab buttons,
   icon buttons), opaque background-colors and all, which is what the
   "background pattern is overtaking controls" bug actually was. Invisible
   with no data-bgpattern set: no rule below matches, so it paints nothing. */
#bgPattern {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  background-repeat: repeat;
  -webkit-mask-repeat: repeat;
          mask-repeat: repeat;
  /* Every pattern above mixes --accent toward transparent at a fixed
     8-16% — fine for the app's original, fairly saturated accent colours,
     but a pale one (the pastel swatches) washes out to almost nothing at
     that opacity against an already-light page. refreshPatternOpacityBoost()
     (13-controls-marks-wiring.js) sets this per-swatch, light-mode only,
     rather than rewriting every pattern rule's own percentage above. */
  opacity: var(--pattern-opacity, 1);
}

/* Local/dev hosts only (see the detection script in index.html's <head>) —
   selection back on for a dev pass, off everywhere the app is actually
   deployed. #card re-asserts user-select: none on its own further down (it
   needs that year-round, live or local, so a slow touch-drag starting a
   swipe doesn't fire the browser's native text-selection instead) — this
   has to override that explicit declaration too, not just body's, or
   inheritance never reaches the actual word/example text at all. */
html[data-textselect="local"] body,
html[data-textselect="local"] #card {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

input, textarea {
  -webkit-user-select: text;
  user-select: text;
  -webkit-touch-callout: default;
}

#topbar {
  padding: 12px 24px;
  flex-shrink: 0;
}

#topbarMain {
  display: flex;
  flex-wrap: wrap;
  justify-content: space-between;
  align-items: center;
  gap: 8px 10px;
}

#topleft {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px 10px;
  min-width: 0;
}

/* One row on anything desktop-sized. The controls fit once the modes are
   icons, but only just, and an interface language with longer words would tip
   it back onto a second row — which costs 40px of vertical space on every
   screen to spare one label. So the row is told not to wrap and to scroll
   sideways in the rare case it genuinely cannot fit, the same behaviour the
   phone layout already uses. */
@media (min-width: 641px) {
  #topbarMain { flex-wrap: nowrap; }
  /* The "Topics" and "From" captions always show (user request); the row scrolls sideways if it cannot fit. */
  #topleft {
    flex-wrap: nowrap;
    overflow-x: auto;
    /* Explicit, not left to default to "visible" -- per the CSS overflow
       spec, pairing an "auto" x-axis with an unset y-axis silently computes
       the y-axis to "auto" too, making the whole row vertically
       scroll/drag-pannable on touch (user feedback, 2026-09-28: "why am I
       able to vertically scroll the toolbar"). Only sideways scroll is
       ever wanted here. */
    overflow-y: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
  }
  #topleft::-webkit-scrollbar { display: none; }
  #topleft > * { flex: 0 0 auto; }
  #topright { flex: 0 0 auto; }
  /* A chip scrolled half out of view otherwise gets clipped flat by the row's
     own edge — a hard, jagged cut through its rounded pill instead of a
     smooth fade. The mask tapers the last ~10px on that side to transparent
     so a partial chip dissolves into the page instead — but only the side
     there is actually more content on (markTopleftEdges() in app.js gates
     `.fadeL`/`.fadeR`, same convention as `.mScopes`). Applied unconditionally
     instead, this faded the last button's own right border into the page
     even when the whole row already fit with room to spare — a permanent,
     undeserved "shadow" on whichever control happened to be last, since
     #topleft has no explicit width and so ends exactly at its last child. */
  #topleft.fadeR {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 10px), transparent);
            mask-image: linear-gradient(to right, #000 calc(100% - 10px), transparent);
  }
  #topleft.fadeL {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 10px);
            mask-image: linear-gradient(to right, transparent, #000 10px);
  }
  #topleft.fadeL.fadeR {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 10px, #000 calc(100% - 10px), transparent);
            mask-image: linear-gradient(to right, transparent, #000 10px, #000 calc(100% - 10px), transparent);
  }

  /* A scrolling row clips anything absolutely positioned inside it, so a panel
     opened from the left group would be cut to the row's height and swallow
     its own clicks. Pin those to the viewport instead, at the corner of the
     button that opened them (app.js sets both offsets). The right group does
     not scroll, so its panels stay as they are.

     Targeted by the .tlPanel marker class rather than `#topleft .langDropPanel`
     — app.js portals an open panel to a direct child of <body> to dodge a
     `position: fixed` inside `overflow: auto` rendering bug (see the portal
     code), and once it's moved it is no longer a descendant of #topleft. An
     ancestor-scoped selector stops matching the instant that happens, so the
     panel would fall back to the base rule's `position: absolute` — anchored
     to <body>'s own top instead of the viewport — and render far down the
     page, invisible. The class travels with the element regardless of where
     it currently lives in the DOM, so the rule keeps applying. */
  .langDropPanel.tlPanel {
    position: fixed;
    top: var(--panel-top, 56px);
    left: var(--panel-left, 12px);
    right: auto;
    /* Escaping to fixed positioning (see above) also escaped the base
       min(60vh, 420px) cap, since a fixed panel measures against the
       viewport instead of #topleft — so the language list ran almost the
       full window height instead of the same short, scrollable panel every
       other dropdown gets. Re-apply the same cap, still bounded by however
       much room is actually below the button. */
    max-height: min(60vh, 420px, calc(100dvh - var(--panel-top, 56px) - 16px));
  }
}

#topright {
  display: flex;
  align-items: center;
  gap: 12px;
}

#settingsBtn,
#accountBtn,
#themeToggle {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1rem;
  line-height: 1;
  padding: 5px 8px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  /* --card-bg, not --card-tint — these sit directly on the fixed #bgPattern
     layer with no backdrop-filter behind them, so a glass skin's translucent
     tint would let the pattern show through instead of reading as a button. */
  background: var(--card-bg);
  color: var(--text);
  cursor: pointer;
}

/* Same brush-border treatment as every other standalone toolbar pill
   (.langDropBtn/#viewNav/.mScope) — rectangular, not rounded. #themeToggle
   is excluded: it isn't part of the top toolbar's button row (Design was
   removed, see bootDesign) and keeping its plain border here is harmless
   dead weight rather than a live surface. */
#settingsBtn,
#accountBtn {
  border: none;
  border-radius: 0 !important;
  position: relative;
}
#settingsBtn::before,
#accountBtn::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

/* The drawn gear/bar-chart icons get a fixed square so their size never
   depends on the surrounding text. */
#settingsBtn,
#accountBtn {
  width: 34px;
  height: 34px;
  padding: 0;
  color: var(--muted);
}
#settingsBtn svg, #accountBtn svg { width: 17px; height: 17px; display: block; }
#settingsBtn:hover, #accountBtn:hover { color: var(--text); }
#settingsBtn.open, #accountBtn.open { color: var(--accent); }

#settingsBtn:active,
#accountBtn:active,
#themeToggle:active { transform: scale(0.92); }

/* ---- the appearance tray ------------------------------------------------ */
#settingsWrap, #accountWrap { position: relative; display: flex; align-items: center; }

/* A column under the gear, right-aligned to it so it cannot push the page
   sideways near the window edge. Above the cards, below the dropdown panels
   its own controls open. */
#settingsTray {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 55;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  padding: 8px;
  min-width: 216px;
  max-width: min(280px, calc(100vw - 24px));
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.22);
}
#settingsTray[hidden] { display: none; }

/* The account tray is the same dropped column as the appearance tray, just
   under its own button and with far less inside it. */
#accountTray {
  position: absolute;
  top: calc(100% + 6px);
  right: 0;
  z-index: 55;
  display: flex;
  flex-direction: column;
  align-items: stretch;
  gap: 2px;
  padding: 8px;
  min-width: 260px;
  max-width: min(340px, calc(100vw - 24px));
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 16px;
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.22);
}
#accountTray[hidden] { display: none; }
/* Wider still once there's room to spare -- same breakpoint and reasoning
   as #settingsTray's own widening just below: a long email address or
   "Manage/cancel subscriptions" label (longer still in several interface
   languages) was wrapping/truncating for no reason once the screen had
   the width to just not do that (user feedback, 2026-10-02). */
@media (min-width: 641px) {
  #accountTray {
    min-width: 300px;
    max-width: min(400px, calc(100vw - 24px));
  }
}
/* Matches #settingsTray's own row font exactly (0.86rem/400, plain
   --text, no underline) rather than the sign-in gate's smaller/bolder
   underlined .authTextLink look -- this is the settings-style account
   menu, not the sign-in choice step, and reusing that class gave it the
   wrong tray's whole look (user feedback, 2026-10-02: "follow the font
   from normal settings" then "remove the underline ... make the colour
   follow the normal settings colour"). */
#accountTray .authTextLink {
  font-size: 0.86rem;
  font-weight: 400;
  color: var(--text);
  text-decoration: none;
}
/* Two rows: Google icon + email on top, password + submit-icon below --
   icons only on the buttons, no wrapping labels. Shared with .authForm/
   .authEmailInput/.authIconBtn/.authRow below, the gated-content overlay's
   own second sign-in form (gating-model-decision, buildInlineSignInForm in
   00-core-state.js) -- same rules, so both look identical. */
#authForm,
.authForm {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 8px 10px;
}
#authForm .authRow,
#resetPasswordForm .authRow,
.authForm .authRow {
  display: flex;
  align-items: center;
  gap: 6px;
}
/* The class selector above beats the UA [hidden] stylesheet rule on
   specificity, so toggling the hidden ATTRIBUTE on #authSignupRow/
   #authLoginRow (the sign-up/log-in steps of the sign-in form, 16-auth.js)
   would otherwise render regardless of it -- see hidden-attribute-
   specificity-trap, the identical bug shape as #accountSignedIn[hidden]
   below. */
#authSignupRow[hidden],
#authLoginRow[hidden] { display: none; }
/* The login step's own row holds Back, the stacked email+password fields,
   and Sign-in -- .authRow's own align-items:center then centers both
   buttons vertically against the whole two-line stack instead of each
   field's own line (user feedback). */
.authLoginInputs {
  flex: 1;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 6px;
}
/* Create an account / Log in, plain underlined text on one line, no button
   chrome or color (user feedback) -- reuses .authTextLink's own look via the
   authChoiceLink class below, just a touch larger since this is the gate's
   actual entry point rather than a secondary link. */
.authChoiceRow {
  text-align: center;
  padding: 4px 10px 2px;
  color: var(--muted);
}
.authChoiceLink { font-size: 0.95rem; font-weight: 600; }
/* This plain span had no font-size of its own, so it inherited a larger
   ambient size than the two links either side of it (user feedback) --
   matched to .authChoiceLink's explicit 0.95rem so all three read as one
   consistent line instead of "or" standing out as the biggest word.
   Two independent "Create an account or Log in" rows exist -- the
   account tray's own static markup (#authChoiceOrLabel, index.html) and
   the sign-in gate's JS-built one (buildInlineSignInForm, 00-core-
   state.js, a bare text node until now with nothing to select at all) --
   same fix needed on both. */
#authChoiceOrLabel, .authChoiceOrLabel { font-size: 0.95rem; }
/* The Google option -- present on the choice step AND the sign-up/log-in
   steps alike (user feedback), so it lives outside the per-step rows below
   rather than being duplicated into each one. */
#authProviders, .authProviders { padding: 6px 10px 2px; }
#authOrLabel, .authOrLabel {
  text-align: center;
  font-size: 0.76rem;
  color: var(--muted);
  margin-bottom: 6px;
}
.authProviderRow { display: flex; justify-content: center; }
#authSignupEmail,
#authSignupPassword,
#authLoginEmail,
#authLoginPassword,
#resetNewPassword,
.authEmailInput {
  flex: 1;
  min-width: 0;
  font: inherit;
  font-size: 0.86rem;
  padding: 6px 8px;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: transparent;
  color: inherit;
}
#authSignupPassword::placeholder, #authSignupEmail::placeholder, #authLoginEmail::placeholder, #authLoginPassword::placeholder, #resetNewPassword::placeholder, .authEmailInput::placeholder { color: var(--muted); opacity: 0.65; }
#authSignupPassword:focus, #authSignupEmail:focus, #authLoginEmail:focus, #authLoginPassword:focus, #resetNewPassword:focus, .authEmailInput:focus { border-color: var(--accent); outline: none; }
/* Applied by JS on a failed submit (see markAuthInputInvalid, 16-auth.js)
   in place of the browser's own native validation-bubble outline, which
   isn't reachable to restyle at all -- this is the custom replacement.
   Cleared as soon as the user edits the field again. */
#authSignupPassword.authInputInvalid, #authSignupEmail.authInputInvalid, #authLoginEmail.authInputInvalid, #authLoginPassword.authInputInvalid, #resetNewPassword.authInputInvalid, .authEmailInput.authInputInvalid {
  border-color: var(--bad);
}
#googleSignInBtn,
#authSignupBackBtn,
#authSignupContinueBtn,
#authLoginBackBtn,
#authLoginSubmitBtn,
#resetPasswordBtn,
#signOutBtn,
.authIconBtn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  padding: 0;
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card-tint);
  color: var(--muted);
  cursor: pointer;
}
#googleSignInBtn svg, #authSignupBackBtn svg, #authSignupContinueBtn svg, #authLoginBackBtn svg, #authLoginSubmitBtn svg, #resetPasswordBtn svg, #signOutBtn svg, .authIconBtn svg { width: 17px; height: 17px; display: block; }
#googleSignInBtn:hover, #authSignupBackBtn:hover, #authSignupContinueBtn:hover, #authLoginBackBtn:hover, #authLoginSubmitBtn:hover, #resetPasswordBtn:hover, #signOutBtn:hover, .authIconBtn:hover { border-color: color-mix(in srgb, var(--accent) 45%, var(--border)); color: var(--text); }
/* Sign-up / log-in steps: borderless fields with a hairline underline, a
   bare back arrow, and the submit as plain accent text wearing the same
   double pencil underline as Know / Don't know (.btnTxt). Beats the ID-level
   icon-button rules above via the extra #authForm / .authForm prefix. */
#authForm .authStepRow,
.authForm .authStepRow { flex-wrap: wrap; gap: 4px 12px; padding: 0 2px; }
#authForm .authStepRow .authLoginInputs,
.authForm .authStepRow .authLoginInputs { order: 1; flex: 1 0 100%; gap: 10px; }
#authForm .authStepRow input,
.authForm .authStepRow input {
  font-size: 1rem;
  padding: 9px 2px 7px;
  border: none;
  border-bottom: 1px solid var(--border);
  border-radius: 0;
  background: transparent;
  text-align: start;
}
#authForm .authStepRow input:focus,
.authForm .authStepRow input:focus { border-bottom-color: var(--accent); }
#authForm .authStepRow input.authInputInvalid,
.authForm .authStepRow input.authInputInvalid { border-bottom-color: var(--bad); }
#authForm .authStepRow .authBackBtn,
.authForm .authStepRow .authBackBtn {
  order: 2; width: auto; height: auto; padding: 6px 4px; border: none; background: none; color: var(--muted);
}
#authForm .authStepRow .authSubmitTxt,
.authForm .authStepRow .authSubmitTxt {
  order: 3; margin-inline-start: auto; width: auto; height: auto;
  padding: 6px 4px 18px; border: none; background: none; border-radius: 0;
  font: inherit; font-size: 1rem; font-weight: 600; color: var(--accent); cursor: pointer;
}
#authForm .authStepRow .authSubmitTxt:disabled,
.authForm .authStepRow .authSubmitTxt:disabled { opacity: 0.55; cursor: default; }
#authForm .authStepRow .authBackBtn:hover,
.authForm .authStepRow .authBackBtn:hover { color: var(--text); border: none; }
#authForm .authStepRow .authSubmitTxt::before,
.authForm .authStepRow .authSubmitTxt::before { content: none; }

/* Forgot-password: a plain text link, not an icon button -- no obvious
   icon for it, only shown during the log-in step (16-auth.js's
   showLoginStep/showChoiceStep/showSignupStep). Going back to the choice
   step is #authSignupBackBtn/#authLoginBackBtn, an icon button inside each
   step's own row instead of a second text link here. */
#authLinks,
.authLinks {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 8px;
  padding: 2px 10px 4px;
}
/* Same specificity trap as #authSignupRow/#authLoginRow above. */
#authLinks[hidden] { display: none; }
.authTextLink {
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 0.78rem;
  color: var(--muted);
  cursor: pointer;
  text-decoration: underline;
  text-underline-offset: 2px;
}
.authTextLink:hover { color: var(--text); }
.authTextLink:disabled { opacity: 0.55; cursor: default; text-decoration: none; }
.authTextLink:disabled:hover { color: var(--muted); }
/* Sits immediately before its button in DOM order -- .authLinks above is a
   row flexbox, so this reads as "to the button's left" in document order
   (mirrored correctly for RTL languages the same way the rest of the row
   already is, since flex direction follows `dir` automatically). Also used
   standalone right before the gate's own "Resend confirmation email" button
   (buildInlineSignInForm, 00-core-state.js) -- an ordinary inline sibling
   there, not a flex child, but plain inline flow puts it immediately to
   that button's left the same way. */
.authCooldownTimer {
  font-size: 0.78rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* The gate sign-in form's escape hatch out of its own terminal states
   (buildInlineSignInForm, 00-core-state.js) -- block + its own top margin
   so it reads as a clearly separate action below the resend link, not just
   another item crowded onto the same row (user feedback: "after some space
   below"). */
.authBackToSignIn { display: block; margin: 14px auto 0; }
#authInfo,
#resetSetInitialInfo {
  padding: 0 10px 8px;
  font-size: 0.8rem;
  color: var(--muted);
}
#authError,
#resetError {
  padding: 0 10px 8px;
  font-size: 0.8rem;
  color: var(--bad);
}
#accountResetPassword[hidden] { display: none; }
#resetBackToSignInBtn { margin: 0 10px 8px; }
/* Signed-in row: email truncates instead of wrapping the tray wider, the
   sign-out icon stays put beside it. */
#accountSignedIn {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 10px;
}
/* The flex rule above has higher specificity than the UA [hidden] rule (see
   the identical #settingsTray .settingToggle[hidden] precedent elsewhere in
   this file), so without this override it renders regardless of the
   attribute -- showing sign-out and sign-in at once. */
#accountSignedIn[hidden] { display: none; }
/* "Manage / cancel subscriptions" is a long label: give it its own row under
   the email + sign-out button instead of squeezing the address down to "na…". */
#accountSignedIn { flex-wrap: wrap; }
#signOutBtn { order: 2; }
#accountSubs { order: 3; flex: 0 0 100%; display: flex; flex-direction: column; gap: 8px; padding: 2px 0 6px; }
#accountSubs[hidden] { display: none; }
.subHeading { font-size: 0.72rem; letter-spacing: 0.06em; text-transform: uppercase; color: var(--muted); }
#unlockedDialog { position: fixed; inset: 0; z-index: 90; display: flex; align-items: center; justify-content: center; padding: 24px; background: rgba(0, 0, 0, 0.55); }
#unlockedDialog .dlgCard { max-width: 380px; text-align: center; }
#unlockedDialog h2 { margin: 0 0 6px; font-size: 1.25rem; }
#unlockedDialog .unlockedBody { margin: 0 0 16px; color: var(--muted); }
.unlockedList { list-style: none; margin: 0 0 20px; padding: 0; display: flex; flex-direction: column; gap: 10px; text-align: start; }
.unlockedList li { display: flex; align-items: center; gap: 10px; font-size: 0.94rem; }
.unlockedList svg { flex: none; width: 1.3em; height: 1.3em; color: var(--accent); }
#unlockedDialog .dlgBtns { justify-content: center; }
.subRow { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.subRowName { font-size: 0.9rem; color: var(--text); }
.subRowMeta { display: block; font-size: 0.75rem; color: var(--muted); }
#manageBillingBtn { order: 3; flex: 0 0 100%; text-align: start; padding: 2px 0 0; }
#changePasswordBtn { order: 3; flex: 0 0 100%; text-align: start; padding: 2px 0 0; }
#changePasswordForm { order: 3; flex: 0 0 100%; display: flex; flex-direction: column; gap: 6px; font-size: 0.75rem; color: var(--muted); }
#changePasswordForm[hidden], #changePasswordMsg[hidden] { display: none; }
#changePasswordCurrent, #changePasswordNew { width: 100%; box-sizing: border-box; font: inherit; font-size: 0.85rem; padding: 6px 8px; color: var(--text); background: transparent; border: 1px solid var(--border); border-radius: 6px; }
#changePasswordMsg { color: var(--text); }
#deleteAccountBtn { order: 4; flex: 0 0 100%; text-align: start; padding: 2px 0 0; }
#deleteAccountForm { order: 5; flex: 0 0 100%; display: flex; flex-direction: column; gap: 6px; font-size: 0.75rem; color: var(--muted); }
#deleteAccountForm[hidden], #deleteAccountError[hidden] { display: none; }
#deleteAccountEmail { width: 100%; box-sizing: border-box; font: inherit; font-size: 0.85rem; padding: 6px 8px; color: var(--text); background: transparent; border: 1px solid var(--border); border-radius: 6px; }
#deleteAccountError { color: var(--text); }
#deleteAccountConfirmBtn { color: var(--bad); }
#deleteAccountConfirmBtn:hover { color: var(--bad); }
#deleteAccountConfirmBtn:disabled { opacity: 0.45; }
.deleteAccountActions { display: flex; gap: 14px; }
#accountTray #changePasswordCancelBtn, #accountTray #deleteAccountCancelBtn { color: var(--muted); }
#accountTray #changePasswordCancelBtn:hover, #accountTray #deleteAccountCancelBtn:hover { color: var(--text); }
/* Plain --text, no underline -- same settings-tray look as .authTextLink
   just above (user feedback, 2026-10-02). */
#copyUserIdBtn { order: 7; flex: 0 0 100%; text-align: start; padding: 2px 0 0; font-size: 0.72rem; }
.accountLegal { order: 6; flex: 0 0 100%; font-size: 0.75rem; color: var(--text); padding-top: 6px; border-top: 1px solid var(--border); }
.accountLegal a { color: inherit; text-decoration: none; }
#accountEmail {
  flex: 1;
  min-width: 0;
  font-size: 0.8rem;
  color: var(--muted);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* On a phone the tray's width is worth keeping tight — there's little room
   to spare. On anything wider there is no such constraint, and the narrow
   column was wrapping a two/three-word toggle label (e.g. "Auto-open Word
   family" in a longer interface language) onto two or three lines for no
   reason. Widened again 2026-10-02 (user feedback) -- 340px still wrapped
   several full-sentence toggle labels ("Sačala pokazyvat' slova tol'ko na
   anglijskom", "Zabyt' vsë vyučennoe na ètom ustrojstve (…)") onto 2-3
   lines even at that width; 420px comfortably fits a full sentence on one
   line in every interface language tried. */
@media (min-width: 641px) {
  #settingsTray {
    min-width: 300px;
    max-width: min(420px, calc(100vw - 24px));
  }
}

/* A labelled row, not a bare circle: the outline that used to be a control's
   only visual claim to being a button is gone, and a hover tint does that
   job now, the same way a row in any list does. Each item reads left to
   right — icon, then what it is — instead of asking you to already know
   what a lone glyph means.
   align-items: flex-start rather than center — some interface languages
   (a Cyrillic toggle label naming both the target and a longer sentence)
   wrap to two or three lines in a tray this narrow, and a centered icon
   floats in the middle of that block instead of sitting next to the first
   line it belongs to. */
#settingsTray .langDropBtn,
#settingsTray #themeToggle,
#settingsTray .settingToggle {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  width: 100%;
  padding: 8px 10px;
  border: 0;
  border-radius: 10px;
  background: none;
  color: var(--text);
  font-size: 0.86rem;
  font-weight: 400;
  text-align: left;
  justify-content: flex-start;
}
/* No hover/open/active background tint here either -- the exact same
   .ldOpt/.ldEndo underline convention (see that rule's own comment)
   copied verbatim, not reinvented: .settingLbl drops flex:1 (was the root
   cause of two failed attempts below) and instead lets .ldCaret carry
   margin-left:auto to reach the far right of the row -- so the label goes
   back to being a plain shrink-to-fit inline-block, exactly like .ldEndo,
   and the --pencil-underline mask sizes itself against that instead of a
   flex-stretched box.
   Two follow-up attempts were tried and rejected before landing on this
   (user feedback each time): text-decoration:underline drew under each
   wrapped line's own text correctly, but wavy clashed with the mask's
   look elsewhere, and solid was colour-consistent but a perfectly
   straight geometric line -- neither is the same asset as
   --pencil-underline. Once .ldOpt/.ldEndo was re-checked (user: "it
   already works in Topics dropdown and anywhere else, why do you make a
   problem out of it"), the actual fix was just to stop giving this label
   flex:1 at all, the same way .ldEndo never had it. */
#settingsTray .settingToggle[hidden] { display: none; }
#settingsTray .ldFlags,
#themeToggleIcon,
#settingsTray .settingToggle svg {
  flex: none;
  width: 20px;
  font-size: 1.05rem;
  text-align: center;
  margin-top: 1px;
}
#settingsTray .settingToggle svg,
#themeToggleIcon svg { width: 18px; height: 18px; }
.settingLbl {
  position: relative;
  display: inline-block;
  line-height: 1.4;
  padding-top: 1px;
}
.settingLbl::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 6px;
  background-color: var(--accent);
  pointer-events: none;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.2s ease;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 26px 6px;
          mask-size: 26px 6px;
}
#settingsTray .langDropBtn:hover .settingLbl::after,
#settingsTray #themeToggle:hover .settingLbl::after,
#settingsTray .settingToggle:hover .settingLbl::after,
/* The row for whichever dropdown is currently open, e.g. Background while its
   panel is showing — mirrors .settingToggle.active below so an open dropdown
   reads the same way an on toggle already does. */
#settingsTray .langDropBtn.open .settingLbl::after {
  transform: scaleX(1);
}
/* Still shown, unlike before — there is room for it in a full-width row,
   and it says a dropdown is a dropdown rather than a plain toggle.
   margin-left:auto is what actually pushes it to the row's far end now
   that .settingLbl no longer stretches to get it there (see above) --
   same technique .ldSel already uses for its own trailing checkmark. */
#settingsTray .ldCaret { flex: none; color: var(--muted); margin-left: auto; }

/* Text/icon stay --text -- only the underline carries --accent, same
   #levelPanel .levelBtn.active precedent as every other selected-state fix
   this pass (previously recoloured the whole row to --accent + a tinted
   background, same bug class). */
#settingsTray .settingToggle.active .settingLbl::after,
#settingsTray .settingToggle[aria-pressed="true"] .settingLbl::after {
  transform: scaleX(1);
}

/* The panels belong to buttons inside a column, so they open beside the tray
   rather than under the button — under it is where the next button is. */
#settingsTray .langDropPanel {
  top: -7px;
  left: auto;
  right: calc(100% + 8px);
}

/* Font sits inside the Design panel as a child dropdown: the Font button and
   the two text-size buttons share one row, the face list opens beneath. */
.ldFontRow { display: flex; align-items: center; }
.ldFontRow #fontDropBtn { flex: 1; width: auto; min-width: 0; }
.fontSizeBtn {
  flex: none;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  border: 0;
  border-radius: 8px;
  background: none;
  color: var(--muted);
  font-size: 0.95rem;
  font-weight: 700;
  line-height: 1;
  cursor: pointer;
}
.fontSizeBtn:hover { color: var(--text); background: color-mix(in srgb, var(--accent) 12%, transparent); }
.fontSizeBtn:disabled { opacity: 0.35; cursor: not-allowed; background: none; }
#backgroundPanel #fontPanel {
  position: static;
  min-width: 0;
  width: auto;
  max-width: none;
  max-height: none;
  overflow: visible;
  padding: 0;
  border: 0;
  box-shadow: none;
  background: none;
}
#backgroundPanel #fontPanel[hidden] { display: none; }

/* The font button IS the specimen: one letter in the face you would get. */
#fontFace {
  font-family: var(--font-head);
  font-size: 1.02rem;
  font-weight: 700;
  line-height: 1;
}

/* The marks button wears the letter its language is known by — ä, ı, é — so
   the control announces which alphabet it governs before it is opened. */
#marksFace {
  font-family: var(--font-head);
  font-size: 1.05rem;
  line-height: 1;
}

/* A keyboard rather than a list. These options are one letter each: laid out
   in rows they read as the alphabet the language adds to the plain one, where
   a stack of full-width rows would only say the same thing down a column and
   make a dozen letters look like a dozen decisions. */
#marksPanel { min-width: 0; width: max-content; max-width: min(260px, calc(100vw - 24px)); }
/* A column of skin swatches, one emoji each, no label — the generic mobile
   panel treatment (stretched edge to edge for a list of names) left most of
   that width empty. Shrink-wrapped like #marksPanel above. */
/* Wider than the other shrink-wrapped panels (max-height uncapped from the
   shared .langDropPanel's 420px too) so the accent-slot picker's 49-swatch
   PRIMARY and SECONDARY grids both fit at a fuller row width without
   needing their own scrollbar — a taller/narrower panel just pushed both
   sections past the generic scroll cap for no reason, since there are only
   ever these two bounded sections here, unlike an open-ended list. */
#themePanel { min-width: 0; width: max-content; max-width: min(276px, calc(100vw - 24px)); max-height: min(90vh, 900px); padding: 8px; }
/* One font name per row, no swatch or flag beside it — the generic mobile
   panel treatment (stretched edge to edge) left most of that width empty
   here too. Shrink-wrapped like #themePanel above. */
.markNote {
  padding: 7px 8px 3px;
  color: var(--muted);
  font-size: 0.76rem;
  line-height: 1.4;
}
.markGrid {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  padding: 6px 8px 8px;
}
/* Off is a letter that is merely there; on is a letter with a ring around it.
   The outline is the whole signal, so nothing else about the button moves when
   it is switched — the row stays a row and the eye reads the rings straight
   off it. */
.markOpt {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 1.5px solid transparent;
  border-radius: 10px;
  background: var(--bg);
  color: var(--muted);
  font-family: var(--font-head);
  font-size: 1.02rem;
  line-height: 1;
  cursor: pointer;
  transition: color 0.12s ease, border-color 0.12s ease;
}
.markOpt:hover { color: var(--text); }
.markOpt.on {
  color: var(--text);
}
/* Outline-on-selected kept Herbarium-only -- see #settingsBtn.open's own
   comment above for why the other Designs drop the border-colour swap. */
[data-design="editorial"] .markOpt.on {
  border-color: var(--accent);
}

/* Each option shown in its own face: the list is its own specimen sheet, which
   is why it needs no genre headings above the rows. */
.ldFontName {
  font-size: 0.92rem;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}

/* Learning-target selector: a plain dropdown matching the Ask/Show ones,
   pinned top-left. Shows the current target's flag + endonym. */
#learnDrop .ldLearnName {
  font-size: 0.86rem;
  font-weight: 400;
  white-space: nowrap;
}

/* Cards-per-session selector: matched to the level-switch chip aesthetic. */
#sessionSize {
  font-size: 0.8rem;
  font-weight: 700;
  padding: 5px 8px;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: var(--card-bg);
  color: var(--text);
  cursor: pointer;
  letter-spacing: 0.02em;
}

#sessionSize.hidden { display: none; }

/* ---- language dropdowns (Ask / Show) ---------------------------------- */
#langDrops {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 6px 10px;
}

.langDrop { position: relative; }

/* Hand-drawn "brush border" (2026-09-17): every flat 1px border in the
   app is replaced by a small hand-wobbled line TILE, repeated along each
   edge (background-repeat: repeat-x/repeat-y at a small fixed tile size)
   rather than one big image stretched to fit -- stretching a single SVG
   via mask-size/background-size 100% 100% turned out to be genuinely
   broken in this Chromium build for any auto-width (shrink-to-fit)
   element: computed style reports the correct box, but the paint uses a
   stale/wrong size regardless (verified with a minimal repro -- a plain
   inline-block div with only text content, no flex, no pseudo-element,
   still failed to stretch). Repeating a small tile sidesteps it
   entirely. Deliberately RECTANGULAR (border-radius: 0), not rounded --
   corrected once already, do not round these again without being asked.
   Two ink variants (this Chromium build also doesn't resolve currentColor
   against the host page for an SVG *background*-image, only for a CSS
   *mask* -- so the color has to be baked in) matching editorial's own
   light/dark --text, swapped the same way the rest of the theme is. */
:root {
  --brush-h: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%228%22%20viewBox%3D%220%200%2040%208%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22g%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%220%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%231c1b19%22%20stop-opacity%3D%220%22/%3E%3Cstop%20offset%3D%220.3%22%20stop-color%3D%22%231c1b19%22%20stop-opacity%3D%220.10%22/%3E%3Cstop%20offset%3D%220.5%22%20stop-color%3D%22%231c1b19%22%20stop-opacity%3D%220.34%22/%3E%3Cstop%20offset%3D%220.7%22%20stop-color%3D%22%231c1b19%22%20stop-opacity%3D%220.10%22/%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%231c1b19%22%20stop-opacity%3D%220%22/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%2240%22%20height%3D%228%22%20fill%3D%22url%28%23g%29%22/%3E%3C/svg%3E');
  --brush-v: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%228%22%20height%3D%2240%22%20viewBox%3D%220%200%208%2040%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22g%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%220%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%231c1b19%22%20stop-opacity%3D%220%22/%3E%3Cstop%20offset%3D%220.3%22%20stop-color%3D%22%231c1b19%22%20stop-opacity%3D%220.10%22/%3E%3Cstop%20offset%3D%220.5%22%20stop-color%3D%22%231c1b19%22%20stop-opacity%3D%220.34%22/%3E%3Cstop%20offset%3D%220.7%22%20stop-color%3D%22%231c1b19%22%20stop-opacity%3D%220.10%22/%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%231c1b19%22%20stop-opacity%3D%220%22/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%228%22%20height%3D%2240%22%20fill%3D%22url%28%23g%29%22/%3E%3C/svg%3E');
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --brush-h: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%228%22%20viewBox%3D%220%200%2040%208%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22g%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%220%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220%22/%3E%3Cstop%20offset%3D%220.3%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220.10%22/%3E%3Cstop%20offset%3D%220.5%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220.34%22/%3E%3Cstop%20offset%3D%220.7%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220.10%22/%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220%22/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%2240%22%20height%3D%228%22%20fill%3D%22url%28%23g%29%22/%3E%3C/svg%3E');
    --brush-v: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%228%22%20height%3D%2240%22%20viewBox%3D%220%200%208%2040%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22g%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%220%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220%22/%3E%3Cstop%20offset%3D%220.3%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220.10%22/%3E%3Cstop%20offset%3D%220.5%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220.34%22/%3E%3Cstop%20offset%3D%220.7%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220.10%22/%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220%22/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%228%22%20height%3D%2240%22%20fill%3D%22url%28%23g%29%22/%3E%3C/svg%3E');
  }
}
:root[data-theme="dark"] {
  --brush-h: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%228%22%20viewBox%3D%220%200%2040%208%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22g%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%220%22%20y2%3D%221%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220%22/%3E%3Cstop%20offset%3D%220.3%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220.10%22/%3E%3Cstop%20offset%3D%220.5%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220.34%22/%3E%3Cstop%20offset%3D%220.7%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220.10%22/%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220%22/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%2240%22%20height%3D%228%22%20fill%3D%22url%28%23g%29%22/%3E%3C/svg%3E');
  --brush-v: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%228%22%20height%3D%2240%22%20viewBox%3D%220%200%208%2040%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22g%22%20x1%3D%220%22%20y1%3D%220%22%20x2%3D%221%22%20y2%3D%220%22%3E%3Cstop%20offset%3D%220%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220%22/%3E%3Cstop%20offset%3D%220.3%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220.10%22/%3E%3Cstop%20offset%3D%220.5%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220.34%22/%3E%3Cstop%20offset%3D%220.7%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220.10%22/%3E%3Cstop%20offset%3D%221%22%20stop-color%3D%22%23f3f0e7%22%20stop-opacity%3D%220%22/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect%20x%3D%220%22%20y%3D%220%22%20width%3D%228%22%20height%3D%2240%22%20fill%3D%22url%28%23g%29%22/%3E%3C/svg%3E');
}

/* A "selected" pencil-underline, used wherever a badge/tab used to show its
   selected state as a solid colour fill (user feedback, 2026-09-19: filled
   badges read too heavy, an underline in the accent colour was asked for
   instead, "pencil style" — everywhere this pattern recurs). One variant
   only, unlike --brush-h/-v above: this is consumed as a CSS *mask*, not a
   background, so only the shape's alpha matters, not its baked stroke
   colour — the actual colour comes from whatever `background-color`
   (typically currentColor) paints underneath the mask. Same seam-safe tile
   idea as --brush-h (starts/ends level so repeat-x has no seam). A brief
   2026-09-24 attempt at a bigger two-bump wobble (~third of the tile's
   height) read as too wavy (user feedback, 2026-09-25) — back to the
   original shallow single dip. */
:root {
  --pencil-underline: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%228%22%20viewBox%3D%220%200%2040%208%22%3E%3Cpath%20d%3D%22M0%204.00%20Q11.20%204.00%2020.00%203.17%20Q28.80%204.00%2040.00%204.00%22%20stroke%3D%22%23fff%22%20stroke-width%3D%221.4%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22/%3E%3Cpath%20d%3D%22M0%204.00%20Q11.20%204.00%2020.00%203.17%20Q28.80%204.00%2040.00%204.00%22%20stroke%3D%22%23fff%22%20stroke-width%3D%221.0%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20opacity%3D%220.7%22%20transform%3D%22translate%280%200.5%29%22/%3E%3C/svg%3E');
}
/* Every animated pencil-underline (transform: scaleX(0)->scaleX(1)) must draw
   in starting from the BEGINNING of the text it sits under and finish at its
   end -- never the middle (user feedback, 2026-09-25: ".viewBtnLabel was
   starting from the word middle"). transform-origin: left is that for a
   left-to-right reading order; a right-to-left one reads (and so must draw)
   the opposite way, hence the :dir(rtl) flip below for every one of these
   eight consumers (.ldEndo, .viewBtnLabel, #modePanel's and #levelPanel's
   panel-row labels, .formShowMe, .gTocTitle/.mTocName, .settingLbl).
   Currently inert -- nothing in the app sets dir="rtl" yet even when an
   RTL-scripted language (fa/ar_*/ur/prs/ps) is the interface language --
   but correct and ready for whenever that lands. */
:dir(rtl) .ldEndo::before,
:dir(rtl) .viewBtn .viewBtnLabel::after,
:dir(rtl) .settingLbl::after,
:dir(rtl) #modePanel .modeBtnLabel::after,
:dir(rtl) #levelPanel .levelBtn .levelBtnLabel::after,
:dir(rtl) .gTocTitle::after,
:dir(rtl) .mTocName .gTocUL,
:dir(rtl) .formShowMe::after { transform-origin: right; }

/* A hand-drawn checkmark, used as a mask (see --pencil-underline's own
   comment on why a mask needs only one variant, no baked colour) wherever
   CSS alone drew a plain "✓" glyph via `content` — .ldSel's own checkmark
   (CHECK_ICON_SVG, 00-core-state.js) is the JS-templated version of this
   same shape for spots that build a real innerHTML string instead. Same
   two-stroke shape both places, kept in sync by hand if either changes. */
:root {
  --check-mark-mask: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20d%3D%22M4.20%2013.00%20Q6.10%2015.30%208.60%2017.40%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222.2%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22/%3E%3Cpath%20d%3D%22M8.40%2017.20%20Q13.60%2011.20%2019.60%205.20%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222.2%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22/%3E%3C/svg%3E');
  /* The wrong-answer counterpart to --check-mark-mask, same hand-drawn
     two-stroke construction as a wobbly "X" instead. */
  --cross-mark-mask: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20viewBox%3D%220%200%2024%2024%22%3E%3Cpath%20d%3D%22M4.50%204.80%20Q11.60%2012.30%2019.50%2019.20%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222.2%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22/%3E%3Cpath%20d%3D%22M19.50%204.80%20Q12.10%2011.90%204.50%2019.20%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222.2%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22/%3E%3C/svg%3E');
}

/* The brush border's H/V tiles each run edge-to-edge, so at every corner
   BOTH the horizontal and vertical line are independently present and
   cross each other there — reads as connected lines are supposed to meet
   at a corner, not cross through one another (user feedback, 2026-09-19,
   screenshot). A shape can't be un-drawn per background-layer with plain
   CSS (a mask applies to a whole element's composited paint, not to one
   layer of it selectively), so instead this masks a small hole at each of
   the ::before's own 4 corners, applied on top of both lines together —
   both lines stop just short of actually meeting, a small deliberate gap
   rather than a crossing. mask-composite: intersect on layers 2-4 is
   required — without it, the default "add" compositing lets whichever
   corner's own opaque region show back through every OTHER corner's hole. */
:root {
  --brush-corner-mask:
    radial-gradient(circle 6px at 0 0, transparent 4.5px, black 6px),
    radial-gradient(circle 6px at 100% 0, transparent 4.5px, black 6px),
    radial-gradient(circle 6px at 0 100%, transparent 4.5px, black 6px),
    radial-gradient(circle 6px at 100% 100%, transparent 4.5px, black 6px);
}

.langDropBtn {
  display: inline-flex;
  align-items: center;
  gap: 7px;
  max-width: 100%;
  padding: 6px 10px;
  position: relative;
  border: none;
  border-radius: 0 !important;
  /* --card-bg, not --card-tint — see #settingsBtn above: no backdrop-filter
     sits behind this pill, so a glass skin's translucent tint let the fixed
     background pattern show straight through it. */
  background: var(--card-bg);
  color: var(--text);
  font: inherit;
  cursor: pointer;
}
.langDropBtn::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
/* NOT here: #settingsTray reuses .langDropBtn for its own plain list rows
   (Colour/Interface language/Background/Font/Marked letters), already
   deliberately borderless (#settingsTray .langDropBtn's own `border: 0`,
   below) — a brush border on every stacked row read as a pile of separate
   boxes, not the one list it is. Whole standalone dropdown pills (the top
   bar) keep it; individual options inside a list of them don't. */
#settingsTray .langDropBtn::before { content: none; }

/* Every pill/icon-button control across the top bar — both rows — shares one
   explicit 34px height, matching #mediaLearnBtn's own fixed circle. This
   file had TWO different "internally consistent" height clusters that were
   never reconciled with each other (this 34px one, and a separate 31px one
   built around #levelNav/#modeNav — see .modeBtn's own comment for that
   half), plus several controls whose height was left to fall out of
   whatever their own padding/font-size happened to compute, landing a px or
   two off either cluster. Explicit height beats padding math every time —
   that's what kept causing near-misses here — so every one of these is
   pinned directly rather than tuned via padding again. */
#topicDropBtn,
#learnDropBtn, #showDropBtn, #mediaDropBtn, #viewNav,
#levelDropBtn, #modeDropBtn, #cshLevelNav, .mSchrTabs, #schrAufgabeNav { height: 34px; }

.ldLabel {
  font-size: 0.62rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
  cursor: help;
  user-select: none;
}
/* Top bar: every label ("From", "Topics", the target name, Study/Media, the
   level) is one size, regular weight, normal case (user request 2026-10-01). */
#lblShow, #lblTopics {
  font-size: 0.86rem;
  text-transform: none;
  letter-spacing: normal;
}

/* #levelFace carries .ldLabel too, but unlike every other .ldLabel use (a
   small muted caption sitting next to the real value, e.g. "topics" beside
   the topic flags) it IS the button's only value -- "A1"/"B1"/etc -- so the
   tiny muted caption treatment above made the button's one visible
   character read as faint, and the wide, near-empty-looking padding around
   such small/light text was what made #levelDropBtn look oversized for a
   2-character value. Solid text + tighter padding fixes both complaints at
   once without touching every other .ldLabel caption use. */
#levelFace {
  font-size: 0.86rem;
  text-transform: none;
  letter-spacing: normal;
  color: var(--text);
  cursor: pointer;
}
#levelDropBtn { padding: 6px 8px; gap: 7px; }

.ldFlags {
  display: inline-block;
  max-width: 132px;
  overflow: hidden;
  white-space: nowrap;
  font-size: 0.98rem;
  line-height: 1;
  letter-spacing: 1px;
}

/* The media picker's face is a title, not a row of flags: the flag treatment
   (letter-spaced, clipped at 132px, faded out at the right) turns a word into
   something that looks broken. */
.ldTitle {
  max-width: 210px;
  letter-spacing: normal;
  font-size: 0.9rem;
  line-height: 1.3;
  text-overflow: ellipsis;
  -webkit-mask-image: none;
          mask-image: none;
}

.ldFlags svg,
.ldTitle svg {
  vertical-align: middle;
}
/* A multi-select face (several mode/topic icons strung together into one
   .ldFlags, see updateModeNav/updateTopicFace) packed them edge-to-edge
   with nothing between — read as "stuck together" (user feedback,
   2026-09-19). Only between consecutive icons, so a single icon or a
   flag+text pairing is untouched. */
.ldFlags svg + svg { margin-left: 4px; }

.ldCaret {
  font-size: 0.65rem;
  color: var(--muted);
  transition: transform 0.15s ease;
}
.langDropBtn.open .ldCaret { transform: rotate(180deg); }

.langDropPanel {
  position: absolute;
  top: calc(100% + 5px);
  left: 0;
  z-index: 60;
  min-width: 224px;
  max-width: min(320px, calc(100vw - 24px));
  max-height: min(60vh, 420px);
  overflow-y: auto;
  /* Scrolling to the end of a short panel (or bouncing at the top of a long
     one) used to hand the gesture on to whatever was underneath — on a
     phone, that is the page itself, and the page handed it on to the
     browser's own pull-to-refresh. Contained here, the scroll just stops. */
  overscroll-behavior-y: contain;
  padding: 5px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: 14px;
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.22);
}
/* Wider than the other dropdown panels: a 31-pattern swatch grid needs the
   extra columns, or it wraps into a cramped, unevenly-filled strip. */
#backgroundPanel { max-width: min(360px, calc(100vw - 24px)); width: max-content; max-height: min(80vh, 640px); padding: 8px; }
#backgroundPanel .ldDesignHead { margin-bottom: 9px; }
/* Wider still: unlike the Ask/Show/Learn lists (short endonyms, room to
   spare), several of the 45 entries here run long on their own —
   "Deutsch (Deutschland)", "Português (Portugal)", "Español rioplatense"
   — and the default width wrapped them onto two lines. The ID beats the
   mobile media query's own (lower-specificity) .langDropPanel override
   further down, so this one declaration covers both. */
#interfaceLangPanel { max-width: min(460px, calc(100vw - 24px)); }
/* Narrower, not wider: every row here is just a 2-character level code
   (A1-C2) plus a checkmark, unlike the other panels' translated labels or
   language names that need the shared 224px min-width. Left at the shared
   width, .levelBtn's width:100% + margin-left:auto checkmark stretched a
   short "A1 ✓" row across a mostly-empty box. */
#levelPanel { min-width: 0; width: 130px; }
/* Also narrower than the shared 224px, but NOT a fixed width like
   #levelPanel above: the study-mode names (Cards/Blanks/Listen) translate
   to very different lengths across languages, so this shrinks to
   whichever row (icon + label + checkmark) is actually widest instead of
   assuming a size, capped by .langDropPanel's own shared max-width for
   the rare very-long translation. A modest min-width keeps short rows
   (a 5-letter label + checkmark) from looking cramped. */
#modePanel { min-width: 180px; width: max-content; }
.langDropPanel[hidden] { display: none; }

.ldOpt {
  position: relative;
  display: flex;
  align-items: center;
  gap: 10px;
  width: 100%;
  padding: 8px 9px;
  border: none;
  border-radius: 9px;
  background: transparent;
  color: var(--text);
  font: inherit;
  text-align: left;
  cursor: pointer;
}
/* No hover/selected background tint — a hand-drawn underline under just the
   row's own text instead (user feedback, 2026-09-24: "make sure the
   highlight design is gone on hover, and click", "underline only goes as
   far as the text", "colour must correspond to the first/main colour of
   the palette" — i.e. var(--accent), not currentColor, since a row's own
   text can be --muted/--text depending on state). Every option list —
   single-select (theme, learn-language, media section) or multi-select
   (show/title languages, topics) — marks its checked row the same way: no
   leading checkbox square, no background, just this underline plus the
   trailing check (.ldSel). One convention app-wide beats a different
   affordance per list. */
.ldEndo {
  position: relative;
  display: inline-block;
}
.ldEndo::before {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 6px;
  background-color: var(--accent);
  pointer-events: none;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.2s ease;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 26px 6px;
          mask-size: 26px 6px;
}
.ldOpt:hover .ldEndo::before,
.ldOpt[aria-checked="true"] .ldEndo::before,
.ldSingle.current .ldEndo::before { transform: scaleX(1); }

.ldSingle { padding-left: 11px; }
.ldSel {
  flex-shrink: 0;
  margin-left: auto;
  color: var(--accent);
  font-weight: 700;
}
.ldSel svg { width: 1.05em; height: 1.05em; display: inline-block; vertical-align: middle; }

.ldFlag { flex-shrink: 0; font-size: 1.2rem; line-height: 1; }

/* Emoji-only single-select rows (the colour-theme picker). */
.ldEmoji .ldFlag { font-size: 1.35rem; }
#sessionPanel, #themePanel { min-width: 92px; }

/* A skin's swatch: the accent colour itself as a dot, not an emoji — the
   picker used to mark each skin with an arbitrary face, but that told you
   nothing about the actual colour, and picking one is a colour decision.
   Square variant (ldSwatchSquare) is unused today but kept generic rather
   than named "ldSwatchRound", in case a future picker (e.g. an image
   thumbnail) wants the same sizing with straight corners instead. */
.ldSwatch {
  display: inline-block;
  flex-shrink: 0;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
}
.ldOpt .ldSwatch { width: 18px; height: 18px; }
.ldSwatchSquare { border-radius: 5px; }
#sessionFace { font-variant-numeric: tabular-nums; }

/* A design's own accent-slot picker (see renderAccentSlotsPanel in
   13-controls-marks-wiring.js) — one labelled row per slot, each a strip of
   small swatch buttons drawn from that design's shared palette. Lives in
   the same #themePanel a plain skin list would otherwise fill; the two
   never show at once, since a design either has slots or it doesn't. */
.ldSlotRow { padding: 12px 8px 16px; }
.ldSlotRow:not(:last-child) { border-bottom: 1px solid var(--border); }
.ldSlotLbl {
  font-size: 0.72rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  color: var(--muted);
  margin-bottom: 7px;
}
.ldSlotStrip { display: flex; flex-wrap: wrap; gap: 8px; }
.ldSwatchBtn {
  width: 22px;
  height: 22px;
  border-radius: 6px;
  border: 2px solid transparent;
  padding: 0;
  cursor: pointer;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
}
.ldSwatchBtn.current { border-color: var(--text); }
/* A native, standard colour-input square sits at the end of each slot's
   strip, same footprint as a swatch button — the browser's own picker UI
   handles the rest (see resolvedSlotAccent/setCustomSlotSwatch,
   13-controls-marks-wiring.js). The ::-webkit-/-moz- pseudo-elements strip
   each engine's own default inset padding/border so it reads as one clean
   square like its neighbours, not a swatch-in-a-frame. */
.ldColorInput {
  width: 22px;
  height: 22px;
  padding: 0;
  border: none;
  border-radius: 6px;
  cursor: pointer;
  background: none;
  box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15);
}
.ldColorInput::-webkit-color-swatch-wrapper { padding: 0; border-radius: 6px; }
.ldColorInput::-webkit-color-swatch { border: none; border-radius: 6px; }
.ldColorInput::-moz-color-swatch { border: none; border-radius: 6px; }
.ldSlotLblRow { display: flex; align-items: center; justify-content: space-between; gap: 8px; margin-bottom: 7px; }
.ldSlotLblRow .ldSlotLbl { margin-bottom: 0; }
.ldCopyPrimaryBtn {
  display: flex;
  align-items: center;
  gap: 4px;
  flex-shrink: 0;
  white-space: nowrap;
  text-transform: none;
  letter-spacing: normal;
  font-size: 0.68rem;
  font-weight: 500;
  text-decoration: none;
}
.ldCopyPrimaryBtn svg { width: 12px; height: 12px; flex-shrink: 0; }
.ldCopyPrimaryBtn:hover { text-decoration: underline; }

/* Collapsed by default — see renderAccentSlotsPanel's own comment on why
   the raw colour-input square hides behind this rather than always showing
   next to the curated swatch grid. */
.ldCustomWrap { margin-top: 8px; }
.ldCustomToggle {
  display: flex;
  align-items: center;
  gap: 5px;
  background: none;
  border: none;
  padding: 0;
  font: inherit;
  font-size: 0.72rem;
  font-weight: 500;
  color: var(--muted);
  cursor: pointer;
}
.ldCustomToggle svg { width: 13px; height: 13px; flex-shrink: 0; }
.ldCustomToggle:hover { color: var(--text); }
.ldCustomBody { margin-top: 8px; }
.ldCustomBody[hidden] { display: none; }

.ldNames {
  display: flex;
  flex-direction: column;
  min-width: 0;
  line-height: 1.25;
}
.ldEndo { font-size: 0.95rem; font-weight: 400; }
/* Theme names read as a colour/gemstone list, not language endonyms — the
   same bold weight the language pickers use (a name you're hunting for
   among many) reads as shouting for a swatch you're just scanning by eye. */
#themePanel .ldEndo { font-weight: 400; }
/* Same call for the Interface Language list — 45 rows is already the
   longest list in the tray; bold on every single one just adds visual
   weight across the board rather than helping any one row stand out.
   font-size matched to the tray row's own text (#settingsTray
   .langDropBtn's 0.86rem) too -- .ldEndo's 0.95rem default read
   noticeably larger than "Interface language" on the button that opens
   this very panel. */
#interfaceLangPanel .ldEndo { font-weight: 400; font-size: 0.86rem; }

/* German in the "show" set is always on — you're learning it. */
.ldOpt.locked { cursor: default; }

/* A language that covers almost none of the level you have picked. Dimmed, not
   disabled: someone whose only language this is would otherwise be shut out of
   the level. The dot says "there is something to know here" without shouting. */
.ldOpt.thin .ldNames { opacity: 0.45; }
.ldOpt.thin .ldFlag { opacity: 0.5; }
.ldOpt.thin .ldEndo::after {
  content: "•";
  margin-inline-start: 6px;
  color: var(--muted);
  font-weight: 400;
}

@keyframes flagShake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-3px); }
  75% { transform: translateX(3px); }
}
.ldOpt.shake { animation: flagShake 0.3s; }

#progress {
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  font-size: 0.95rem;
}

/* ---- view toggle (Cards / Grammar) ------------------------------------ */
#viewNav {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  position: relative;
  border: none;
  border-radius: 0 !important;
  background: var(--card-bg);
}
#viewNav::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

.viewBtn {
  display: inline-flex;
  align-items: center;
  font-size: 0.86rem;
  font-weight: 400;
  padding: 5px 12px;
  border: none;
  border-radius: 0 !important;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
}

/* The icon exists in the markup for the phone layout below; on anything
   wider the text label alone is what these tabs have always shown. */
.viewBtnIcon { display: none; width: 1.05em; height: 1.05em; }

/* Selected reads as a hand-drawn underline in the accent colour, not a
   solid fill (user feedback, 2026-09-19 — see --pencil-underline above).
   Text/icon colour stays --text, not --accent -- only the underline itself
   should carry the accent colour (user feedback, 2026-09-25, screenshot
   comparison against #levelPanel's .levelBtn.active, which already got
   this right: "colour of text and icons shouldn't change as they get
   selected"). */
.viewBtn.active {
  background: transparent;
  color: var(--text);
}
/* Scoped to .viewBtnLabel (the text itself), not .viewBtn (the padded
   button box) -- underline must only run as wide as the text it's under,
   never the whole control (user feedback, 2026-09-24). The mask lives on
   the label always, not just while .active, so a still-inactive tab can
   draw the same underline in on hover -- scaleX(0) at rest, same
   draw-in-not-fade-in transition as #levelPanel/.formShowMe's own
   pencil-underline, locked open by .active (user feedback, 2026-09-24:
   "hovering must produce an animation" -- it used to just pop in). */
.viewBtn .viewBtnLabel { position: relative; display: inline-block; }
.viewBtn .viewBtnLabel::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -4px;
  height: 6px;
  background-color: var(--accent);
  pointer-events: none;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.2s ease;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 34px 7px;
          mask-size: 34px 7px;
  -webkit-mask-position: center;
          mask-position: center;
}
.viewBtn:hover .viewBtnLabel::after,
.viewBtn.active .viewBtnLabel::after { transform: scaleX(1); }

/* ---- study-mode toggle (Cards / Blanks / Listen) --------------------- */
/* #modeNav is now the .langDrop wrapper (2026-09-16, was the inline icon
   row itself) — the four .modeBtn buttons live in #modePanel now, so this
   ID no longer needs its own display/gap/border/background rule, .langDrop
   already gives it that shape (#topicDrop's shared look). */

/* Icon buttons: square-ish and big enough to read the glyph. The name lives
   in the tooltip AND, on hover/focus, in a label that slides out beside the
   icon — .modeBtnLabel is width-collapsed to nothing until then, so the
   button (and the glued #modeNav strip around it) only widens for the one
   being pointed at. Only true inside #modePanel's OWN dropdown row shape
   below overrides this back to always-visible, full-width rows — this
   compact version still applies wherever else .modeBtn is used, if ever. */
.modeBtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* The global button{} rule's gap:8px is otherwise inherited here too —
     harmless while the icon was the only flex child, but now .modeBtnLabel
     is a second one, so that phantom 8px reserved a gap even while the
     label sits collapsed to max-width:0, shoving the icon off-centre and
     eating into the 25px box. The label's own margin-left already supplies
     the icon/label spacing on hover, so this must stay 0 in every state. */
  gap: 0;
  /* #modeNav's own 2px padding + 1px border (both sides) add 6px, so 28px
     here makes the whole group exactly the top bar's shared 34px (see
     #topicDropBtn's shared rule) — every control in this row reads as one
     continuous toolbar instead of a slightly-taller strip of icons between
     two shorter pill groups. A min-width (not width) keeps that math for
     the collapsed state while still letting the button grow to fit its
     label on hover. */
  min-width: 28px;
  height: 28px;
  line-height: 1;
  font-weight: 600;
  padding: 0;
  border: none;
  border-radius: 8px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  opacity: 0.55;
  transition: opacity 0.12s ease, background 0.12s ease, color 0.12s ease, padding 0.2s ease;
}
.modeBtn svg { width: 15px; height: 15px; display: block; flex-shrink: 0; }
.modeBtn:hover, .modeBtn:focus-visible { opacity: 0.85; padding: 0 8px 0 6px; }
/* A stroked SVG inherits currentColor, so both the accent background AND the
   icon's own colour now actually shift for the active mode — an emoji kept
   its own colours regardless of either. */
.modeBtn.active {
  opacity: 1;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 18%, transparent);
}
.modeBtnLabel {
  max-width: 0;
  overflow: hidden;
  opacity: 0;
  white-space: nowrap;
  font-size: 0.78rem;
  font-weight: 600;
  margin-left: 0;
  /* Much slower than a typical hover reveal on purpose (user feedback) --
     the label stretching into view is meant to read as a deliberate
     unfurl, not a snap. */
  transition: max-width 1.1s ease, opacity 0.8s ease, margin-left 1.1s ease;
}
.modeBtn:hover .modeBtnLabel, .modeBtn:focus-visible .modeBtnLabel {
  max-width: 110px;
  opacity: 1;
  margin-left: 5px;
}

/* Inside #modePanel (the dropdown's own list, 2026-09-16), a row reads its
   name always, not on hover — a closed dropdown already hides the whole
   list, so the hover-reveal .modeBtnLabel above (built for four icons
   glued into one visible strip) has nothing left to save space against.
   Same full-width, left-aligned dropdown-row shape every other list in
   the app uses, plus a trailing check on the active ones instead of the
   background-tint-only look .modeBtn.active keeps everywhere else. */
#modePanel .modeBtn {
  /* .modeBtn's base display is inline-flex (right for the compact icon
     strip this class is normally used in, where several sit side by side)
     -- left as inline-flex here, four of these inline-level 100%-width
     boxes made the PANEL's own max-content size balloon to fit them
     side by side without wrapping (~375px+), not the width of any one
     row. #levelPanel's rows already set display:flex for the same
     full-width dropdown-row shape; this needed the same override. */
  display: flex;
  width: 100%;
  min-width: 0;
  height: auto;
  justify-content: flex-start;
  padding: 8px 9px;
  opacity: 1;
  border-radius: 9px;
  /* Overrides the generic .modeBtn.active's own 18% accent tint below --
     that one's for the OTHER context .modeBtn is used in (the compact
     hover-reveal icon strip), not this dropdown-row one. */
  background: none;
}
/* ...and its color: var(--accent) too -- text/icon stay --text here, same
   #levelPanel .levelBtn.active precedent (user feedback, 2026-09-25: "colour
   of text and icons shouldn't change as they get selected", only the
   underline/checkmark should). */
#modePanel .modeBtn.active { color: var(--text); }
/* No hover/active background tint -- a hand-drawn underline under just the
   label text instead (user feedback, 2026-09-24). */
#modePanel .modeBtnLabel {
  position: relative;
  display: inline-block;
  max-width: none;
  overflow: visible;
  opacity: 1;
  font-size: 1rem;
  font-weight: 400;
  margin-left: 8px;
  transition: none;
}
#modePanel .modeBtnLabel::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 6px;
  background-color: var(--accent);
  pointer-events: none;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.2s ease;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 26px 6px;
          mask-size: 26px 6px;
}
#modePanel .modeBtn:hover .modeBtnLabel::after,
#modePanel .modeBtn:focus-visible .modeBtnLabel::after,
#modePanel .modeBtn.active .modeBtnLabel::after { transform: scaleX(1); }
/* The checkmark belongs on .modeBtn itself (the flex ROW), not on
   .modeBtnLabel (just the text span) -- float:right on the label only
   floated the check to the text's OWN right edge, landing it glued right
   after a short word like "Cards" instead of at the row's far end. As a
   flex child of .modeBtn with margin-left:auto, it now lands at the end
   of the row every time, same technique #levelPanel's checkmark uses. */
#modePanel .modeBtn.active::after {
  content: "";
  margin-left: auto;
  padding-left: 12px;
  width: 13px;
  height: 13px;
  background-color: var(--accent);
  /* content-box, not the mask default (border-box) -- otherwise the check
     shape paints into the padding-left gap meant as pure spacing instead
     of staying confined to its own box. */
  -webkit-mask-origin: content-box;
          mask-origin: content-box;
  -webkit-mask-image: var(--check-mark-mask);
          mask-image: var(--check-mark-mask);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-position: center;
          mask-position: center;
}


/* Blanks-mode prompt: a real example sentence with the target word removed and
   a text input in its place; type the word and press Enter to complete it. A
   translation clue sits underneath. Overrides the big headword sizing. */
.clozePrompt { font-size: 1.5rem; line-height: 1.5; }
.clozeSentence { font-weight: var(--head-weight); }
/* The pencil-underline mask (see .viewBtn.active/--pencil-underline) can't
   live on the <input> itself -- ::before/::after render nothing on a
   replaced element -- so it lives on this wrapping span instead
   (wrapClozeInput() in 04-rendering-marks-synonyms.js), matching a straight
   solid bar with a hand-drawn one (user feedback, 2026-09-19: "change the
   style of underline to match the selected button underline"). Deliberately
   no width/flex sizing of its own: .clozeInput keeps ALL of its own sizing
   below (base + .clozeInputBlock/.clozeInputWide + the input-event resize
   listener, all unchanged) and this shrink-wraps to whatever size that
   resolves to, inline-block hugging its one child exactly. Giving the
   wrapper its own competing width was tried first and dropped — the input's
   width and the wrapper's would drift apart across the 3+ different
   contexts .clozeInput is used in (an inline sentence gap, a standalone
   prompt, a full-width Listen row). */
.clozeInputWrap {
  display: inline-block;
  position: relative;
  vertical-align: bottom;
  color: var(--accent);
}
.clozeInputWrap::after {
  content: "";
  position: absolute;
  left: 2px;
  right: 2px;
  bottom: 0;
  height: 7px;
  background-color: currentColor;
  pointer-events: none;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 34px 7px;
          mask-size: 34px 7px;
  -webkit-mask-position: center;
          mask-position: center;
}
/* .listenRow.listenRowWide needs ITS flex-growing direct child to be the
   wrapper now, not the input inside it — mirrors .clozeInputWide's own
   flex:1 1 auto below. */
.listenRow.listenRowWide .clozeInputWrap { flex: 1 1 auto; min-width: 0; }
.clozeInput {
  font-family: inherit;
  font-size: 1em;
  font-weight: 600;
  text-align: center;
  width: 6em;
  /* An empty box has nothing else marking where it sits (no placeholder, no
     native chrome — background is transparent by design) other than this
     rule, so it can't be allowed to render thinner than the line needs to
     read as a box at all, regardless of how short the expected answer is. */
  min-width: 3em;
  max-width: 60vw;
  margin: 0 0.15em;
  padding: 0 0.2em 5px;
  color: inherit;
  background: transparent;
  border: none;
  outline: none;
  /* Without this, some browsers' native text-field chrome (an inset shadow,
     a platform-drawn edge) can compete with or partly obscure the custom
     border/background above instead of fully yielding to them. */
  appearance: none;
  -webkit-appearance: none;
}
.clozeClue {
  margin-top: 12px;
  font-family: var(--font-body, inherit);
  font-size: 0.95rem;
  font-weight: 400;
  color: var(--muted);
}
/* The completed answer, once revealed — highlighted; green when the typed
   guess matched. The learner's wrong guess is shown beneath in red.
   Pencil-underline mask, not a flat border-bottom (same swap as
   .clozeInputWrap above) -- background-color: currentColor means the mask
   itself needs no per-state colour of its own, it just follows whichever
   color the state below already sets. */
.clozeAnswer {
  position: relative;
  font-weight: 700;
  color: var(--accent);
  padding: 0 0.1em 5px;
}
.clozeAnswer::after {
  content: "";
  position: absolute;
  left: 1px;
  right: 1px;
  bottom: 0;
  height: 6px;
  background-color: currentColor;
  pointer-events: none;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 30px 6px;
          mask-size: 30px 6px;
  -webkit-mask-position: center;
          mask-position: center;
}
.clozeAnswer.clozeOk { color: #16a34a; }
.clozeAnswer.clozeBad { color: #dc2626; }
.clozeYou {
  margin-top: 10px;
  font-size: 0.95rem;
  font-weight: 600;
  color: #dc2626;
}
/* A near miss — the right answer with a slip in it. Scored as remembered, so
   it is amber rather than red. */
.clozeNear {
  margin-top: 10px;
  font-size: 0.95rem;
  font-weight: 600;
  color: #d97706;
}
/* A synonym of the card's word, where the prompt fitted both. Nothing about it
   is wrong, so it is green like the answer above it rather than struck out; the
   note underneath is what does the teaching. */
.clozeSyn {
  margin-top: 10px;
  font-size: 0.95rem;
  font-weight: 600;
  color: #16a34a;
}
.clozeDiff.clozeSyn .dfSame { color: #16a34a; font-weight: 600; }
.clozeSynNote {
  display: block;
  margin-top: 3px;
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--muted);
}

/* The feedback line: your answer with the differences marked, so you can see
   what went wrong instead of comparing two blocks of text by eye. The kept
   parts recede; only the edits carry colour. */
.clozeDiff {
  max-width: 34em;
  margin-left: auto;
  margin-right: auto;
  line-height: 1.5;
}
.clozeDiff .dfSame { color: var(--muted); font-weight: 500; }
/* Typed but not wanted — a stray word, a doubled letter, a comma too many. */
.clozeDiff .dfDel {
  color: #dc2626;
  text-decoration: line-through;
  text-decoration-thickness: 2px;
}
/* Wanted but not typed — what was missing from your version. */
.clozeDiff .dfIns {
  color: var(--accent);
  text-decoration: underline;
  text-decoration-style: dotted;
  text-underline-offset: 3px;
}
/* The dictionary headword, named under a sentence that needed an inflected
   form of it. Quiet — the sentence is the thing being read. */
.clozeLemma {
  margin-top: 8px;
  font-size: 0.9rem;
  color: var(--muted);
  font-style: italic;
}

/* Listen prompt: a big round speaker, with the typed-answer input stacked
   beneath it (and, when a card resolves to Listen while Blanks is also
   selected, above the fill-in sentence). */
.listenPrompt {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 12px;
  font-size: 1.5rem;
}
.clozeInputBlock {
  width: 8em;
  max-width: 60vw;
  text-align: center;
}

/* A box for an answer that is a phrase rather than a word: full width, so a
   clause can be read back as it is typed instead of scrolling out of view. */
.clozeInputWide {
  width: 100%;
  max-width: 100%;
  font-size: 0.8em;
  text-align: center;
}
/* The Listen question: speaker and box on one line, with the way out ("Show
   me") on its own line under them, exactly as a Forms question is laid out. */
.listenPrompt.listenAsk {
  flex-direction: column;
  gap: 10px;
  width: 100%;
}
.listenRow {
  display: flex;
  flex-direction: row;
  align-items: center;
  justify-content: center;
  gap: 12px;
}
/* The speaker always precedes the box on the same line, never stacks above
   it (user feedback, 2026-09-07) — including once the answer is long enough
   to need a full-width field: the box grows to fill the remaining row
   instead of the row switching to a column. .listenBtn is a fixed-size flex
   item (flex: 0 0 auto) already, so it holds its width; min-width: 0 is
   load-bearing on the input, not decorative — a flex item's default
   min-width is its content's unwrapped width, which would otherwise force
   the row wider than the card instead of letting the box actually shrink to
   fit (same trap as .mOptText elsewhere in this file). */
.listenRow.listenRowWide {
  width: 100%;
}
/* The revealed Listen card: answer above, feedback below. */
.listenPrompt.listenFilled {
  flex-direction: column;
  gap: 10px;
  width: 100%;
}
.clozePrompt.clozePromptWide { width: 100%; }
/* "Show me" under a gap-in-a-sentence prompt. The sentence around it is set at
   1.5rem, so the button has to be pulled onto its own line and back to its own
   size rather than sitting inline at the end of the clue. */
.clozePrompt > .formShowMe {
  display: block;
  margin: 16px auto 0;
  font-size: 0.78rem;
}

/* Listen-mode prompt: the same speaker icon as the one beside a word (it also
   carries .speakBtn, so hover/speaking/loading states come along), just scaled
   up because here it *is* the prompt rather than an afterthought. */
.listenBtn {
  flex: 0 0 auto;
  margin-inline-start: 0;
  font-size: 1.9rem;
  opacity: 0.7;
}

/* ---- level switch (A1 / A2 / B1) -------------------------------------- */
/* #cshLevelNav (the Cheat Sheet's own copy, same reasoning as its topic
   dropdown below) shares this container styling exactly — it must look
   identical to Study mode's switch, only its state is independent.
   .mSchrTabs (the Goethe-Schreiben "ПРИМЕР 1/2/3" example switcher, one per
   card rather than one per page) reuses the same pill chrome for the same
   reason. #schrAufgabeNav (the header's own Aufgabe 1/2/3 switch,
   14b-goethe-schreiben.js) never had a dedicated rule at all — it only ever
   looked right by accident, riding .levelBtn's OLD default (inline-block-ish)
   flow to sit its buttons side by side with no real wrapper styling of its
   own. .levelBtn now sets display:flex for its own internal centering (see
   its own comment), which blockifies each button when it ISN'T a flex
   item of something -- exactly #schrAufgabeNav's case -- so without a real
   flex wrapper each "Aufgabe N" button now stacked on its own line. Added
   here for a real fix, not a coincidence: it should look identical to
   these other three anyway. .mTaleTrTabs (Tales' translation-language
   switcher, 11-lyrics-songbook-media.js) is the same pattern again: one per
   tale, picking which study-from language's translation shows underneath. */
#cshLevelNav, .mSchrTabs, #schrAufgabeNav, .mTaleTrTabs {
  display: inline-flex;
  gap: 2px;
  padding: 2px;
  position: relative;
  border: none;
  border-radius: 0 !important;
}
/* #cshLevelNav alone keeps the boxed brush-border frame + card-bg fill --
   user feedback, 2026-09-30: the other three read as over-boxed for a
   plain in-page tab row and lost both the frame and the fill, leaving just
   the bare .levelBtn pills. */
#cshLevelNav { background: var(--card-bg); }
#cshLevelNav::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

#cshLevelNav.hidden { display: none; }

/* German runs A1-C2, so its switch carries six rungs where every other target
   has three. Six at the normal width push the header onto another row on a
   phone; tightening the padding keeps the whole ladder on one line. */
#cshLevelNav.wide .levelBtn { padding-left: 7px; padding-right: 7px; }

/* #levelNav's own rungs moved into a dropdown panel (2026-09-16, was an
   inline pill row) — #levelNav itself is now the .langDrop wrapper
   (#topicDrop's shared shape), so its own display/gap/border/background
   rule above no longer applies to it; #cshLevelNav (Cheat Sheet's own,
   untouched, still the inline row) keeps using it. Each row inside the
   panel needs the same full-width, left-aligned shape every other
   dropdown list uses, overriding .levelBtn's own centered-pill defaults
   (still correct for its OTHER callers: Cheat Sheet's row, Schreiben's
   version tabs). */
#levelPanel .levelBtn {
  display: flex;
  width: 100%;
  justify-content: flex-start;
  padding: 8px 9px;
  border-radius: 9px;
}
#levelPanel .levelBtn.active { color: var(--text); }
#levelPanel .levelBtn .levelBtnLabel {
  position: relative;
  display: inline-block;
}
#levelPanel .levelBtn .levelBtnLabel::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 6px;
  background-color: var(--accent);
  pointer-events: none;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.2s ease;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 26px 6px;
          mask-size: 26px 6px;
}
#levelPanel .levelBtn:hover .levelBtnLabel::after,
#levelPanel .levelBtn.active .levelBtnLabel::after { transform: scaleX(1); }
#levelPanel .levelBtn.active::after {
  content: "";
  margin-left: auto;
  width: 13px;
  height: 13px;
  background-color: var(--accent);
  -webkit-mask-image: var(--check-mark-mask);
          mask-image: var(--check-mark-mask);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-position: center;
          mask-position: center;
}

.levelBtn {
  /* #levelNav/#cshLevelNav/.mSchrTabs now force an explicit 34px height on
     themselves (see #topicDropBtn's shared rule above) and stretch this
     button to fill it -- needs its own centering too, or the label sits
     top-aligned inside the taller box instead of in the middle of it. */
  display: flex;
  align-items: center;
  justify-content: center;
  font-size: 0.8rem;
  font-weight: 700;
  padding: 5px 11px;
  border: none;
  border-radius: 10px;
  background: transparent;
  color: var(--muted);
  cursor: pointer;
  letter-spacing: 0.02em;
}

/* Underlined, not filled (user feedback, 2026-09-24 -- same swap as
   .viewBtn.active/--pencil-underline, and the same var(--accent) main
   colour everywhere, overriding the old Secondary-accent-for-level-rungs
   split below: every one of these underlines now matches). Text/icon colour
   stays --text -- only the underline itself carries --accent (user
   feedback, 2026-09-25, same #levelPanel .levelBtn.active precedent as
   #modePanel above). */
.levelBtn.active {
  background: transparent;
  color: var(--text);
}
.levelBtn .levelBtnLabel { position: relative; display: inline-block; }
.levelBtn.active .levelBtnLabel::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -4px;
  height: 5px;
  background-color: var(--accent);
  pointer-events: none;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 26px 6px;
          mask-size: 26px 6px;
  -webkit-mask-position: center;
          mask-position: center;
}

#deck {
  flex: 1;
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 14px;          /* room for the reference panel flanking the card */
  padding: 16px;
  min-height: 0;
  /* The card's swipe gesture listens here too (see enableSwipe() in app.js),
     not just on #card itself, so a swipe started anywhere in the surrounding
     space still counts. pan-y keeps vertical scroll/pan native and leaves
     horizontal drags to that JS, same division #card already has. */
  touch-action: pan-y;
}
/* Study mode's own sign-in gate (gating-model-decision, added 2026-08-20) --
   unlike Reference/Media there's no separate preview to build, since the
   real card is already rendered here regardless of session state; this
   blurs THAT in place rather than building one. #studyGateOverlay is
   excluded from the blur since it's the CTA sitting on top of it.
   #stats gets the identical treatment (own overlay id, listed alongside
   #deck's below) — it's #deck's sibling, shown instead of it rather than
   inside it, so signing out while the "Fet!"/"Done!" summary is on screen
   otherwise left that real tally and a live "Start anew" fully exposed. */
#deck.studyLocked,
#stats.studyLocked { position: relative; }
#deck.studyLocked > *:not(#studyGateOverlay),
#stats.studyLocked > *:not(#studyGateOverlayStats) {
  filter: blur(6px);
  pointer-events: none;
  user-select: none;
}
#studyGateOverlay,
#studyGateOverlayStats {
  position: absolute;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  overflow: hidden;
  /* Solid, not a color-mix blend toward transparent -- .gateRain's own top
     64px fade (below) makes every falling card invisible in that band, so a
     translucent overlay background left the page's Background pattern
     (#bgPattern, sitting behind everything) showing through as a bare,
     unobstructed strip right at the top of the gate (user feedback,
     screenshot) -- nothing else there to break it up the way the cards do
     lower down. */
  background: var(--bg);
}
/* The falling-cards backdrop (buildGateRain, 00-core-state.js) -- appended
   before buildGateBox("signin") so normal DOM-order stacking puts the
   sign-in box on top without needing an explicit z-index on either. Fills
   the same overlay box these attach to (#studyGateOverlay/Stats above are
   already position:absolute, so inset:0 here sizes against that, not the
   viewport) and clips via that overlay's own overflow:hidden. */
.gateRain {
  position: absolute;
  inset: 0;
  /* Cards start above the viewport (.gateRainCard's top: -140px) and fall
     into view, so at any moment several are mid-entry, clipped by this
     container's overflow:hidden -- without a fade a clipped card reads as
     a stray hard-edged corner/border sitting right at the very top of the
     page (user feedback, screenshot). Fading the top strip to transparent
     makes that entry soft instead of a hard cut. */
  -webkit-mask-image: linear-gradient(to bottom, transparent, black 64px);
  mask-image: linear-gradient(to bottom, transparent, black 64px);
}
.gateRainCard {
  position: absolute;
  top: -140px;
  width: 132px;
  padding: 10px 12px;
  border-radius: var(--radius-sm);
  background: var(--card-tint);
  border: 1px solid var(--border);
  box-shadow: var(--card-shadow);
  opacity: 0.55;
  animation-name: gateRainFall;
  animation-timing-function: linear;
  animation-iteration-count: infinite;
  will-change: transform;
}
.gateRainFlag,
.gateRainWord,
.gateRainGloss {
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
.gateRainFlag { font-size: 0.72rem; color: var(--muted); margin-bottom: 4px; }
.gateRainWord { font-size: 1rem; font-weight: 700; margin-bottom: 2px; }
.gateRainGloss { font-size: 0.78rem; color: var(--muted); }
.gateRainCard.gateRainWide { width: 192px; }
.gateRainTag {
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--accent);
  margin-bottom: 4px;
}
.gateRainWord.gateRainWrap,
.gateRainGloss.gateRainWrap {
  white-space: normal;
  overflow: hidden;
  display: -webkit-box;
  -webkit-line-clamp: 2;
  -webkit-box-orient: vertical;
  text-overflow: ellipsis;
}
@keyframes gateRainFall {
  from { transform: translateY(0); }
  to { transform: translateY(calc(100vh + 200px)); }
}
@media (prefers-reduced-motion: reduce) {
  .gateRainCard { animation: none; top: 12%; }
}
/* The level/mode/topic controls live outside #deck (part of the header
   row), so #deck.studyLocked's own blur selector above never reaches them --
   without this they stayed crisp and clickable while the deck itself was
   gated, controlling a session no one signed out can actually use.
   studyControlsLocked (not studyGateLocked) covers both that signed-out
   case, where these three are just as pointless to leave live -- see
   applyStudyGate(). #topicPanel is listed
   by its own id, not as a #topicDrop descendant: it gets portaled to a
   direct child of <body> while open (trackPanelAnchor(), 12-mode-level-
   lang-switches.js) to escape a clipping ancestor, so an ancestor selector
   would stop matching it at the exact moment it's open and would otherwise
   need blurring. */
/* A locked language (signed in, not unlocked): only the target picker and the
   account menu stay. Everything else -- From, level,
   mode, topics, the Media section picker, settings, the progress counter --
   is removed rather than left blurred. */
html.studyLangLocked #viewNav,
html.studyLangLocked #langDrops,
html.studyLangLocked #levelNav,
html.studyLangLocked #modeNav,
html.studyLangLocked #topicDrop,
html.studyLangLocked #topicPanel,
html.studyLangLocked #mediaDrop,
html.studyLangLocked #settingsWrap,
html.studyLangLocked #settingsTray,
html.studyLangLocked #progress {
  display: none !important;
}
/* ...and no solid fill behind the gate box: the page's own background (and
   Background pattern) shows through instead of a flat dark panel. The solid
   fill is only needed for the sign-in gate's falling-cards backdrop. */
/* A locked language has no real card worth blurring behind the box (it never
   hydrated) -- hide it outright instead of leaving a smudge. */
html.studyLangLocked #deck.studyLocked > *:not(#studyGateOverlay),
html.studyLangLocked #stats.studyLocked > *:not(#studyGateOverlayStats) {
  visibility: hidden;
}
#studyGateOverlay[data-gate-kind="subscribe"],
#studyGateOverlayStats[data-gate-kind="subscribe"] {
  background: transparent;
}
html.studyControlsLocked #levelNav,
html.studyControlsLocked #modeNav,
html.studyControlsLocked #topicDrop,
html.studyControlsLocked #topicPanel {
  filter: blur(4px);
  pointer-events: none;
  user-select: none;
}
/* Signed out (user feedback) blurs the WHOLE header, not just the three
   Study-specific controls above -- language pickers, the Study/Reference/
   Media tabs, settings, account and the stats/progress readout are all
   just as usable-before-you-should-be otherwise, and #accountBtn's own
   tray duplicates the sign-in gate's own inline form (verified working
   independently of it) rather than being needed alongside it -- both
   open at once (user feedback, screenshot) just reads as two redundant,
   slightly different-looking sign-in prompts stacked on the same screen.
   Deliberately studyGateLocked, not studyControlsLocked. .tlPanel/
   #settingsTray/#accountTray are listed by their own class/id for the
   same portaling reason as #topicPanel above -- trackPanelAnchor()
   (12-mode-level-lang-switches.js) moves whichever of them is currently
   open to a direct child of <body>, so an ancestor selector would stop
   reaching it at the exact moment it's open. */
html.studyGateLocked #topleft,
html.studyGateLocked #topright,
html.studyGateLocked .tlPanel,
html.studyGateLocked #settingsTray,
html.studyGateLocked #accountTray {
  /* Hidden outright rather than blurred (user feedback) -- a blurred bar
     sitting right above the new gateRain background (see
     #studyGateOverlay above) read as a hard, jarring seam where the blur
     cut off. Removing the whole header is simpler and cleaner than any
     blur-transition fix, and none of it is usable while locked anyway. */
  display: none;
}
/* #topbar itself has no background of its own anywhere else in the app
   (deliberate -- lets the Background pattern peek through the gaps between
   its normal controls) -- but every one of those controls is hidden while
   studyGateLocked (the rule above), leaving #topbar's own strip completely
   empty, with nothing left to break up #bgPattern showing through it: a
   bare, full-width band of pattern sitting right above the sign-in gate
   (user feedback, screenshot, repeatedly). Same solid #studyGateOverlay
   fill directly below it (see that rule's own comment) rather than
   #topbar's usual transparency, so the whole gated screen reads as one
   continuous plain background instead of a pattern strip on top of it. */
html.studyGateLocked #topbar {
  background: var(--bg);
}

/* Above the narrow-layout breakpoint, #deck becomes a fixed three-column
   grid — 340px reserved for each panel, whether or not it is actually open —
   rather than a flex row that only reserves space for whichever children are
   currently visible. That distinction is the whole point: with flex,
   `.hidden`'s `display:none` removes a closed panel from the layout
   entirely, so opening just the left one made the pair re-centre as a unit
   and the card visibly slide right. A grid's column tracks are a property of
   the container, sized by the template regardless of whether a given cell's
   content exists — so the card's own column never moves, no matter which of
   the other two are empty. */
@media (min-width: 1181px) {
  /* Both side cards live in ONE column to the right of the study card, Word
     family above Forms. The card's own track never changes with the panels
     (720px at most), so opening a panel just adds the 420px column and the
     whole [card + panels] group re-centres -- the card slides left by half
     that instead of shrinking. A closed panel's column is `auto` = 0. */
  #deck {
    display: grid;
    grid-template-columns: minmax(0, 1fr) minmax(0, 720px) auto minmax(0, 1fr);
    /* The card spans the two middle rows; the panels stretch to fill them,
       so together they are never shorter than the card (and a lone panel is
       exactly as tall as it). The outer 1fr rows centre the group. */
    grid-template-rows: minmax(0, 1fr) auto auto minmax(0, 1fr);
    gap: 0;
  }
  /* Card width follows its content (never wider than 720px). */
  #deck #card {
    grid-column: 2; grid-row: 2 / 4;
    justify-self: center; align-self: center;
    width: fit-content; min-width: min(340px, 100%); max-width: 720px;
  }
  #refPanelFamily, #refPanelForms { grid-column: 3; width: 420px; margin-left: 14px; justify-self: stretch; align-self: stretch; }
  #refPanelFamily { grid-row: 2; }
  #refPanelForms { grid-row: 3; }
  #refPanelFamily:not(.hidden) + #refPanelForms:not(.hidden) { margin-top: 14px; }
  /* Both open: share the card's height between them. --cardH is set by
     syncPanelHeight(). */
}

/* The cap is set inline (syncPanelHeight() in app.js) to the card's own
   rendered height, in pixels, not a percentage: a percentage max-height only
   resolves against a DEFINITE containing-block height, and an auto-sized
   grid row's height is derived FROM its items, including this one — a
   circular reference the spec resolves by treating the percentage as unset,
   which is how a panel's own content used to grow the row (and itself)
   without limit instead of being capped and scrolling. Matching the card's
   actual height, rather than the row's, is also the more precise read of
   "cap it at the card" than "cap it at whatever the deck allows." */

#card {
  position: relative;
  background: var(--card-tint);
  border: none;
  border-radius: 0 !important;
  box-shadow: var(--card-shadow);
  backdrop-filter: blur(var(--card-blur));
  -webkit-backdrop-filter: blur(var(--card-blur));
  padding: 40px 36px;
  /* Content-hugging rather than a fixed wide box: most cards are a word, a
     couple of chips and two example lines, and 560px of width for that was
     mostly padding. A floor of 20% of the viewport keeps a one-word card
     from shrinking to an awkward sliver on a wide screen. */
  max-width: 480px;
  min-width: 20vw;
  width: 100%;
  max-height: 100%;
  overflow-y: auto;
  touch-action: pan-y;
  user-select: none;
  /* Fast enough that the next card feels instant rather than waited-for, but
     an eased curve (not linear, not 0) so a swipe's snap-back still reads as
     motion rather than a jump-cut. max-width/max-height are the slower,
     separate pair covering #deck.bothRefsClosed widening the card and reveal
     adding content that needs more room — both are a size CHANGE settling
     into place, not a snap-back, so they get the gentler 0.22s to match
     #deck's own grid-template-columns transition above rather than reusing
     the swipe's snappier timing. */
  transition: transform 0.1s ease-out, opacity 0.1s ease-out,
    max-width 0.22s ease, max-height 0.22s ease;
}
/* Matches #deck.bothRefsClosed's widened middle track — the track alone
   doesn't widen the card, since #card centers within it at its own
   max-width rather than stretching to fill the column. */
#deck.bothRefsClosed #card { max-width: 720px; }
#card::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}


/* A soft fade at whichever edge still has more content past it — #card, the
   reference panels and the word-preview popover all scroll internally, and
   without this a line of text at the very edge just stops, cut sharp by the
   box's own boundary rather than reading as "more below, keep scrolling".
   `updateScrollFade()` in app.js turns `.fade-top`/`.fade-down` on only when
   there truly is more that way, so a box short enough to show everything at
   once carries neither and its edges are untouched. */
/* mask-clip: padding-box (not the default border-box) keeps the mask inside
   the border — without it, the gradient fades the border itself out along
   with the text near the scroll edge, which reads as the card's outline
   dissolving rather than as "more content below". */
#card.fade-top, .refPanel.fade-top, .wordPreview.fade-top,
#card.fade-down, .refPanel.fade-down, .wordPreview.fade-down {
  -webkit-mask-clip: padding-box;
  mask-clip: padding-box;
}
#card.fade-top, .refPanel.fade-top, .wordPreview.fade-top {
  -webkit-mask-image: linear-gradient(to bottom, transparent, black 28px);
  mask-image: linear-gradient(to bottom, transparent, black 28px);
}
#card.fade-down, .refPanel.fade-down, .wordPreview.fade-down {
  -webkit-mask-image: linear-gradient(to top, transparent, black 28px);
  mask-image: linear-gradient(to top, transparent, black 28px);
}
#card.fade-top.fade-down, .refPanel.fade-top.fade-down, .wordPreview.fade-top.fade-down {
  -webkit-mask-image: linear-gradient(to bottom, transparent, black 28px, black calc(100% - 28px), transparent);
  mask-image: linear-gradient(to bottom, transparent, black 28px, black calc(100% - 28px), transparent);
}

#card.drag-know {
  transform: translateX(40px) rotate(2deg);
  border-color: var(--accent);
}

#card.drag-unknown {
  transform: translateX(-40px) rotate(-2deg);
  border-color: var(--bad);
}

/* The "one action left" states — confirming/walking back an already-claimed
   verdict (saidKnown/saidUnknown in updateControls) — read as neither a
   fresh hit nor a fresh miss, so neither green nor red fits. Same orange as
   #btnUnknown.verdict-warn, the button this drag maps to either way. No
   transform override here (unlike drag-know/drag-unknown): this state can
   commit from either direction (saidUnknown), so no single rotation fits. */
#card.drag-learnanyway {
  border-color: #d97706;
}

#word {
  font-family: var(--font-head);
  font-size: 2.1rem;
  font-weight: var(--head-weight);
  text-align: center;
  margin-bottom: 0;
  line-height: 1.15;
  overflow-wrap: break-word;
  word-break: break-word;
}

/* Pronunciation aid under a logographic / non-Latin headword (pinyin, romaji,
   romanization). Sits tight below the word and pushes the 18px gap down to it. */
#wordReading {
  text-align: center;
  margin-top: -12px;
  margin-bottom: 18px;
  color: var(--muted);
  font-size: 1.05rem;
  letter-spacing: 0.02em;
}
#wordReading:empty { display: none; }

/* Face-down, the headword (or the cloze / speaker / question standing in for
   it) is the only thing in the card — everything below is hidden until the
   reveal. Its bottom margin then has nothing to separate it from, and 40px of
   padding above against 58px below is enough to read as the word sitting high
   in its box. So the trailing gap goes, and the card centres on its content. */
#card.answer-hidden #word { margin-bottom: 0; }
/* The reading takes over as the last line when there is one — and it hangs off
   the word's margin, so give it back the 6px that margin was providing. */
#card.answer-hidden #wordReading { margin-top: 6px; margin-bottom: 0; }

/* The reading tucked after the target word on its translation row, and under a
   target example sentence — both muted, smaller than the script they gloss. */
.deReading {
  color: var(--muted);
  font-size: 0.85em;
  font-weight: 400;
  margin-inline-start: 0.35em;
}
/* The sense note beside a headword — "(informatique)", "(Plural)". It says
   which of two cards you are on; it is not part of the word, and is set so it
   cannot be mistaken for letters to type. */
.headNote {
  color: var(--muted);
  font-size: 0.7em;
  font-weight: 500;
  font-style: italic;
  letter-spacing: 0;
  margin-inline-start: 0.4em;
  white-space: nowrap;
}
.example .exReading {
  color: var(--muted);
  font-size: 0.82rem;
  font-weight: 400;
  margin-top: 2px;
}

/* When the target sentence has a reading under it, the speaker button is
   inserted right after the sentence text (before .exReading in the DOM), so
   it sits at the end of the sentence line and the reading wraps to its own
   line below — normal block flow, no flex needed. */

#translations {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 6px;
  margin-bottom: 16px;
  font-size: 1.05rem;
}

/* Each row wraps its badges; German gets its own row above the rest. */
.trRow {
  display: flex;
  gap: 12px;
  justify-content: center;
  flex-wrap: wrap;
  align-items: stretch;
}

/* A row of badges is a row of different scripts, and scripts do not share a
   line box: Arabic and Devanagari ask for far more room above and below the
   baseline than Latin does. Left as plain blocks the badges came out the same
   height (the row stretches them) but the Latin word sat at the top of its
   box while the Arabic one filled it — "cell" floating over its own chip.
   Each badge centres its own contents instead, so the words line up through
   the middle of the row however tall the row has to be. Direct children only:
   the reading and the speaker button live inside a badge and are not badges. */
/* A badge is a slightly deeper patch of the card it sits on, mixed out of the
   text colour into transparency rather than painted with --bg. --bg is the
   base palette's page colour — a warm khaki in light mode — and it ignores the
   skin, so on a card tinted pink or violet the badges were the one surface
   still reading green. Mixing into transparency keeps them the card's own hue
   under every skin, and inverts by itself in dark mode. */
.trRow > span {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--text) 6%, transparent);
  border: 1px solid color-mix(in srgb, var(--text) 15%, transparent);
  border-radius: 10px;
  padding: 6px 12px;
}

/* The target-language translation is the answer — the one badge among the
   row that this whole card is testing you on, so it stays visually distinct
   from the rest with an accent outline and glow when the card is revealed. */
#translations .deBadge {
  border-color: var(--accent);
  box-shadow: 0 0 0 1px var(--accent),
              0 0 12px 1px color-mix(in srgb, var(--accent) 45%, transparent);
}


#card.answer-hidden #translations,
#card.answer-hidden #examples { display: none; }
/* Forms asks a question with its own answer controls, so "tap to reveal" is
   the wrong instruction there; app.js hides it for that mode. */

#examples {
  display: flex;
  flex-direction: column;
}

.example {
  border-top: 1px solid var(--border);
  margin-top: 14px;
  padding-top: 14px;
}
.example:first-child {
  margin-top: 0;
}
/* .wlExpand already draws its own top border to separate the Wordlist row's
   header from its expanded content. When there's no synonym/reading/forms
   row ahead of the first example to sit between the two, that example's own
   border-top (still drawn — :first-child above only zeroes the margin) reads
   as a second, near-touching divider line. Only the first example directly
   inside .wlExpand is ever in that position, so only it drops its border. */
.wlExpand > .example:first-child {
  border-top: 0;
  padding-top: 0;
}

.example .exLearn {
  font-weight: 600;
  margin-bottom: 4px;
}

/* The target word's own gentle glow inside an example chain (app.js:
   glowLineHtml/appendGlowText) — a soft, STATIC pulse on the word's own
   background, never a moving/sweeping highlight, so it reads as "this is the
   word you're learning" rather than drawing the eye across the sentence. Two
   properties animate (background opacity and a matching soft shadow) rather
   than just background-color, so the glow still reads at the same gentle
   strength regardless of the page's own background under it, light or dark.
   Lands on the target-language line and on every translation line beside it
   (see exampleLangHtml/stackedLines) — in practice that is almost always
   just the target line, since a translation only carries the match when it
   happens to keep the target word's own spelling verbatim. */
.wordGlow {
  background: none;
  color: inherit;
  font: inherit;
  border-radius: 4px;
  padding: 0 0.15em;
  margin: 0 -0.15em;
  animation: wordGlowPulse 2.6s ease-in-out infinite;
}
@keyframes wordGlowPulse {
  0%, 100% {
    background: color-mix(in srgb, var(--accent) 10%, transparent);
    box-shadow: 0 0 0 0 color-mix(in srgb, var(--accent) 0%, transparent);
  }
  50% {
    background: color-mix(in srgb, var(--accent) 22%, transparent);
    box-shadow: 0 0 6px 1px color-mix(in srgb, var(--accent) 18%, transparent);
  }
}
/* A learner who has turned off motion elsewhere (see prefers-reduced-motion
   handling for other animations in this file) gets a single steady tint
   instead of a pulse — still marks the word, costs nothing to a vestibular
   disorder. */
@media (prefers-reduced-motion: reduce) {
  .wordGlow { animation: none; background: color-mix(in srgb, var(--accent) 14%, transparent); }
}

/* Muted speaker button that plays the German pronunciation. Sits inline next
   to the German word / example line and inherits currentColor for its icon. */
.speakBtn {
  appearance: none;
  -webkit-appearance: none;
  border: none;
  background: none;
  padding: 0;
  margin-inline-start: 0.4em;
  color: var(--muted);
  cursor: pointer;
  opacity: 0.55;
  vertical-align: middle;
  line-height: 0;
  font-size: 0.8em;
  transition: opacity 0.15s ease, color 0.15s ease, transform 0.1s ease;
}
.speakBtn:hover { opacity: 1; color: var(--accent); }
.speakBtn:active { transform: scale(0.88); }
/* "Show speech button" off: hide every decorative speaker instantly and
   everywhere, including ones already on screen — a class toggle on <html>
   rather than a render-time check, so switching it doesn't need whatever
   happens to be showing to re-render itself to notice. A Listen card's own
   speaker (.listenBtn) IS the exercise there, not a convenience next to
   one, so it stays regardless. */
html.hideSpeakBtn .speakBtn:not(.listenBtn) { display: none; }
/* Same idiom as hideSpeakBtn above, for the Latin transcription line
   (pinyin/romaji/romanization/Paiboon) under target sentences and
   headwords — every render call site that can print `reading` at once. */
html.hideTranscription #wordReading,
html.hideTranscription .exReading,
html.hideTranscription .deReading,
html.hideTranscription .wlReading,
html.hideTranscription .gExReading { display: none; }
.speakBtn.speaking { opacity: 1; color: var(--accent); }
/* While Piper synthesizes (or first-time model download), pulse the icon. */
.speakBtn.loading { opacity: 1; color: var(--accent); animation: speakPulse 0.9s ease-in-out infinite; }
@keyframes speakPulse { 0%, 100% { opacity: 0.35; } 50% { opacity: 1; } }
.speakBtn svg { display: block; }

.example > div:not(.exLearn) {
  color: var(--muted);
  font-size: 0.92rem;
  line-height: 1.4;
}

/* A frosted strip, not a flat panel: no tint of its own — over the skin's
   flat background, blurring a flat colour shows no seam at all, and over
   any texture behind the deck it still softens it, without ever reading
   as a differently-coloured rectangle the way a tinted bar did.
   3px, not the 6-22px other frosted surfaces use — picked directly against
   a live blur-radius slider over the real patterns, not guessed: a
   Background pattern's own stripes/dots are already subtle (8-14% accent
   tint) so they don't fight the study card for attention, and most
   patterns repeat every 9-16px with a mostly-empty gap — a blur radius
   anywhere near that erases the texture into a flat average colour before
   you'd call it "frosted", which is exactly the "differently-coloured
   rectangle" this strip is meant to avoid. The mask fades the blur in from
   nothing at its own top edge, so what's behind gradually softens into the
   bar instead of snapping from sharp to blurred in one line. */
#controls {
  display: flex;
  gap: 16px;
  justify-content: center;
  padding: 20px;
  flex-shrink: 0;
  position: relative;
  background: transparent;
  backdrop-filter: blur(3px);
  -webkit-backdrop-filter: blur(3px);
  -webkit-mask-image: linear-gradient(to bottom, transparent, #000 28px);
          mask-image: linear-gradient(to bottom, transparent, #000 28px);
}
/* The full label everywhere there's room for it; a phone-width #controls
   (see the 640px breakpoint) swaps which of the two is shown instead. */
.btnLabelShort { display: none; }

button {
  font-family: inherit;
  font-size: 1rem;
  font-weight: 600;
  padding: 14px 22px;
  border-radius: var(--radius-sm);
  border: 1px solid var(--border);
  background: var(--card-bg);
  color: var(--text);
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: 8px;
}

button:active {
  transform: scale(0.97);
}
/* A Wordlist row's click just opens/closes it — no press animation on the
   word itself, which read as the text flinching before it expanded. */
.wlHeadBtn:active { transform: none; }

#btnUnknown { color: var(--bad); }
#btnKnown { color: var(--accent); }
#btnBack, #btnForward, #btnToCurrent { color: var(--muted); }
#btnBack, #btnUnknown, #btnKnown, #btnForward, #btnToCurrent {
  position: relative;
  border: none;
  border-radius: 0 !important;
}
#btnBack::before, #btnUnknown::before, #btnKnown::before, #btnForward::before, #btnToCurrent::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
/* Post-reveal relabeling of btnUnknown once a pre-reveal claim (preVerdict)
   is on record: "Learn anyway" walks back a claimed Know, in the same amber
   as a near-miss elsewhere on the card, not the --accent green (which varies
   by skin and would read as another confirm button next to btnKnown's real
   one); "Learn" is the sole leftover of a claimed Don't know, and just
   confirms — same green as btnKnown normally is. Both override #btnUnknown's
   red, so they need the extra specificity. */
#btnUnknown.verdict-warn { color: #d97706; }
#btnUnknown.verdict-safe { color: var(--accent); }

/* Under a Design, Know/Don't-know are the artifact's own filled
   ".btn.primary"/".btn.secondary" pills rather than the bare fallback's
   quiet colour-on-neutral text — checked directly against the artifact's own
   rendering (not just its source), which pairs Know with the Primary
   accent and Don't-know with Secondary throughout, e.g. Blueprint's own
   caveat text about keeping "the 'known' state's accent... not read as
   punitive" — Secondary, not the app's fixed red, is genuinely the
   intended colour for "don't know" here. Back stays the plain neutral
   button, matching the artifact's own unstyled ".btn". Drag-preview and
   verdict-relabel states are untouched — their higher selector specificity
   still wins as before. */
/* Know / Don't know sit on the same opaque surface as Back (no see-through
   buttons over a gradient): plain text with a bold, doubled sketched
   underline in their own colour (red = don't know, amber = learn anyway,
   Primary = know). */
[data-design] #btnKnown,
[data-design] #btnUnknown,
[data-design="ember"] #btnKnown {
  background: var(--card-bg); color: var(--text); border-color: transparent;
}

/* Live preview while dragging the card, mirroring #card.drag-know etc — the
   same class names, applied by app.js to whichever button the in-progress
   swipe would actually commit, so the controls visibly arm along with the
   card's own border tint instead of only reacting once the swipe ends. */
#btnKnown.drag-know {
  background: color-mix(in srgb, var(--accent) 18%, var(--card-bg));
  border-color: var(--accent);
}
#btnUnknown.drag-unknown {
  background: color-mix(in srgb, var(--bad) 18%, var(--card-bg));
  border-color: var(--bad);
}
#btnUnknown.drag-learnanyway {
  background: color-mix(in srgb, #d97706 18%, var(--card-bg));
  border-color: #d97706;
}

button:disabled { opacity: 0.4; cursor: default; }
.hidden { display: none !important; }

kbd {
  font-family: inherit;
  font-size: 0.78rem;
  color: var(--muted);
  background: var(--bg);
  border: 1px solid var(--border);
  border-radius: 6px;
  padding: 1px 6px;
}

#stats {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 32px;
  text-align: center;
}

#stats.hidden,
#deck.hidden,
#controls.hidden {
  display: none;
}

@media (max-width: 640px) {
  /* Centered content that runs taller than its container only ever exposes
     the *bottom* overrun to overflow-y: auto — center/justify-center never
     falls back to start alignment on overflow, so the top of an overlong
     summary gets clipped out of the scrollable range rather than merely
     scrolled past. Same trap already fixed for #deck and #media.learning
     #mediaBody; a phone's shorter viewport is where a summary tall enough to
     trigger it comes up often enough to need the same fix here. */
  #stats { justify-content: flex-start; }
}

#statsTitle {
  font-family: var(--font-head);
  font-weight: var(--head-weight);
  font-size: 2.4rem;
  margin: 0 0 28px;
}

/* Three stat tiles — each its own little card, so the summary reads as a
   dashboard rather than loose numbers. The accent highlights what you learned;
   mistakes glow red only when there are any. */
#statNumbers {
  margin-bottom: 24px;
  display: flex;
  gap: 14px;
  flex-wrap: wrap;
  justify-content: center;
}

.statTile {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  min-width: 92px;
  padding: 16px 18px;
  border: var(--card-border-w) solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card-tint);
  backdrop-filter: blur(var(--card-blur));
  -webkit-backdrop-filter: blur(var(--card-blur));
}
.statTile b {
  font-family: var(--font-head);
  font-size: 2rem;
  font-weight: var(--head-weight);
  line-height: 1;
}
.statTile span {
  font-size: 0.85rem;
  color: var(--muted);
}
.statTile--good {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
  background: color-mix(in srgb, var(--accent) 10%, var(--card-tint));
}
.statTile--good b { color: var(--accent); }
/* Mistakes tile stays neutral at zero; lights up red once you miss one. */
.statTile--bad.on {
  border-color: color-mix(in srgb, var(--bad) 45%, var(--border));
  background: color-mix(in srgb, var(--bad) 10%, var(--card-tint));
}
.statTile--bad.on b { color: var(--bad); }

/* Gated-content overlay (gating-model-decision) — real content blurred in
   the background inside a locked Media/Reference tab, a scrim, and a
   floating CTA box on top. See renderGate/buildGateBox in
   00-core-state.js. No purchase flow exists yet, hence gateNote. */
/* overflow: hidden -- a gated tab must not be scrollable at all (nothing to
   scroll TO, since the real content behind the blur is unreadable anyway);
   also stops the CTA box's centering (below) from ever landing off-screen
   at the bottom of a tall blurred list, which is what a scrollable host
   used to let happen. Covers Reference, where #grammarBody (this element)
   is itself the scrolling element; Media's scrolling ancestor is further
   up (#media), handled separately below since overflow on a descendant
   can't reach it. */
.gateHost {
  position: relative;
  min-height: 320px;
  overflow: hidden;
  /* Fades gateHost's WHOLE painted composite -- the blurred preview AND the
     scrim over it, not just one of the two -- out to fully transparent
     toward the edges, so the section washes into the surrounding page
     background instead of getting clipped to a hard rectangle by
     overflow: hidden above (user feedback, repeated: "soft blurry gradient
     wash into the main background, not the sharp frame"). Has to live here,
     not on .gateBlur alone -- .gateScrim is a flat, unmasked rectangle of
     its own, so fading only the blur layer still left a sharp-edged tinted
     box behind it. */
  -webkit-mask-image: radial-gradient(ellipse at center, #000 40%, transparent 100%);
          mask-image: radial-gradient(ellipse at center, #000 40%, transparent 100%);
}
/* #grammarBody carries its own overflow-y:auto (an ID selector, specificity
   1,0,0) which otherwise beats .gateHost's overflow:hidden (0,1,0) outright,
   regardless of source order -- ID selectors always win that tie. This is
   the one place .gateHost IS #grammarBody (see the comment above it), so it
   needs a selector that actually beats it: #grammarBody.gateHost (1,1,0). */
#grammarBody.gateHost { overflow: hidden; }
.gateBlur {
  filter: blur(6px);
  pointer-events: none;
  user-select: none;
}
.gateScrim {
  position: absolute;
  inset: 0;
  background: color-mix(in srgb, var(--bg) 55%, transparent);
}
/* buildGateLoading (00-core-state.js) -- the account-state-still-in-flight
   stand-in for the real gate (gateState()'s "loading" tier). Same fixed,
   viewport-relative centering as .gateBox below, deliberately with none of
   its card chrome (no border/background/padding) -- just a label over the
   blur/scrim every gate already has (user feedback: "just a blurred screen
   with a Loading... label in the center"). */
.gateLoading {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  /* renderGate() now appends this straight to <body> (see the comment
     above clearGatePortal(), 00-core-state.js) rather than nesting it
     inside gateHost, so it needs its own stacking position instead of
     inheriting one from that ancestor -- matches #studyGateOverlay's own
     z-index exactly, the other half of this same "signed-out front door"
     mechanism. The hidden topbar/tray this replaces sit no higher than
     55-60, so this comfortably clears them without reaching into true
     modal territory (#confirmDialog and friends start at 90+). */
  z-index: 50;
  color: var(--muted);
  font-size: 0.95em;
}
/* Fixed, not absolute -- always centered in the actual viewport regardless
   of gateHost's own (possibly tall, now non-scrollable) content height,
   rather than centered within that whole height and potentially starting
   off-screen. Was silently broken by gateHost's mask-image (see
   clearGatePortal()'s comment) turning gateHost into this box's containing
   block instead -- fixed by moving the box itself out of gateHost's DOM
   subtree; the positioning below was already correct all along. */
.gateBox {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  z-index: 50;
  width: min(420px, calc(100% - 32px));
  max-height: calc(100% - 32px);
  overflow-y: auto;
  padding: 28px 24px;
  text-align: center;
  /* No hard border line (user feedback: "the sharp frame") -- a wide, soft
     shadow glow gives the box just enough definition against the blurred
     wash behind it without a crisp geometric edge anywhere. */
  border: none;
  border-radius: var(--radius-sm);
  background: var(--card-tint);
  backdrop-filter: blur(var(--card-blur));
  -webkit-backdrop-filter: blur(var(--card-blur));
  box-shadow: 0 0 60px 12px color-mix(in srgb, var(--bg) 45%, transparent);
}
/* Media's own scrolling container is #media (an ancestor of #mediaBody,
   this pass's .gateHost), not #mediaBody itself -- overflow:hidden on the
   host above never reaches it, so it needs its own rule. */
#media:has(.gateHost) { overflow: hidden; }
.gateIcon { font-size: 28px; margin-bottom: 8px; }
/* The sign-in tier's own icon.className swap (buildGateBox, 00-core-state.js) --
   BOOK_SVG (07-reference-panels.js) at the same 28px .gateIcon used, plus the
   "beeins" wordmark at the same size right after it (user feedback). */
.gateBrand {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-size: 28px;
  margin-bottom: 8px;
  color: var(--text);
}
.gateBrand svg { width: 1em; height: 1em; display: block; }
.gateBrandWordmark { font-size: 1em; font-weight: 700; line-height: 1; }
.gateTitle { margin: 0 0 8px; font-size: 1.2em; }
.gateBody { margin: 0 0 18px; color: var(--muted); }
.gateLegal {
  margin-top: 18px;
  font-size: 0.78rem;
  color: var(--muted);
}
.gateLegal a { color: inherit; }
.gateLegal a:hover { color: var(--text); }
.gateBtn {
  display: block;
  width: 100%;
  box-sizing: border-box;
  padding: 10px 20px;
  border: none;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: #fff;
  font-weight: 600;
  cursor: pointer;
}
.gateNote { margin: 12px 0 0; color: var(--muted); font-size: 0.9em; }
.gateNote.hidden { display: none; }

/* A non-interactive stand-in for a study card, blurred behind the sign-in
   tier's CTA box — see buildStudyPreview in 00-core-state.js. Loosely
   evokes #card's typography without needing to be pixel-identical, since
   it is never seen sharp. */
.gatePreviewCard { max-width: 420px; margin: 0 auto; text-align: center; }
.gatePreviewWord { font-size: 1.8em; font-weight: 700; margin-bottom: 10px; }
.gatePreviewLine { margin: 4px 0; color: var(--muted); }

/* The gate's own sign-in form, a second instance of the account tray's
   Google/email+password controls (16-auth.js) — shares the authIconBtn/
   authForm/authEmailInput/authRow/authTextLink/authChoiceRow/authChoiceLink/
   authLoginInputs/authProviders/authOrLabel/authProviderRow classes below
   with that form so
   both look identical without duplicating every rule. */
.gateSignIn { margin-top: 4px; }
.gateSignInSent { margin-top: 10px; color: var(--muted); font-size: 0.9em; }
.gateSignInSent.hidden { display: none; }
.gateSignInError { margin-top: 6px; padding: 0 10px; color: var(--bad); font-size: 0.78rem; }
.gateSignInError.hidden { display: none; }

#unknownList {
  max-width: 560px;
  width: 100%;
  text-align: left;
  margin-bottom: 28px;
  max-height: 45vh;
  overflow-y: auto;
}

.missRow--ok {
  color: var(--accent);
  font-weight: 600;
  text-align: center;
}

/* Every missed word as one flowing, wrapping paragraph — word • word •
   word • ... — rather than a card per word. Smaller than body text: this
   is a quick recap, not a reading list. Its translation used to sit inline
   right after it (another bullet, another span, class .t below) — moved to
   a hover/focus tooltip (`title`, set in 09-actions-stats-grammar.js's
   missFlow()) since it read as distracting clutter right next to the word,
   and — being one unbreakable token with no internal spaces — an
   untranslatable-length translation pushed whole onto the next line left a
   ragged gap on whichever line it didn't fit (user feedback, 2026-09-07).
   overflow-wrap covers the word span too, for the rare very long compound
   headword that hits the same failure mode on its own. */
.missFlow {
  margin: 0;
  font-size: 0.82rem;
  line-height: 1.7;
  overflow-wrap: anywhere;
}
.missFlow .w { font-weight: 600; }
/* Same "hover me" cue .syn.synLinked uses. */
.missFlow .w.hasTip {
  cursor: help;
  text-decoration: underline dotted color-mix(in srgb, var(--text) 35%, transparent);
  text-underline-offset: 2px;
}
.missBullet { margin: 0 6px; color: var(--muted); opacity: 0.6; }

/* Heading over the list of words missed this session. */
#unknownList .missHead {
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 10px;
}

/* ---- language summary -------------------------------------------------
   The standing picture, under the session's own numbers: what is banked,
   what is waiting, how many days in a row — and a strip of the last 13
   weeks, so a run of study is something you can see rather than a number
   you have to take on trust. */
/* ---- pace ---------------------------------------------------------------
   Where the session's time went. Same card as the language summary so the two
   read as one block of afterwards-information rather than two panels. */
#pace {
  width: 100%;
  max-width: 560px;
  margin-bottom: 18px;
  padding: 18px 20px;
  border: var(--card-border-w) solid var(--border);
  border-radius: var(--radius-sm);
  background: var(--card-tint);
  backdrop-filter: blur(var(--card-blur));
  -webkit-backdrop-filter: blur(var(--card-blur));
}
#pace.hidden { display: none; }

.pcList { display: flex; flex-direction: column; gap: 2px; }
.pcRow {
  display: flex;
  align-items: baseline;
  gap: 8px;
  font-size: 0.86rem;
  padding: 3px 0;
}
/* The label takes the slack so the times line up in a column on the right,
   which is the only way a list of durations is comparable at a glance. */
.pcLabel {
  flex: 1;
  min-width: 0;
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.pcSub { color: var(--muted); font-size: 0.76rem; }
.pcVal { font-variant-numeric: tabular-nums; font-weight: 700; color: var(--accent); }

.lsTitle {
  font-size: 0.82rem;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  color: var(--muted);
  margin-bottom: 14px;
}

.lsNums {
  display: flex;
  justify-content: space-around;
  gap: 12px;
  margin-bottom: 16px;
}
.lsNum { display: flex; flex-direction: column; align-items: center; gap: 3px; min-width: 0; }
.lsNum b {
  font-family: var(--font-head);
  font-size: 1.5rem;
  font-weight: var(--head-weight);
  line-height: 1;
}
.lsNum span { font-size: 0.78rem; color: var(--muted); text-align: center; }
.lsNum.lsGood b { color: var(--accent); }
.lsNum.lsBad b { color: #d97706; }

.lsSub {
  font-size: 0.75rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
  margin: 16px 0 8px;
}
/* 12px, not 6px -- each chip's own ::before brush border bleeds 3px
   outward per side, below; anything tighter lets neighbours' bled edges
   touch (same bug .mScopes hit, see its own comment). */
.lsParked { display: flex; flex-wrap: wrap; gap: 12px; justify-content: center; }
.lsChip {
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  font: inherit;
  font-size: 0.82rem;
  padding: 4px 11px;
  position: relative;
  border: none;
  border-radius: 0 !important;
  background: transparent;
  color: var(--muted);
}
.lsChip::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.lsChip:hover { color: var(--accent); }

#progress { display: inline-flex; align-items: baseline; gap: 6px; }

/* Cards per session, typed into the counter. Outline only — it belongs to the
   readout it sits in rather than announcing itself as a control, and the
   placeholder carries how many cards there actually are to draw from.
   The brush border lives on the wrapping span, not the <input> itself —
   ::before generates no box on a replaced element like <input> in this
   Chromium build, same reason every other bordered control here wraps or
   is a plain element, never an <input>/<select> directly. */
#sessionInputWrap {
  display: inline-flex;
  position: relative;
}
#sessionInputWrap::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
#sessionInput {
  width: 4.2em;
  font: inherit;
  font-variant-numeric: tabular-nums;
  text-align: center;
  padding: 2px 6px;
  border: none;
  border-radius: 0;
  background: transparent;
  color: inherit;
  cursor: text;
  transition: color 0.12s ease;
}
#sessionInput::placeholder { color: var(--muted); opacity: 0.65; }
#sessionInputWrap:hover { color: color-mix(in srgb, var(--accent) 45%, var(--text)); }
#sessionInput:focus {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
  color: var(--text);
}
/* The spinners crowd a 4-character box and invite click-by-click changes,
   each of which would deal a fresh session. */
#sessionInput::-webkit-outer-spin-button,
#sessionInput::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
#sessionInput { -moz-appearance: textfield; appearance: textfield; }

#statsActions {
  display: flex;
  align-items: center;
  gap: 16px;
}

/* Plain muted text, not a boxed button — the one place left to reset a
   session now that the top bar's own reset icon is gone. */
#statsReset {
  appearance: none;
  -webkit-appearance: none;
  border: none;
  background: none;
  cursor: pointer;
  font-size: 0.9rem;
  font-weight: 600;
  padding: 6px 2px;
  color: var(--muted);
}
#statsReset:hover { color: var(--text); }
#statsReset:active { transform: scale(0.96); }

/* The "reload and start over" button — a solid accent call-to-action. */
#btnReload {
  appearance: none;
  -webkit-appearance: none;
  border: none;
  cursor: pointer;
  font-size: 0.95rem;
  font-weight: 700;
  padding: 12px 24px;
  border-radius: var(--radius-sm);
  background: var(--accent);
  color: #fff;
  box-shadow: 0 6px 20px color-mix(in srgb, var(--accent) 30%, transparent);
  transition: transform 0.1s ease, opacity 0.15s ease, box-shadow 0.15s ease;
}
#btnReload:hover { opacity: 0.94; box-shadow: 0 8px 26px color-mix(in srgb, var(--accent) 38%, transparent); }
#btnReload:active { transform: translateY(1px) scale(0.99); }

/* ---- synonyms on the card --------------------------------------------- */
#synonyms {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
  align-items: center;
  margin-bottom: 8px;
}
#translations > #synonyms { margin-top: 10px; margin-bottom: 0; }

/* ---- grammatical forms -------------------------------------------------
   Gender, plural, principal parts, aspect partner, classifier — whatever the
   word carries. Revealed with the answer, and absent entirely for a word that
   has nothing to add, so an uninflected language's card is unchanged. */
#forms {
  display: flex;
  flex-wrap: wrap;
  gap: 7px;
  justify-content: center;
  align-items: center;
  margin-bottom: 18px;
}
#forms:empty { display: none; }
/* `visibility: hidden` still reserves the row and its margin, which on a Forms
   card left a band of nothing under the question. Collapse it instead. */
#card.answer-hidden #forms { display: none; }

/* ---- reference panels -----------------------------------------------------
   Two independent side cards flanking the study card, each opened by its own
   handle in its bottom corner: Word family to the left (#refPanelFamily),
   Forms to the right (#refPanelForms). (Meaning/Etymology retired
   2026-08-22 — that used to be the left panel. Word family and Forms were
   themselves briefly merged into one two-column card the same day, then
   split back into two independent cards 2026-08-23.)

   They read as quieter than the card on purpose — a card you are being
   tested on and a reference you are reading are not the same kind of thing,
   and the one you are answering should stay the brightest thing on screen.
   Same radius, blur and border width; a thinner surface, muted body text and
   headings in the accent.

   Below 1180px there is no room for side columns, so each card drops into
   the flow below the study card instead. */
.refPanel {
  position: relative;
  align-self: stretch;
  max-height: 100%;
  overflow-y: auto;
  padding: 18px 20px 22px;
  background: color-mix(in srgb, var(--card-tint) 62%, transparent);
  border: var(--card-border-w) solid color-mix(in srgb, var(--border) 72%, transparent);
  border-radius: var(--radius);
  box-shadow: var(--card-shadow);
  backdrop-filter: blur(max(var(--card-blur), 6px));
  -webkit-backdrop-filter: blur(max(var(--card-blur), 6px));
  font-size: 0.82rem;
  color: var(--muted);
  text-align: left;
}

.refSec { margin-top: 18px; }
.refSec:first-child { margin-top: 0; }

.refHead {
  margin: 0;
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--accent);
}

.refProse {
  margin: 7px 0 0;
  line-height: 1.55;
  color: var(--muted);
}
.refWait { opacity: 0.65; }

/* ---- the panels' tables --------------------------------------------------
   No table — a flowing line of prose per row (a name or word, a bullet,
   then the content that goes with it), wrapping as ordinary text instead of
   being confined to a rigid grid column. A table read as heavier than the
   text it held, especially once one column wrapped to several lines and its
   neighbour sat there empty beside them. */
/* A long list (many Forms rows, say) reflows into side-by-side columns
   instead of one ever-taller single line — the same "let the browser work
   out how many columns fit" trick as the Cheat Sheet's .cshList below, so a
   long panel uses the width its shorter sibling panel left idle instead of
   just growing past it. break-inside keeps one row's label + example +
   translation together as a unit; a row that happens to land at the top of
   a new column still carries its own top divider, same as any other row. */
.refFlowList {
  margin-top: 8px;
  /* One column always -- the sidecards are wide enough to read a row whole. */
  column-count: 1;
}
.refFlowRow {
  padding: 5px 0;
  border-top: 1px solid color-mix(in srgb, var(--accent) 16%, transparent);
  break-inside: avoid;
}
.refFlowList .refFlowRow:first-child { border-top: 0; padding-top: 0; }

/* A tense group (app.js: buildRefReference) — several persons of the same
   tense (aux/impf/k2/subj/pass/fut) sharing one heading instead of each
   repeating the tense name on its own row. Same outer divider and
   break-inside guard as a lone .refFlowRow, since it takes that row's place
   in the list. The rows inside sit close together with no divider of their
   own — they're all still the same tense, just a different person, so a
   line between every one of them read as more separation than there
   actually is; the one divider that does earn its keep (.refFlowPlBreak)
   sits only where the persons actually change register, singular to
   plural. */
.refFlowList .refFlowGroup:first-child { border-top: 0; padding-top: 0; }
.refFlowGroup {
  padding: 10px 0;
  border-top: 1px solid color-mix(in srgb, var(--accent) 16%, transparent);
  break-inside: avoid;
}
.refFlowGroupHead { color: var(--accent); font-weight: 600; margin-bottom: 6px; }
.refFlowGroup .refFlowRow { padding: 3px 0; }
.refFlowGroup .refFlowRow.refFlowPlBreak {
  margin-top: 5px;
  padding-top: 8px;
  border-top: 1px solid var(--border);
}

.refFlowHead { color: var(--text); line-height: 1.45; }
.refFlowName { color: var(--accent); font-weight: 400; }
.refBullet { margin: 0 6px; color: var(--muted); opacity: 0.6; }
.refFlowEx { color: var(--text); }

/* A case-pair row (akk/dat — app.js: refCasePairSpan). Name, sentence and
   every checked language's translation all flow on one bulleted line now
   (.refBullet between each); the two forms that differ sit inline inside
   their shared carrier phrase too, "Nach der/den Umarmung(en) …", instead
   of stacked as two lines. --accent always marks the first form passed in
   (singular, nominative, whichever "slot A" is for that row), --accent2
   always the second — same two colours in the target sentence and in every
   translation, so a learner can match "der …Umarmung" to "…объятия" by
   colour without re-reading which is which. */
.refPairA { color: var(--accent); }
.refPairB { color: var(--accent); }

/* The simple half of the Forms section — a name and a bare value, no
   sentence — flows several to a line instead of each claiming a full row:
   a page of one-word facts stacked vertically was mostly empty space. */
.refFormFlow {
  display: flex;
  flex-wrap: wrap;
  column-gap: 20px;
  row-gap: 8px;
  margin-top: 8px;
}
.refFormItem { display: inline-flex; align-items: baseline; }
.refFormFlow + .refFlowList { margin-top: 12px; }
/* A form's translation, one per checked language, chained after its example
   on the same bulleted line (.refBullet between each) — same treatment as
   the case-pair and word-family rows just above/below. */
.refFlowTr { font-size: 0.82rem; color: var(--muted); }

/* The word family's word keeps the body colour rather than the accent —
   colour in there means something specific (prefix/suffix/…) and a green
   key beside it would not — while each translation chained after it is
   muted, same as any other secondary text. */
.famWord {
  color: var(--text);
  font-weight: 600;
  /* A segmented word broken across two lines is unreadable — the dots stop
     meaning "here is the seam" and start meaning "here is the line end" —
     and it takes its speaker button with it. */
  white-space: nowrap;
}
.famTrInline { color: var(--muted); font-weight: 400; }
/* The article is not a part of the word being built, so it stays out of the
   segmentation and out of its colours. */
.famArt { color: var(--muted); font-weight: 400; }

/* ---- the family-word preview popover -------------------------------------
   The small book icon on every Word family row, and the popover it opens: a
   miniature study card for a word that, on the row itself, is nothing more
   than a segmented spelling and a translation. Hover on a device that has one,
   tap everywhere — the icon inherits the muted/active treatment the panel
   handles (#refBtns .refBtn) already use, at a size that fits inline with a
   line of text instead of sitting in a card corner. */
.wordPreviewBtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 22px;
  margin-left: 2px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--muted);
  opacity: 0.55;
  font-size: 0.8rem;
  cursor: pointer;
  transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease;
  vertical-align: -3px;
}
.wordPreviewBtn:hover, .wordPreviewBtn:focus-visible { opacity: 1; color: var(--accent); }

/* The popover itself: fixed to the viewport (not the panel it was opened from)
   so it can float clear of a right panel that is itself a fixed sheet on a
   narrow screen. Styled like the reference side panels — same muted surface —
   at a size that reads as "a note about one word", not a third card. */
.wordPreview {
  position: fixed;
  z-index: 70;
  /* A fixed width, not fit-content: shrink-to-fit sizes the box exactly to
     its widest line (usually the forms-chip row) with zero slack left over,
     so an example sentence that nearly fills that width has nowhere left
     for its trailing speaker icon and the icon drops to its own line. A
     stable width lets text reflow normally within it — the same reason the
     main study card, which is also a fixed width, never has this problem. */
  width: min(90vw, 340px);
  max-width: min(90vw, 340px);
  min-width: 200px;
  max-height: min(82vh, 600px);
  overflow-y: auto;
  padding: 14px 16px;
  background: color-mix(in srgb, var(--card-tint) 88%, var(--bg));
  /* A real blur, not just a tinted background: this floats over the busiest
     part of the screen (the card, or another panel), and on a translucent
     skin a tint alone still let whatever was behind it show through sharp
     and readable, competing with the popover's own text. Blurred, it reads
     as frosted glass instead of a second layer of text. */
  backdrop-filter: blur(max(var(--card-blur), 14px));
  -webkit-backdrop-filter: blur(max(var(--card-blur), 14px));
  border: 1px solid color-mix(in srgb, var(--accent) 30%, var(--border));
  border-radius: 14px;
  /* Not var(--card-shadow) — every overlay in this file (dropdown panels,
     the settings/account trays, the stats-page modal, the confirm dialog)
     deliberately hardcodes its own elevation shadow instead of reading
     that variable, because all 3 selectable Designs zero it to `none` for
     ordinary in-page cards. This popover was the one overlay that still
     read the variable, so it had zero shadow on every Design a user can
     actually pick (user report with screenshot, 2026-09-07: a popover
     overlapping study-card text had no visual boundary between the two).
     Same value the other tray/dropdown-weight overlays use, not a modal's
     stronger one — a word-preview popover is closer in kind to those. */
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.22);
  font-size: 0.86rem;
  text-align: left;
}

.wordPreviewHead {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 6px;
  font-family: var(--font-head);
  font-weight: 700;
  font-size: 1rem;
  color: var(--text);
}
/* The form chips (gender, plural — inline with the word now, not a section
   of their own below) keep their own weight and size, not the header's. */
.wordPreviewHead .wordPreviewForms { margin-top: 0; font-weight: 400; }
.wordPreviewHead [dir="auto"] { flex: none; }

.wordPreviewTr {
  margin-top: 4px;
  color: var(--muted);
}

.wordPreviewForms {
  display: flex;
  flex-wrap: wrap;
  gap: 5px;
  margin-top: 9px;
}

.wordPreviewLadder { margin-top: 10px; }
.wordPreviewLadder .csBar { justify-content: flex-start; }
.wordPreviewLadder .csMeta { margin-top: 4px; }

.wordPreviewEx {
  margin-top: 10px;
  padding-top: 9px;
  border-top: 1px solid var(--border);
}
.wordPreviewExTarget { color: var(--text); font-weight: 600; }
.wordPreviewExTr { margin-top: 2px; color: var(--muted); font-size: 0.82rem; }

.wordPreviewRef {
  margin-top: 11px;
  padding-top: 10px;
  border-top: 1px solid color-mix(in srgb, var(--accent) 22%, transparent);
}
.wordPreviewRefHead {
  font-size: 0.7rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--accent);
  margin-bottom: 6px;
}
.wordPreviewRefLink {
  display: block;
  margin-top: 9px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--accent);
  font-size: 0.8rem;
  font-weight: 600;
  cursor: pointer;
  text-align: left;
}
.wordPreviewRefLink:hover { text-decoration: underline; }

/* ---- word-family segment colours ----------------------------------------
   Fixed hues rather than anything derived from --accent: the accent moves with
   all fifteen skins, and a derived scale would collide with itself on half of
   them. Underlined text rather than a filled background, so the row still
   reads on a translucent skin. The root stays in the body colour — it is what
   every row has in common, so colouring it would say nothing. */
:root {
  --seg-p: #3b5bdb;   /* prefix */
  --seg-s: #d9480f;   /* suffix */
  --seg-i: #862e9c;   /* stem change */
  --seg-l: #0b7285;   /* linking element */
  --seg-c: #5c940d;   /* second compound stem */
}
@media (prefers-color-scheme: dark) {
  :root:not([data-theme]) {
    --seg-p: #91a7ff;
    --seg-s: #ffa94d;
    --seg-i: #e599f7;
    --seg-l: #66d9e8;
    --seg-c: #a9e34b;
  }
}
:root[data-theme="dark"] {
  --seg-p: #91a7ff;
  --seg-s: #ffa94d;
  --seg-i: #e599f7;
  --seg-l: #66d9e8;
  --seg-c: #a9e34b;
}

/* Sketched underline (the app's one --pencil-underline mask, in the segment's
   own colour via currentColor), not a straight text-decoration line. */
.segWord .seg { position: relative; }
.segWord .seg::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  bottom: -8px;
  height: 14px;
  background-color: currentColor;
  pointer-events: none;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 36px 14px;
          mask-size: 36px 14px;
}
.segWord .seg-r::after { content: none; }
.seg-p { color: var(--seg-p); }
.seg-s { color: var(--seg-s); }
.seg-i { color: var(--seg-i); }
.seg-l { color: var(--seg-l); }
.seg-c { color: var(--seg-c); }
.seg-e { color: var(--muted); }
.segWord .seg-r { color: inherit; text-decoration: none; }

/* The dot is not decoration: it is what makes the split readable to anyone who
   cannot tell the hues apart, which is the whole reason colour is never the
   only signal here. */
.segDot {
  margin: 0 1px;
  color: var(--muted);
  opacity: 0.55;
  font-weight: 400;
}

.segLegend {
  display: flex;
  flex-wrap: wrap;
  gap: 4px 12px;
  margin-top: 9px;
  font-size: 0.71rem;
  color: var(--muted);
}
.segKey { display: inline-flex; align-items: center; gap: 5px; }
.segSwatch {
  width: 22px;
  height: 10px;
  background: currentColor;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
}

/* ---- the two handles on the card ----------------------------------------
   In the card's bottom corners, in the flow rather than pinned: the card
   scrolls inside itself, and an absolutely positioned handle would either
   scroll away from the corner or cover the text it sits on. Hidden while the
   card is face-down — a word family and its forms are the answer. */
#refBtns {
  display: flex;
  align-items: center;
  justify-content: space-between;
  /* Kept inside cardStats' own horizontal inset — flush with the card's
     rounded corners read as spilling past the divider line above, rather
     than belonging to the row it shares. Only the bottom margin still
     extends past #cardFoot's padding, flush with the card's bottom edge. */
  margin: 0 0 -22px;
}
#refBtns:empty { display: none; }
#card.answer-hidden #refBtns { display: none; }

.refBtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--muted);
  opacity: 0.5;
  font-size: 1.15rem;
  cursor: pointer;
  transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.refBtn:hover { opacity: 1; color: var(--text); }
.refBtn.active {
  opacity: 1;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}

#refScrim { display: none; }

@media (max-width: 1180px) {
  /* No room for a side column: the panel drops into the flow below the
     card instead of sliding in as a fixed sheet over it — a sheet+scrim
     covering the card is exactly the "hidden/shadowed" complaint a panel
     should never cause. `order` puts it after the card regardless of DOM
     position. #deck becomes a column and scrolls itself once the card plus
     an open panel run taller than the space available — the phone tier
     below layers tighter spacing on top of this same structure. */
  #deck { flex-direction: column; justify-content: flex-start; overflow-y: auto; }
  /* flex-shrink: 0 — without it, a flex item whose own overflow isn't
     `visible` gets an automatic minimum size of 0 (the flexbox spec's "long
     content in a scrollable box" carve-out), so a card (and the study card,
     below) would rather shrink itself down and scroll its own tiny window
     than let #deck grow past its slot and scroll as a whole. Word family is
     given the lower order so it stacks above Forms, matching its position to
     the left of Forms on the wide layout — #card sits between them at the
     default order (0) regardless of where it falls in the DOM. */
  #refPanelFamily { order: 2; flex-shrink: 0; }
  #refPanelForms { order: 3; flex-shrink: 0; }
  /* The card no longer scrolls its own content on this layout — it grows to
     whatever height it needs and #deck (above) scrolls the whole column
     instead, same as the panel already does. A card whose own box scrolled
     independently of the page around it read as "stuck"/collapsed, and hid
     the Know/Don't-know controls' relationship to where you actually were
     in the card.
     margin: auto 0 (rather than centering #deck itself with
     justify-content) is what actually vertically centers it when it's
     short enough to fit: auto margins soak up the leftover space evenly
     above and below, and collapse to 0 the moment the card (plus an open
     panel) is tall enough to need #deck's own scroll — so the centering
     never fights the "scroll from the top" behavior above. Centering via
     justify-content instead would have clipped the card's top out of
     reach of that scroll the moment content overflowed. */
  #card { flex-shrink: 0; max-height: none; overflow-y: visible; margin: auto 0; }
  /* align-self: center overrides .refPanel's own align-self: stretch — at
     this width the panel is already capped at max-width: 480px, so a
     stretch just gets clamped down to that and left where stretch always
     starts (the cross-axis start edge), flush against #deck's left edge
     instead of sitting under the card it belongs to. */
  .refPanel { width: 100%; max-width: 480px; max-height: none; margin-top: 14px; align-self: center; }
  #refScrim { display: none !important; }
}

.formChip {
  display: inline-flex;
  align-items: baseline;
  gap: 6px;
  font-size: 0.86rem;
  padding: 4px 11px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, var(--accent) 28%, var(--border));
  background: color-mix(in srgb, var(--accent) 7%, transparent);
  color: var(--fg);
}
/* The label is the quiet half — the form itself is what is being taught. */
.formChip b {
  font-size: 0.72rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--muted);
}
/* ---- the forms drill ---------------------------------------------------
   A different question from the rest of the app — not what the word means but
   how it behaves — so the card leads with the word, states the question, and
   then either offers the handful of possible answers or takes a typed one. */
.formPrompt { display: flex; flex-direction: column; align-items: center; gap: 12px; }
/* Everything on a Forms card is inside #word: the headword, the question and
   the answer controls. The other blocks are empty, and each still contributed
   its bottom margin — five of them, on a card whose content is 140px tall. */
/* Revealed, the answer is followed by the form chips and they were touching
   its underline. (Face-down needs no gap either, but that is now true of every
   mode and handled with the rest of the hidden card, up by #word.) */
#card.forms-card:not(.answer-hidden) #word { margin-bottom: 22px; }
#card.forms-card #synonyms,
#card.forms-card #wordReading { display: none; }
#card.forms-card #translations:empty,
#card.forms-card #examples:empty { display: none; margin: 0; }
#card.forms-card.answer-hidden #translations,
#card.forms-card.answer-hidden #examples,
#card.forms-card.answer-hidden #cardStats { display: none; }
.formHead {
  font-family: var(--font-head);
  font-weight: var(--head-weight);
  font-size: 1em;
}
.formQuestion {
  font-family: var(--font-body, inherit);
  font-size: 0.8rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  color: var(--muted);
}
/* The name the language being learned gives this form, next to the name in the
   learner's own. Not uppercased — it is a German word, and "PRÄTERITUM" shouts
   where "Präteritum" is simply what the textbook calls it. */
.formQuestion .formQTerm {
  text-transform: none;
  letter-spacing: 0;
  color: var(--accent);
}
.formQuestion .formQTerm::before {
  content: " · ";
  color: var(--muted);
  font-weight: 400;
}
/* Which person or number is being asked for — quieter than the form's name,
   because it is a qualifier rather than the question. */
.formQuestion .formQPerson {
  text-transform: none;
  letter-spacing: 0;
  font-weight: 400;
  color: var(--muted);
}
.formQuestion .formQPerson::before { content: " ("; }
.formQuestion .formQPerson::after { content: ")"; }
.formChoices { display: flex; flex-wrap: wrap; gap: 10px; justify-content: center; }
.formChoice {
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  font-family: inherit;
  font-size: 0.62em;
  font-weight: 700;
  padding: 10px 26px;
  border-radius: var(--radius-sm);
  border: 1px solid color-mix(in srgb, var(--accent) 40%, var(--border));
  background: color-mix(in srgb, var(--accent) 8%, transparent);
  color: var(--accent);
  transition: background 0.12s ease, transform 0.08s ease;
}
.formChoice:hover { background: color-mix(in srgb, var(--accent) 18%, transparent); }
.formChoice:active { transform: translateY(1px); }
/* Your answer and the right one, side by side rather than stacked. */
/* The meaning under a Forms headword. Quieter than the question, which is the
   thing actually being asked. */
.formMeaning { margin-top: 6px; margin-bottom: 2px; }

.formAnswer {
  font-size: 1em;
  display: flex;
  align-items: baseline;
  justify-content: center;
  gap: 16px;
  flex-wrap: wrap;
}
/* Inside the answer row it is a chip on a line, not a line of its own. */
.formAnswer .clozeYou { margin-top: 0; }

/* "Show me" — the way out of a question you cannot answer. Quiet on purpose:
   it is always available but should never be the obvious thing to press, since
   pressing it costs the same as getting the answer wrong. */
.formShowMe {
  position: relative;
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  font-family: var(--font-body, inherit);
  font-size: 0.78rem;
  font-weight: 600;
  letter-spacing: 0.02em;
  margin-top: 2px;
  padding: 5px 14px;
  border-radius: 0;
  border: none;
  background: none;
  color: var(--muted);
  opacity: 0.75;
  transition: opacity 0.12s ease, color 0.12s ease;
}
/* Invisible tap-target padding: the pill itself stays small and quiet, but a
   tap in the whitespace right around it still counts — the whole point of a
   pseudo-element here rather than just more padding, which would inflate the
   visible pill along with the hit area. */
.formShowMe::before {
  content: "";
  position: absolute;
  top: -10px;
  bottom: -10px;
  left: -12px;
  right: -12px;
}
/* Hover draws an underline in rather than boxing the pill in an outline
   (user feedback, 2026-09-19: "should not outline it, but create an
   animation of underlining") -- scaleX from 0, not a fade, so it reads as
   the line being drawn rather than just appearing. */
.formShowMe::after {
  content: "";
  position: absolute;
  left: 14px;
  right: 14px;
  bottom: 2px;
  height: 6px;
  background-color: currentColor;
  pointer-events: none;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.2s ease;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 26px 6px;
          mask-size: 26px 6px;
  -webkit-mask-position: center;
          mask-position: center;
}
.formShowMe:hover {
  opacity: 1;
  color: var(--text);
}
.formShowMe:hover::after,
.formShowMe:focus-visible::after {
  transform: scaleX(1);
}

/* A flag ("plural only") is all label and no value. */
.formChip.formFlag {
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  color: var(--muted);
  background: none;
}

#synonyms .synLabel {
  font-size: 0.7rem;
  text-transform: uppercase;
  letter-spacing: 0.06em;
  color: var(--muted);
}

/* Plain text, same convention as the Wordlist row's own copy of this list
   (.wlSyn .syn) — a "· " separator between multiple synonyms rather than a
   badge outline, since the badge/pill look (background, dashed border,
   pill radius) read as more UI chrome than a synonym needs. */
#synonyms .syn {
  font-size: 0.92rem;
  color: var(--muted);
}
/* A real element (app.js), not ::before — a linked chip's underline is an
   ancestor decoration that paints through a pseudo-element's own
   text-decoration: none, so the separator has to be an actual sibling
   outside the underlined chip to not get underlined itself. */
.synDot { color: var(--muted); }

#card.answer-hidden #synonyms { display: none; }
/* No synonyms for this word → collapse the row so it adds no gap. */
#synonyms:empty { display: none; }

/* A synonym chip that actually names a real headword (app.js: buildSynChip)
   opens the same word-preview popover a word-family row's book icon does —
   not scoped to #synonyms alone, since the Wordlist row's own copy of this
   list (.wlSyn .syn) shares the same chip markup. Underline rather than a
   colour change: a synonym chip already has its own dashed border for
   "aside", so the cue that this one is also a link has to be a different
   visual channel, not a competing border. */
.syn.synLinked {
  cursor: pointer;
  text-decoration: underline dotted color-mix(in srgb, var(--text) 35%, transparent);
  text-underline-offset: 2px;
}
.syn.synLinked:hover, .syn.synLinked:focus-visible {
  color: var(--accent);
  border-color: color-mix(in srgb, var(--accent) 45%, transparent);
}

/* ---- per-card learning history + ref-panel handles ----------------------
   One row at the foot of the card: cardStats (a ladder of pips — one per
   Leitner box, filled up to where this word has climbed — and a muted tally
   line) centred, the two ref-panel handles pinned to its left/right edges
   (see #refBtns below). Stacked in the same grid cell rather than two
   separate rows, so the row's height always follows cardStats — the taller
   of the two — and the handles stay vertically centred against it whether
   cardStats is two lines (a brand-new word) or three (with a tally). */
#cardFoot {
  display: grid;
  margin-top: 22px;
}
#card.answer-hidden #cardFoot { display: none; }
#cardStats, #refBtns { grid-column: 1; grid-row: 1; }

#cardStats {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  padding-top: 16px;
  border-top: 1px solid var(--border);
}

#card.answer-hidden #cardStats { display: none; }
#cardStats:empty { display: none; }

.csBar {
  display: flex;
  gap: 5px;
  align-items: center;
}

.csSeg {
  width: 20px;
  height: 5px;
  border-radius: 999px;
  background: color-mix(in srgb, var(--muted) 22%, transparent);
  transition: background 0.2s ease;
}

.csSeg.on { background: var(--accent); }

/* The last two rungs are the long-term check-ups, months after the word already
   counts as learned. Drawn shorter and set slightly apart so the ladder reads
   as "learned, plus a couple of check-ups" — not as a moved goalpost. */
.csSeg.csLate { width: 11px; opacity: 0.65; }
.csSeg.csLate.on { opacity: 1; }
.csSeg.csLate:not(.csLate + .csLate) { margin-left: 7px; }

.csMeta {
  font-size: 0.78rem;
  letter-spacing: 0.03em;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  /* Plain status text, not a selectable/editable field — without this the
     browser's own "this looks like text" heuristic shows an I-beam cursor
     right up against .csReset's own pointer cursor, so the tiny hop from
     text to the icon button one em away kept flipping the cursor glyph
     back and forth, making the button read as if it wouldn't hold still
     under the mouse. */
  cursor: default;
}

.csMeta.csDone { color: var(--accent); font-weight: 600; }

/* Sits inline right after the ✓/✗ tallies — small and muted like any other
   secondary icon in this row (BULB_SVG/BOOK_SVG's 1em convention), not a
   34px circular tray button like .refBtn: this one lives inside the small
   .csMeta text line, not its own row. */
.csReset {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  margin-left: 7px;
  padding: 0;
  border: 0;
  background: none;
  color: var(--muted);
  opacity: 0.55;
  font-size: inherit;
  vertical-align: -0.15em;
  cursor: pointer;
  transition: opacity 0.15s ease, color 0.15s ease;
}
.csReset svg { display: block; }
.csReset:hover, .csReset:focus-visible { opacity: 1; color: var(--text); }

/* Only shown on a word that keeps being missed: an acknowledgement and the way
   out. Amber rather than red — it is a nudge, not a telling-off. */
.csLeech {
  display: flex;
  align-items: center;
  justify-content: center;
  flex-wrap: wrap;
  gap: 10px;
  margin-top: 4px;
  font-size: 0.8rem;
  color: #d97706;
}
.csPark {
  appearance: none;
  -webkit-appearance: none;
  cursor: pointer;
  font: inherit;
  font-weight: 700;
  padding: 4px 12px;
  border-radius: 999px;
  border: 1px solid color-mix(in srgb, #d97706 45%, var(--border));
  background: color-mix(in srgb, #d97706 10%, transparent);
  color: #d97706;
}
.csPark:hover { background: color-mix(in srgb, #d97706 20%, transparent); }

/* ---- forget-everything tray item ---------------------------------------
   A settingToggle row like any other, tinted with the same red the
   wrong-answer states use elsewhere — a quiet signal that this one row
   undoes rather than adjusts. Constant colour, not a selection state, so
   it keeps --bad at rest -- but the hover affordance is the same underline
   as every other row, in --bad instead of --accent. */
#settingsTray .settingDanger { color: var(--bad); }
#settingsTray .settingDanger .settingLbl::after { background-color: var(--bad); }
#settingsTray .settingDanger:hover .settingLbl::after { transform: scaleX(1); }

/* ---- grammar cheat sheet ---------------------------------------------- */
/* Two panes that scroll independently, rather than one scrolling section with a
   sticky rail: the height available is then simply 100% of a parent that does
   not scroll, with no viewport arithmetic to get wrong, and the contents stay
   put while you read. Collapses back to a single scroller on mobile below. */
#grammar {
  flex: 1;
  min-height: 0;
  overflow: hidden;
  padding: 8px 20px 0;
}

#grammar.hidden { display: none; }

/* The contents and the sheet, centred as a pair — so the rules stay where the
   eye expects them and the tree hangs off their left edge. The cap is exactly
   the two panes plus the gap, so neither ends up with dead space beside it and
   the pair's optical centre is the same as its geometric one. */
.gLayout {
  display: flex;
  align-items: stretch;
  gap: 28px;
  max-width: 1178px;
  height: 100%;
  margin: 0 auto;
}
/* #grammarIndex hidden (locked Reference tab, gating-model-decision) leaves
   #grammarBody as the row's only child — center that single pane in the
   space the sidebar would have occupied, rather than letting flex's default
   flex-start leave it hugging the left edge. */
.gLayout:has(#grammarIndex.hidden) { justify-content: center; }

/* No `scrollbar-width: thin` here: the tree used to be the one place in the app
   that drew a bar, and a permanent rule down the middle of the layout is a
   worse answer to "there is more below" than the list running to the edge.
   Falls through to the global rule, so it still scrolls — just silently. */
/* Wide enough that most topic titles are one line. At 208px entries like
   "Perfekt with haben and sein" broke over two or three lines, which turns a
   list you scan into a list you read — and the wrapped continuation lines sat
   under the icon, so the left edge no longer told you where an entry started.
   270px keeps that and hands the difference to the tables, which use it. */
#grammarIndex {
  flex: 0 0 270px;
  overflow-y: auto;
  padding: 16px 14px 40px;
  /* Same soft-card recipe .gTopic uses (radius + blur, no border, no
     shadow) — against a Background pattern, plain text directly on
     #bgPattern was hard to read; this reads as a card floating over the
     pattern, like every other panel on the page, rather than a
     hard-edged blur box. Deliberately NOT var(--card-tint) at full
     strength: most Designs resolve that to a fully opaque, unblurred
     card (see :root and most [data-design] blocks above), but the
     Reference page's own two panes stay blurred regardless of the
     active Design — a standing request for this page specifically, not
     a global card restyle.
     The local dilution used to go all the way down to 55% opacity,
     leaning on this panel's own soft shadow to still read as a distinct
     surface despite how washed-out that left it -- once that shadow (and
     any border) was banned outside Herbarium (user feedback), the same
     55% became unreadable on its own, especially compounded with
     Designs like Dawn where --card-tint is ALREADY translucent (72%) in
     its own right. 92% keeps a last trace of glassiness (backdrop-filter
     still has something to blur) while reading as solidly light/on-brand
     again -- contrast now comes from the fill itself, not from an edge
     effect propping up a too-thin one. */
  background: color-mix(in srgb, var(--card-tint) 92%, transparent);
  border: none;
  border-radius: var(--radius);
  box-shadow: none;
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
}

/* The heading is the mobile disclosure control. On a wide screen there is
   nothing to disclose, so it reads as a plain label and stops being a button.
   Shared with the songbook rail, which is the same control over the same kind
   of tree — one rule, so the two cannot drift apart. */
.tocLabel {
  display: block;
  width: 100%;
  margin: 0 0 8px;
  padding: 0;
  border: 0;
  background: none;
  font: inherit;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.09em;
  text-transform: uppercase;
  text-align: left;
  color: var(--muted);
  cursor: default;
}

.gTocGroup + .gTocGroup { margin-top: 14px; }

/* The level a group of topics belongs to — A1, or whatever this target calls
   it. Quiet: it is a divider, not an entry you can click. */
.gTocLevel {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--accent);
  padding: 0 0 4px 2px;
  border-bottom: 1px solid var(--border);
  margin-bottom: 4px;
}

.gTocLink {
  display: flex;
  align-items: baseline;
  gap: 7px;
  width: 100%;
  padding: 4px 8px 4px 6px;
  border: 0;
  border-left: 2px solid transparent;
  border-radius: 0 7px 7px 0;
  background: none;
  font: inherit;
  font-size: 0.82rem;
  line-height: 1.3;
  text-align: left;
  color: var(--text);
  cursor: pointer;
}
/* No hover background tint -- the same --pencil-underline mask every other
   hover-select component in the app uses, not the text-decoration wavy
   trick tried here in earlier rounds (user feedback, 2026-09-25: "selection
   in tales/grammar should also use our proper underline"). Scoped to the
   title span, not the row, so it only runs as wide as the text (same
   .viewBtnLabel/.modeBtnLabel convention). A title that wraps to 2 lines
   will underline only under the full box (bottom-anchored), not per line
   -- a known trade-off versus the old text-decoration approach, which
   followed wrapped lines natively. .mTocName (Tales rail, below) has since
   moved to a per-line fix; .gTocTitle (Grammar reference) keeps this
   single-box version -- its topic titles aren't long enough in practice to
   make the trade-off visible, so there's nothing to revisit there yet. */
.gTocTitle {
  position: relative;
  display: inline-block;
}
.gTocTitle::after {
  content: "";
  position: absolute;
  left: 0;
  right: 0;
  bottom: -3px;
  height: 6px;
  background-color: var(--accent);
  pointer-events: none;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.2s ease;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 26px 6px;
          mask-size: 26px 6px;
}
.gTocLink:hover .gTocTitle::after { transform: scaleX(1); }

/* .mTocName (a tale's title in the Tales rail): the single-box underline
   above stretches to the row's full width once a title wraps to a second
   line, instead of tracking just the short remainder of text on that line
   -- user feedback, 2026-09-30, screenshotted on a 2-line title. Fixed
   with a per-LINE underline instead: paintWrapUnderline() in
   11-lyrics-songbook-media.js measures each wrapped line's own rendered
   rect (via Range.getClientRects(), which returns one rect per visual
   line) and drops one .gTocUL bar per line, each sized to that line's
   actual text -- same mask/colour recipe as .gTocTitle::after above, just
   real child elements instead of one pseudo-element, since a pseudo-
   element can't be cloned per wrapped line in CSS alone. */
.mTocName {
  position: relative;
  display: inline-block;
}
.gTocUL {
  position: absolute;
  height: 6px;
  background-color: var(--accent);
  pointer-events: none;
  transform: scaleX(0);
  transform-origin: left;
  transition: transform 0.2s ease;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 26px 6px;
          mask-size: 26px 6px;
}
.mTocSong:hover .mTocName .gTocUL {
  transform: scaleX(1);
}

/* A plain running number in front of every row — replaces the old per-topic
   emoji (t.icon), removed 2026-09-19. A pencil-icon bullet was tried first
   and rejected ("appoint a number ... not the 'edit' icon" — a pencil reads
   as an edit affordance, which this isn't). Resets to 1 at each level
   group, same grouping .gTocLevel already shows. */
.gTocLink .gTocBullet {
  flex: 0 0 auto;
  min-width: 1.3em;
  font-size: 0.78rem;
  font-variant-numeric: tabular-nums;
  color: var(--muted);
  opacity: 0.85;
}

/* Where you are in the sheet. Set by the scroll spy, so scrolling the rules
   moves the marker — a contents list that cannot answer "where am I" is only
   half a contents list. Underlined, not filled (user feedback, 2026-09-19),
   now the same --pencil-underline mask as .gTocTitle/.mTocName's hover
   state above (user feedback, 2026-09-25). Text stays --text -- var(--fg)
   was a stale/undefined custom property (dead CSS, silently fell back to
   the inherited colour anyway); only the underline carries --accent, same
   #levelPanel .levelBtn.active precedent as every other selected-state fix
   this round. */
.gTocLink.on,
.mTocSong.on {
  border-left-color: var(--accent);
  background: none;
  color: var(--text);
}
.gTocLink.on .gTocTitle::after { transform: scaleX(1); }
.mTocSong.on .mTocName .gTocUL { transform: scaleX(1); }
/* Under a Design, the active topic drops the left stripe -- still the same
   underline, in the palette's main --accent (not --accent2/secondary,
   user feedback 2026-09-24: "colour must correspond to the first/main
   colour of the palette"). Text stays --text here too, not --accent (user
   feedback, 2026-09-25 -- see .gTocLink.on above). */
[data-design] .gTocLink.on,
[data-design] .mTocSong.on {
  border-left-color: transparent;
  background: none;
  color: var(--text);
}

/* A rule is one sentence and an example; at 1020px those ran the full width of
   the pane and the eye had to travel back across the whole sheet for the next
   one. 880px is set by the tables rather than the prose — it is where a
   six-column conjugation grid stops scrolling sideways — and is still short of
   the measure at which running text becomes hard to track. */
#grammarBody {
  flex: 1;
  min-width: 0;
  max-width: 880px;
  overflow-y: auto;
  padding-bottom: 60px;
}

/* Between the mobile disclosure and the full pair there is no room to spend
   270px on the tree — the sheet would be the one paying, and the sheet is what
   you came for. The tree gives the width back and titles wrap again. */
@media (min-width: 901px) and (max-width: 1150px) {
  #grammarIndex { flex: 0 0 210px; }
}

/* Narrow: the tree has nowhere to stand, so it becomes a disclosure above the
   sheet — shut by default, costing one line of height, which is what the old
   scrolling strip was protecting. One scroller again, as everywhere else. */
@media (max-width: 900px) {
  #grammar { overflow-y: auto; padding-bottom: 60px; }
  .gLayout { display: block; height: auto; }
  #grammarIndex {
    overflow: visible;
    /* Horizontal padding dropped to 0 here used to leave "MỤC LỤC" and every
       topic row touching the card's own rounded edges directly (user
       feedback, screenshot) -- the mobile override only meant to trim the
       generous desktop padding down for a compact disclosure header, not
       remove the sides entirely; border-radius/background/shadow/blur all
       still come from the base #grammarIndex rule above regardless of this
       override, so it stays the same rounded card either way. */
    padding: 10px 14px 8px;
    margin-bottom: 4px;
    border-bottom: 1px solid var(--border);
  }
  #grammarBody { max-width: none; overflow: visible; padding-bottom: 0; }
  .tocLabel { cursor: pointer; margin-bottom: 0; }
  .tocLabel::after { content: " ▾"; }
  #grammarIndex.open #gTocToggle { margin-bottom: 8px; }
  #grammarIndex.open .tocLabel::after,
  #mediaIndex.open .tocLabel::after { content: " ▴"; }
  #grammarIndex:not(.open) #gTocTree,
  #mediaIndex:not(.open) #mTocTree { display: none; }
  /* #mTocSlot (the search box, once moved into the rail -- placeMediaSearch)
     is #mTocTree's SIBLING, not its child, so the rule above never touched
     it: collapsed, the disclosure still showed a live search box with
     nothing under it to search through. User feedback, 2026-09-30,
     screenshotted on Tales. Grammar's rail has no search box of its own,
     so this is Media-only. */
  #mediaIndex:not(.open) #mTocSlot { display: none; }
}

/* Deliberately NOT the standard solid #card look (background: var(--card-tint)
   at ~full opacity, hard 1px border) -- Reference's topic sheet is glass: a
   translucent tint the page's own background pattern shows through and a
   real blur behind it, no hard edge and (user feedback: no shadowing
   outside Herbarium, which superseded this panel's own former soft
   text-tinted shadow too) no shadow either. Local opacity raised from the
   original 55% to 92% (see #grammarIndex's own comment above, the exact
   same fix) -- unreadable once nothing but that dilution stood between
   the panel and a similarly-toned page background, with no shadow or
   border left to define its edge instead. Independent of --card-blur
   (which most Designs leave near 0 for the ordinary #card) so this
   stays glassy even under Designs that keep their flashcards crisp. */
.gTopic {
  position: relative;
  background: color-mix(in srgb, var(--card-tint) 92%, transparent);
  border: none;
  border-radius: var(--radius);
  box-shadow: none;
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  padding: 22px 22px 20px;
  margin-top: 16px;
  scroll-margin-top: 12px;
}
/* margin-top above exists to separate consecutive topic panels from each
   other, not to push the very first one down from the top of
   #grammarBody -- left unguarded, that 16px was the only difference
   between #grammarIndex's own box (no margin) and the sheet's first
   panel, so the two didn't start at the same height (user feedback,
   screenshot). #grammarBody's own children are plain .gTopic elements
   (so :first-child here always reaches the real first topic). */
#grammarBody > .gTopic:first-child { margin-top: 0; }

.gTopic h2 {
  font-family: var(--font-head);
  font-size: 1.25rem;
  font-weight: var(--head-weight);
  margin: 0 0 2px;
}

.gTopic h2 .gDe {
  font-size: 0.85rem;
  font-weight: 600;
  color: var(--muted);
  margin-left: 10px;
}

.gTopic .gIntro {
  color: var(--text);
  font-size: 0.95rem;
  line-height: 1.5;
  margin: 8px 0 4px;
}

.gTopic .gIntro.alt { color: var(--muted); font-size: 0.9rem; }

/* A long, narrow table (every cardinal number, say) as several side-by-side
   groups instead of one column of mostly-empty width. Each group is its
   own full <table class="gTable"> (own header, own borders), just laid out
   in a grid rather than stacked. */
.gTableGrid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
}
.gTableGrid table.gTable { width: 100%; }
@media (max-width: 900px) {
  .gTableGrid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
}
@media (max-width: 480px) {
  .gTableGrid { grid-template-columns: 1fr; }
}

.gTableWrap {
  margin: 16px 0 4px;
  padding: 3px;
  position: relative;
  border: none;
  border-radius: 0;
}
.gTableWrap::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
/* The actual scroller — see renderTable()'s own comment on why this can't
   be .gTableWrap itself. */
.gTableScroll { overflow-x: auto; }

.gCaption {
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--accent);
  padding: 10px 12px;
}

table.gTable {
  border-collapse: collapse;
  width: 100%;
  font-size: 0.9rem;
}

table.gTable th,
table.gTable td {
  /* Hand-drawn grid lines, not a flat border: each cell paints its own
     bottom+right edge (border-collapse can't be used with a background
     image, since collapsed cells share one border box between neighbours)
     -- the perimeter comes from .gTableWrap's own brush ::before instead,
     so the last row/column drop their own bottom/right line to avoid
     doubling up with it (below). background-COLOR, never the `background`
     shorthand, in every rule that also needs a fill (thead th/.gRowLabel) --
     the shorthand resets this image layer to none. */
  border: none;
  background-repeat: repeat-x, repeat-y;
  background-position: bottom left, top right;
  background-size: 40px 8px, 8px 40px;
  background-image: var(--brush-h), var(--brush-v);
  padding: 7px 11px;
  text-align: left;
  vertical-align: top;
  /* Most cells (a form, a number) are short enough to sit on one line
     regardless; a prose column (a "Breakdown" explanation, a long word list)
     is not, and forcing it onto one line anyway is what stretched the whole
     table past the viewport instead of letting the auto layout shrink and
     wrap it to fit. */
  white-space: normal;
}
/* Only for a standalone table sitting directly in the scroller — its own
   outer edge IS .gTableWrap's perimeter. A table inside .gTableGrid keeps
   all four of its own edges: it sits beside its sibling groups, not against
   the shared wrap's edge, and dropping its line there would erase the only
   separator between two side-by-side groups.
   `tbody tr:last-child`, not a bare `tr:last-child` -- :last-child is
   per-parent, and a <thead> with a single row IS that row's last-child
   too, so the bare selector was also (wrongly) stripping the header row's
   own bottom line, not just the table's true last row (user report,
   screenshot: header row had no line under it at all). */
.gTableScroll > table.gTable tbody tr:last-child th,
.gTableScroll > table.gTable tbody tr:last-child td {
  background-image: var(--brush-v);
  background-position: top right;
  background-size: 8px 40px;
  background-repeat: repeat-y;
}
.gTableScroll > table.gTable th:last-child,
.gTableScroll > table.gTable td:last-child {
  background-image: var(--brush-h);
  background-position: bottom left;
  background-size: 40px 8px;
  background-repeat: repeat-x;
}
.gTableScroll > table.gTable tbody tr:last-child th:last-child,
.gTableScroll > table.gTable tbody tr:last-child td:last-child {
  background-image: none;
}
table.gTable thead th {
  background-color: var(--bg);
  font-size: 0.78rem;
  letter-spacing: 0.02em;
  color: var(--muted);
  position: sticky;
  top: 0;
}
/* The artifact's header cells sit flush on the card, no fill of their own. */
[data-design] table.gTable thead th { background-color: transparent; }

table.gTable th.gRowLabel,
table.gTable td.gRowLabel {
  font-weight: 600;
  /* a neutral grey wash over the card, not --bg: the page background carries
     the design's warm tint, which read as yellow in the first column */
  background-color: color-mix(in srgb, var(--text) 6%, var(--card-bg));
  color: var(--text);
  white-space: normal;
}

.gSection {
  margin-top: 16px;
}

.gSection h3 {
  font-size: 0.72rem;
  text-transform: uppercase;
  letter-spacing: 0.07em;
  color: var(--muted);
  margin: 0 0 8px;
}

.gExample {
  border-left: 3px solid var(--border);
  padding: 2px 0 2px 12px;
  margin-bottom: 12px;
}

/* Same convention as .mIdiom/.mAbbr/.wlRow's own RTL-alignment fix just
   below in this file: the original (target-language) sentence stays at the
   card's left edge like every other target's -- dir="auto" (set where this
   is built, 09-actions-stats-grammar.js) correctly right-aligns an RTL
   target's own script, but that puts Hebrew/Arabic examples on the opposite
   side from every other language's, and a learner scanning down a list of
   examples needs the original in the same place every time (user feedback,
   2026-09-30). Only the block's own alignment is forced -- dir stays
   language-driven so the Hebrew/Arabic text itself still shapes and orders
   right-to-left internally. */
.gExample .gExDe { font-weight: 600; margin-bottom: 3px; text-align: left; }

/* Romanisation under a CJK grammar example (pinyin / romaji / RR). */
.gExample .gExReading {
  color: var(--muted);
  font-size: 0.85rem;
  font-style: italic;
  letter-spacing: 0.01em;
  margin-bottom: 4px;
}

.gExample .gExTr {
  color: var(--muted);
  font-size: 0.9rem;
  line-height: 1.4;
}

.gHints {
  margin-top: 16px;
  background: var(--bg);
  position: relative;
  border: none;
  border-radius: 0;
  padding: 12px 14px;
}
.gHints::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}

.gHint {
  display: flex;
  gap: 10px;
  padding: 5px 0;
  line-height: 1.45;
  font-size: 0.92rem;
}

.gHint + .gHint { border-top: 1px dashed var(--border); }

.gHint .gHintLang {
  flex-shrink: 0;
  font-size: 1.1rem;
  line-height: 1.3;
  min-width: 22px;
  padding-top: 1px;
}

#grammar.hidden,
#deck.hidden { display: none; }

/* Badges sit on a single scrolling line, so each keeps its natural width. */

/* ---- mobile / small screens ------------------------------------------- */
html { overflow-x: hidden; }
body { -webkit-tap-highlight-color: transparent; }

/* Whichever of these is the actual scrolling middle panel at the current
   breakpoint (#deck/#grammar/#grammarBody/#media/#mediaBody/#stats each get
   their own overflow-y: auto at various widths) never hands a scroll
   gesture on past its own bounds once its content runs out -- an exhausted
   drag/swipe used to keep "scrolling" into blank space past the real
   content instead of just stopping there (user feedback, screenshot), even
   through a non-linear swipe path. `none`, not `contain`: contain only
   stops the gesture chaining on to an ancestor once THIS element's own
   scroll is exhausted -- iOS Safari still plays its own local rubber-band
   bounce/glow on the element itself either way, which is the actual "blank
   space" being reported (confirmed still visible after switching to
   contain alone). none suppresses that local overscroll effect too, not
   just the chaining. Harmless to set even when an element isn't currently
   overflowing at all. */
#deck, #grammar, #grammarBody, #media, #mediaBody, #stats {
  overscroll-behavior-y: none;
}

@media (max-width: 640px) {
  /* Pin the header and the Know/Don't-know controls to the viewport; only the
     middle panel (card, or grammar/stats) scrolls when its content overflows.
     `position: fixed` rather than a height + `overflow: hidden` — a percentage
     or `dvh` height can be a fraction of a pixel taller than the real visible
     area once a mobile browser's collapsing toolbar is in the mix, and that
     sliver was enough for the page itself to become scrollable despite every
     child fitting with room to spare. Fixed positioning pins body to the
     actual visual viewport directly, so there is no height arithmetic left to
     round: the page as a whole simply cannot scroll, in either direction. */
  /* overflow-y here on html too (not just the page-wide overflow-x: hidden
     above), plus overscroll-behavior on both: iOS Safari can still let a
     fast or non-linear drag trigger a page-level bounce/rubber-band despite
     body's own fixed positioning below -- confirmed still happening after
     only fixing the individual scrolling panels further down (user
     feedback). Scoped to this same ≤640px tier as body's own fix, not
     applied page-wide, since desktop's layout isn't guaranteed to keep
     every view's height within the viewport the way this tier's panels are
     built to. */
  html { overflow-y: hidden; overscroll-behavior: none; }
  body { position: fixed; inset: 0; min-height: 0; height: auto; overflow: hidden; overscroll-behavior: none; }

  /* iOS Safari's compact/floating address bar isn't reserved chrome the way
     a notch is — it can float directly over the top of a `position: fixed`
     header instead of pushing it down, so the tabs and language dropdowns
     ended up drawn partly underneath it. `env(safe-area-inset-top)` grows to
     cover it while it's showing (0 everywhere it isn't, so this is a no-op
     on a browser without one); `max()` keeps the header's own 10px on top of
     whatever that inset adds rather than being replaced by it. */
  #topbar { padding: max(10px, env(safe-area-inset-top)) 12px 10px; }

  /* One row and no more, same as desktop: #topleft (language, From, tabs,
     levels, modes, topics) scrolls sideways instead of wrapping, while
     #topright (settings gear, reset, progress) keeps its natural width
     pinned at the end of that same row rather than claiming a row of its
     own — so the header costs one fixed ~54px band no matter how many chips
     the left group holds. The scrollbar is hidden — iOS shows none anyway,
     and a visible one here would read as a stray divider under the row. */
  #topbarMain { flex-wrap: nowrap; align-items: center; gap: 6px; }
  #topleft {
    flex: 1 1 auto;
    min-width: 0;
    flex-wrap: nowrap;
    overflow-x: auto;
    overflow-y: hidden;
    scrollbar-width: none;
    -ms-overflow-style: none;
    overscroll-behavior-x: contain;
  }
  /* Same taper as the desktop scroll tier, same gating (see the .fadeL/.fadeR
     comment there) — without it, whichever chip is mid-scroll sits flush
     against the settings gear with a hard-edged cut through its pill instead
     of fading into the page. */
  #topleft.fadeR {
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 10px), transparent);
            mask-image: linear-gradient(to right, #000 calc(100% - 10px), transparent);
  }
  #topleft.fadeL {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 10px);
            mask-image: linear-gradient(to right, transparent, #000 10px);
  }
  #topleft.fadeL.fadeR {
    -webkit-mask-image: linear-gradient(to right, transparent, #000 10px, #000 calc(100% - 10px), transparent);
            mask-image: linear-gradient(to right, transparent, #000 10px, #000 calc(100% - 10px), transparent);
  }
  /* No -webkit-overflow-scrolling: touch here — iOS Safari treats a
     position:fixed descendant of a touch-momentum scroll container as
     contained by it instead of escaping to the viewport, which is exactly
     the trick #topleft .langDropPanel below relies on to get past this row's
     own clipping. With the property set, every dropdown opened from inside
     #topleft (language, topics, media section) rendered clipped to this
     ~54px-tall strip on iOS — invisible rather than merely misplaced.
     Modern iOS scrolls natively without it. */
  #topleft::-webkit-scrollbar { display: none; }
  /* Chips keep their natural width; the row scrolls rather than squeezing them. */
  #topleft > * { flex: 0 0 auto; }
  #topleft { gap: 6px 8px; }
  #topright { flex: 0 0 auto; gap: 8px; }
  #progress { white-space: nowrap; }
  /* The word beside the count is the header's least essential text claim on
     a row already scrolling to fit everything else — the number alone still
     answers "how many so far". */
  .progressLabel { display: none; }

  #langDrops { flex-basis: auto; min-width: 0; }
  .langDropBtn { max-width: 100%; }
  .ldFlags { max-width: 104px; }
  /* The target/reading-language name next to its flag is the header's single
     biggest text claim on a row that's already scrolling to fit everything
     else — the flag alone is enough to tell the two dropdowns apart. */
  .ldLearnName { display: none; }

  /* Study / Reference / Media as three glyphs instead of three words — the
     single biggest text claim in the scrolling row after the language name
     above. The button keeps its text as a title tooltip and aria-selected
     state; only the visible label swaps for the icon. */
  /* Study / Media keep their text labels on phones too; the icons stay hidden (user request). */
  .viewBtn { padding: 7px 10px; }

  /* #deck's column layout and scroll come from the ≤1180px tier above; a
     phone just needs tighter edge padding than a tablet-width screen does. */
  #deck { padding: 12px; }
  #card { padding: 26px 18px; border-radius: min(var(--radius), 16px); order: 1; }
  #word { font-size: 1.75rem; margin-bottom: 14px; }
  #translations { gap: 10px 12px; font-size: 1rem; margin-bottom: 18px; }
  #synonyms { margin-bottom: 8px; }

  /* The panels already stack below the card and #deck already scrolls as a
     column (see the ≤1180px tier above) — this just tightens the panel's
     own padding/radius to match the card's phone-sized ones; app.js scrolls
     the panel into view the moment it opens rather than leaving it to be found. */
  .refPanel {
    padding: 22px 18px 24px;
    border-radius: min(var(--radius), 16px);
    box-shadow: var(--card-shadow);
    backdrop-filter: blur(var(--card-blur));
    -webkit-backdrop-filter: blur(var(--card-blur));
  }
  #refPanelFamily, #refPanelForms { border: var(--card-border-w) solid var(--border); }

  /* #topleft is a scroll container, and a scroll container clips any
     absolutely positioned descendant — a panel opened from inside it would be
     cut down to the row's own height. So on a phone they are pinned to the
     viewport instead, hanging off the bottom-left corner of the button that
     opened them (`--panel-top`/`--panel-left`, computed by trackPanelAnchor()
     in app.js, which already keeps the panel on screen if that corner is
     close to the right edge). No ancestor uses transform/filter, so `fixed`
     really does resolve against the viewport here.
     Shrink-wrapped, not stretched edge to edge: a two-word row (a language
     name, a topic) doesn't need the full screen width, and stretching it
     that far turned every dropdown into a much bigger sheet than its own
     content, with most of the width sitting empty. */
  .langDropPanel {
    position: fixed;
    top: var(--panel-top, 56px);
    left: var(--panel-left, 10px);
    right: auto;
    width: max-content;
    min-width: 0;
    max-width: min(280px, calc(100vw - 24px));
    max-height: calc(100dvh - var(--panel-top, 56px) - 16px);
  }

  /* The tray is clipped by #topright's sideways scroll exactly as the panels
     are, so it takes the same escape: fixed, with the gear's bottom-left corner
     as its anchor. Unlike a panel it stays a narrow column — there is no list
     to give room to, only three circles. */
  #settingsTray,
  #accountTray {
    position: fixed;
    top: var(--panel-top, 56px);
    left: var(--panel-left, 10px);
    right: auto;
  }
  /* ...and once the tray itself is fixed, the panels its buttons open go back to
     the same shrink-wrapped treatment above. Restated here because the
     desktop rule that opens them sideways (#settingsTray .langDropPanel,
     earlier in this file) is more specific than the bare .langDropPanel one.
     trackPanelAnchor() already routes a tray row's panel (theme, background,
     font, marks) to hang off the gear button rather than its own row — using
     --panel-left here (instead of a hardcoded 10px) is what actually lets it
     land next to the tray instead of stranded at the opposite edge of the
     screen with a gap of bare page between the two. */
  #settingsTray .langDropPanel {
    top: var(--panel-top, 56px);
    left: var(--panel-left, 10px);
    right: auto;
  }

  /* Bottom padding grows for the home-indicator inset (viewport-fit=cover
     draws the page edge-to-edge, so without this the row would sit right
     under it). */
  #controls { gap: 8px; padding: 14px 10px max(14px, env(safe-area-inset-bottom)) 10px; flex-wrap: wrap; }
  #controls button { padding: 12px 14px; font-size: 0.95rem; }
  #controls kbd { display: none; } /* key hints are meaningless on touch */
  /* "Learn anyway" is the longest label #controls ever shows, at exactly
     the moment (a walked-back "Know" claim) the button row is already
     tightest — the plain "Learn" it falls back to here is one of the
     other buttons' own label (see .btnLabelFull/.btnLabelShort in
     08-case-pair-rows.js), not a new string, so it stays consistent with
     what "Learn" already means everywhere else on the card. */
  .btnLabelFull { display: none; }
  .btnLabelShort { display: inline; }

  .gTopic { padding: 18px 16px 16px; }
}

@media (max-width: 380px) {
  #word { font-size: 1.5rem; }
  #controls button { padding: 11px 10px; font-size: 0.88rem; }
  .ldFlags { max-width: 92px; }
}

/* ---- loading screen ---------------------------------------------------- */
/* Shown until app.js removes `app-loading` from <html>: just the word
   "Loading", centred, on the page backdrop. */
#appSkeleton {
  position: fixed;
  inset: 0;
  z-index: 60;
  background: var(--page-bg);
  display: flex;
  align-items: center;
  justify-content: center;
}
html:not(.app-loading) #appSkeleton { display: none; }
.skLoadingText {
  color: var(--muted);
  font-size: 1.1rem;
  letter-spacing: 0.08em;
}

/* ---- topics filter ----------------------------------------------------- */
#topicFace {
  display: inline-flex;
  align-items: center;
  gap: 3px;
  font-size: 0.95rem;
  line-height: 1;
}
/* The option's leading icon in the topics panel — a stroked SVG now, not an
   emoji, so it inherits the row's text colour instead of keeping its own. */
.ldTopicEmoji {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  font-size: 1.05rem;
  width: 1.4em;
  flex-shrink: 0;
}
/* Topic emojis in the card's top-left corner. */
#cardTopics {
  position: absolute;
  top: 12px;
  left: 14px;
  display: flex;
  gap: 5px;
  flex-wrap: wrap;
  max-width: 62%;
  pointer-events: auto;
}
.cardTopic {
  font-size: 1rem;
  line-height: 1;
  cursor: default;
  opacity: 0.8;
}
.cardTopic:hover { opacity: 1; }

/* ---- media -------------------------------------------------------------
   Browsable content that is neither a card nor a grammar rule. The list is
   deliberately denser than a grammar topic: the point is to see many items at
   once, so an item is a compact block rather than a full-width panel. */
#media {
  flex: 1;
  min-height: 0;
  overflow-y: auto;
  padding: 8px 20px 60px;
  display: flex;
  flex-direction: column;
  align-items: center;
}

#media.hidden { display: none; }

/* Plain top-of-list block, not sticky: it scrolls away with the rest of the
   list rather than pinning itself over it, so there is no overlay, no fade,
   and nothing behind the search box or chips to keep legible against
   whatever is scrolling underneath. */
#mediaHead {
  flex: 0 0 auto;
  width: 100%;
  max-width: 780px;
  padding-bottom: 4px;
}

#mediaSearchWrap {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 8px 0 6px;
}

#mediaSearch {
  flex: 1 1 auto;
  min-width: 0;
  font: inherit;
  font-size: 0.9rem;
  color: var(--text);
  background: var(--card-bg);
  border: none;
  border-radius: 0 !important;
  padding: 7px 14px 7px 34px;
  outline: none;
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%238c887f' stroke-width='2' stroke-linecap='round'%3E%3Ccircle cx='11' cy='11' r='7'/%3E%3Cpath d='M20 20l-3.5-3.5'/%3E%3C/svg%3E");
  background-repeat: no-repeat, repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: 11px center, top left, bottom left, top left, top right;
  background-size: 15px 15px, 40px 8px, 40px 8px, 8px 40px, 8px 40px;
}

#mediaSearch:focus { border-color: var(--accent); }
#mediaSearch::-webkit-search-cancel-button { cursor: pointer; }

#mediaCount {
  flex: 0 0 auto;
  font-size: 0.8rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}

/* The list and, for the songbook, the rail beside it. Ordinary sections keep
   the old single column: the wrapper carries the measure so the body inside it
   is still centred under the search box. */
.mLayout {
  position: relative;
  z-index: 1;
  flex: 0 0 auto;
  width: 100%;
  max-width: 780px;
}

#mediaBody {
  width: 100%;
}

/* ---- the songbook frame ------------------------------------------------ */
/* Reference's layout, borrowed: two panes scrolling independently inside a
   section that does not scroll, so the contents stay put while you read. It is
   the only media section that needs it — 4,990 songs indexed by letter, where
   every other section is one list you can scroll to the end of. */
#mediaIndex { display: none; }

#media.indexed {
  overflow: hidden;
  padding-bottom: 0;
}

/* Learn mode is one card and three controls, not a scrollable list — so it
   sits centred in the pane instead of hugging the top under the (hidden)
   search head. min-height:0 down the flex chain keeps a tall card scrolling
   inside #media, which still overflows normally, rather than clipping. */
#media.learning .mLayout {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
}
#media.learning #mediaBody {
  display: flex;
  flex-direction: column;
  justify-content: center;
  min-height: 0;
}

@media (max-width: 640px) {
  /* Centered content that runs taller than its container only ever exposes
     the *bottom* overrun to `overflow-y: auto` — align-items/justify-content
     :center never falls back to start alignment on overflow, so the top of
     an overlong card is clipped, not merely scrolled past. Same trap #deck's
     own ≤640px tier already routes around. A phone's shorter viewport hits
     this often enough on a long question (many answer options, e.g. the
     civics quiz) that learn mode needs the same fix: hug the top so #media's
     own scroll actually reaches all of it. */
  #media.learning .mLayout { align-items: flex-start; }
  #media.learning #mediaBody { justify-content: flex-start; }
}

/* The head keeps the genre chips and, beside them, whose the words are; the
   search box has moved into the rail. */
#media.indexed #mediaHead {
  display: flex;
  align-items: center;
  flex-wrap: wrap;
  gap: 4px 20px;
  padding-bottom: 8px;
  /* Same override #media.indexed .mLayout needs and why -- #mediaHead's own
     base rule above caps every other section's head at 780px. */
  max-width: none;
}
#media.indexed #mediaScopes { flex: 0 0 auto; }

/* Small, and beside the chips rather than over the songs: it is a fact about the
   section, and the pane to the right is one song, where a standing notice would
   read as a line of it.

   Both classes are named so this outranks `.mNote` further down the file rather
   than merely coming before it — on one class each the later rule won, and the
   14px bottom margin it kept is a margin the chip row does not have, so
   centring the pair put the note half that margin above the chips. */
.mNote.mHeadNote {
  flex: 1 1 320px;
  min-width: 0;
  margin: 0;
  font-size: 0.74rem;
  line-height: 1.45;
}
.mNote.mHeadNote.hidden { display: none; }

#media.indexed .mLayout {
  display: flex;
  align-items: stretch;
  gap: 28px;
  flex: 1 1 auto;
  min-height: 0;
  /* User feedback, 2026-09-30: the reading pane wasted the full width of a
     wide screen -- unlike every other media section, Tales' rail+pane pair
     now uses the whole page instead of being capped to a fixed measure.
     `.mLayout`'s own base rule (above) sets max-width:780px for every
     OTHER media section -- an explicit override here, not just leaving it
     unset, or that base cap would keep quietly winning. */
  width: 100%;
  max-width: none;
}

/* The same 270px as the Reference tree, and for the same reason: the entries are
   titles, and a title that wraps turns a list you scan into a list you read. */
#media.indexed #mediaIndex {
  display: block;
  flex: 0 0 270px;
  overflow-y: auto;
  padding: 16px 14px 40px;
  /* Same soft-card recipe #grammarIndex uses, and for the same reason: this
     rail sits directly over the app's own Background pattern (#bgPattern),
     and plain text there is hard to read (user report, 2026-09-17, once the
     Tales section put this rail — previously only used by the archived
     songbook feature — in front of real users for the first time). */
  background: color-mix(in srgb, var(--card-tint) 92%, transparent);
  border-radius: var(--radius);
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
}

/* Between the phone layout and the full pair, the same trim the grammar tree
   takes: at those widths a wide rail is taken out of the song. */
@media (min-width: 901px) and (max-width: 1150px) {
  #media.indexed #mediaIndex { flex: 0 0 218px; }
}

#media.indexed #mediaBody {
  flex: 1;
  min-width: 0;
  overflow-y: auto;
  padding-bottom: 60px;
}

/* The search box, once it is in the rail: full width with the count under it
   rather than beside it, since 220px will not hold both on one line. */
#mTocSlot #mediaSearchWrap {
  display: block;
  padding: 0 0 12px;
}
#mTocSlot #mediaSearch {
  width: 100%;
  font-size: 0.85rem;
}
#mTocSlot #mediaCount {
  display: block;
  margin: 5px 0 0 3px;
  font-size: 0.72rem;
}

/* The letter, and the fold for the run of titles under it. It keeps the look of
   the Reference tree's level mark exactly — a quiet divider, not an entry — and
   only the chip at the right edge says it can be pressed. Both classes' worth of
   declarations are restated rather than inherited from `.gTocLevel`, because a
   button brings its own font, border and alignment along and every one of them
   has to be put back.

   Sits after `.gTocLevel` in the file, so the ties it has with it go this way. */
.mTocLetter {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 8px;
  width: 100%;
  /* The same right inset as a song row, so the chip stands in the column the
     line counts already run down rather than half a step outside it. */
  padding-right: 10px;
  border: 0;
  border-bottom: 1px solid var(--border);
  background: none;
  font-family: inherit;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  color: var(--accent);
  text-align: left;
  cursor: pointer;
}

/* Small, muted, and the same glyph either way — it turns rather than swaps, so
   the eye tracks one mark through the fold instead of reading a new one. */
.mTocArrow {
  flex: 0 0 auto;
  font-size: 0.64rem;
  line-height: 1;
  color: var(--muted);
  transition: transform 0.14s ease;
}
.mTocLetter:hover .mTocArrow { color: var(--accent); }
.gTocGroup.shut .mTocArrow { transform: rotate(-90deg); }
.gTocGroup.shut .mTocRun { display: none; }

/* A song row: the title, and how long the song runs pushed to the far edge —
   the one thing worth knowing before you open it, and quiet enough that the
   column of titles still reads as a column of titles. */
.mTocSong { justify-content: space-between; gap: 10px; padding-right: 10px; }
.mTocName { min-width: 0; }
/* Only a hit inside the title itself earns this — a match found down in the
   lyric text still surfaces the row but leaves the title plain, so the
   accent stays a promise that the title itself is what matched. */
.mTocHit { color: var(--accent); font-weight: 700; }
.mTocCount {
  flex: 0 0 auto;
  font-size: 0.7rem;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
/* The reading pane: the one song or tale, and nothing else in it. Same
   soft-card recipe #grammarIndex/#mediaIndex/.gTopic use, and for the same
   reason: this sits directly over the app's own Background pattern, and
   plain text there is hard to read (user report, 2026-09-19 — the rail got
   this treatment already, the pane had not). */
.mSheet {
  padding: 20px 22px 22px;
  background: color-mix(in srgb, var(--card-tint) 92%, transparent);
  border-radius: 0;
  backdrop-filter: blur(18px);
  -webkit-backdrop-filter: blur(18px);
  position: relative;
}
/* Same hand-drawn brush-border framing every other standalone surface gets
   (#settingsBtn/.langDropBtn/#mediaSearch/.mLearnLimitInputWrap) — user
   feedback, 2026-09-24: the reading pane's new card background (above)
   still read as unframed next to those. Rectangular, not rounded, like
   every other brush-bordered element (hence border-radius: 0 above). */
.mSheet::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.mSheetTitle {
  margin: 0 0 2px;
  font-size: 1.24rem;
  font-weight: 700;
  line-height: 1.25;
}
.mSheet .mLyBody { padding: 0; }

/* The text itself: plain paragraphs, not verse — no hanging indent, just
   enough line-height and paragraph spacing to read a long tale comfortably. */
.mTaleBody { padding: 4px 0 0; }
.mTaleP {
  margin: 0 0 14px;
  font-size: 0.97rem;
  line-height: 1.6;
}
.mTaleP:last-child { margin-bottom: 0; }

/* Original + translation: which arrangement reads better depends on how
   long the tale actually is, not on the viewport alone — user feedback,
   2026-09-30: "if they take less than 30% screen in original, keep the
   separation horizontal [i.e. stacked]... we wanna use the space in smart
   way". A short tale side by side with its translation leaves both columns
   mostly empty air; a long one benefits from using the now-full-width pane
   (see #media.indexed .mLayout above) to read both at once. Default here is
   the stacked arrangement (a plain flex column, translation running under
   the original); syncTaleColumnLayout() in 11-lyrics-songbook-media.js
   measures the rendered original against the visible pane after each tale
   is drawn and adds `.side` only when it clears the 30% threshold. */
.mTaleCols { display: flex; flex-direction: column; }
.mTaleOrigCol, .mTaleTrCol { min-width: 0; }
.mTaleCols.side {
  flex-direction: row;
  /* stretch, not flex-start: while the translation is still loading
     trCol has almost no content of its own (just the title/tabs), and
     the vertical divider (border-left, below) is only as tall as trCol's
     own box -- stretch makes that box match origCol's full height so the
     divider runs all the way down even then. User feedback, 2026-09-30.
     Once loaded, syncTaleParagraphAlignment() already keeps both columns
     the same height anyway, so this is a no-op visually in that case. */
  align-items: stretch;
  gap: 20px;
}
.mTaleCols.side .mTaleOrigCol,
.mTaleCols.side .mTaleTrCol { flex: 1 1 0; }
/* Spacing between the original and the translation lives on the column,
   not on .mTaleTrTabs/.mTaleTr themselves, so side mode (top-aligned next
   to the original, no "continuing downward" relationship) can zero it out
   in one place rather than overriding every child. */
.mTaleTrCol { padding-top: 24px; color: var(--muted); }
/* The translated title: same weight/size as the original (.mSheetTitle,
   reused directly), inheriting .mTaleTrCol's muted colour. User feedback,
   2026-09-30: it needs to sit BEFORE the language tabs (stacked mode) and
   at the same row as the original title (side mode) -- both already fall
   out of being trCol's first child, same as the original title is
   origCol's first child. */
/* ...and then, same-day follow-up feedback: the tabs/label belong on that
   SAME row, right after the title, not a row of their own underneath --
   .mTaleTrTitleRow (built in renderTaleSheet only once a title exists)
   holds both; .mTaleTrTitle drops its own bottom margin there since the
   row itself carries it instead, sized to sit visually as one line even
   though the title text and the flag pills differ in height. */
.mTaleTrTitle { margin: 0 0 2px; }
.mTaleTrTitleRow {
  display: flex;
  align-items: baseline;
  flex-wrap: nowrap;
  gap: 10px;
  /* Same 2px as .mSheetTitle/.mTaleTrTitle's own default -- the row now
     usually runs straight into body text exactly like the original title
     does (tabs/label moved onto this row instead of a block beneath it,
     see renderTaleSheet()), so it needs the same tight gap, not the wider
     one the lone title briefly had while tabs still sat underneath it.
     User feedback, 2026-09-30: the two sides read as mismatched without
     this. */
  margin: 0 0 2px;
}
.mTaleTrTitleRow .mTaleTrTitle,
.mTaleTrTitleRow .mTaleTrTabs,
.mTaleTrTitleRow .mTaleTrLabel { margin: 0; }
/* The title is the one flex item allowed to shrink (and, inside itself,
   wrap its own text across 2+ lines) -- without min-width:0 a flex item's
   default min-width is its own un-wrapped content width, which is wider
   than many columns can offer for a long title, so the browser wrapped
   the TABS onto their own new flex line instead of letting the title text
   wrap internally and keeping the tabs beside its first line. User
   feedback, 2026-09-30, screenshotted on exactly this: a long title
   pushed its flags down to a line of their own. Tabs/label stay fixed
   size so they never shrink into illegibly cramped flag pills. */
.mTaleTrTitleRow .mTaleTrTitle { flex: 1 1 auto; min-width: 0; }
.mTaleTrTitleRow .mTaleTrTabs,
.mTaleTrTitleRow .mTaleTrLabel { flex: 0 0 auto; }
/* The vertical delimiter -- same divider recipe .wlRefBodies already uses
   for its own side-by-side pair (border + matching padding, one becoming
   the other's mirror rather than a doubled gap). Stacked mode carries no
   rule/border at all: real space is what separates the two blocks there. */
.mTaleCols.side .mTaleTrCol {
  padding-top: 0;
  border-left: 1px solid var(--border);
  padding-left: 20px;
}
.mTaleTrTabs { margin: 0 0 14px; gap: 4px; }
/* Smaller and tighter than a standalone tab row's usual .levelBtn chrome
   -- user feedback, 2026-09-30: sharing a line with the title reads
   better with compact flag pills than the fuller pill size .levelBtn
   uses everywhere else (Cheat Sheet's level chips, Schreiben's example
   tabs), so this is scoped to .mTaleTrTabs alone. */
.mTaleTrTabs .levelBtn { padding: 2px 4px; }
.mTaleTrTabs .levelBtnLabel { font-size: 0.9rem; line-height: 1; }
.mTaleTrLabel {
  font-size: 0.78rem;
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.03em;
  color: var(--muted);
  opacity: 0.7;
  margin: 0 0 10px;
}
/* "Loading…" while the pack is still in flight: stays vertically centred
   in whatever part of the pane is actually on screen as you scroll the
   (much longer) original beside it, rather than sitting pinned at the
   top of a mostly-empty trCol -- user feedback, 2026-09-30. `height` is
   set by syncTaleColumnLayout() to the pane's own visible height (only in
   side mode; see that function's comment), giving `sticky` a box exactly
   one "screenful" tall to centre within as it tracks the scroll. */
.mTaleTrPending {
  position: sticky;
  top: 0;
  margin: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  text-align: center;
}

/* Narrow: the songbook folds the way Reference does — one scroller, the tree
   behind its own label — with one difference. Here it is the rail, not the head,
   that stays pinned, because the rail is where the search box now lives and on a
   list of five thousand a search box that scrolls away is worse than a row of
   genre chips that does. Opened, the letter list is capped rather than allowed
   to push the songs off the screen.

   This block sits after the frame rules above rather than in the collapse block
   further up the file: same specificity, so whichever comes last wins, and up
   there the wide layout was quietly winning on a phone. */
@media (max-width: 900px) {
  #media.indexed { overflow-y: auto; padding-bottom: 60px; }
  #media.indexed #mediaHead { max-width: 780px; }
  /* `contents`, not `block`: the rail has to be a direct child of the scrolling
     box for `position: sticky` to pin it — nested one level down inside a
     wrapper it scrolled away with the songs, which is the whole thing this mode
     is trying to prevent. Dissolving the wrapper puts both panes back in the
     section's own flex column, where the head has always stuck. */
  #media.indexed .mLayout { display: contents; }
  #media.indexed #mediaBody {
    width: 100%;
    max-width: 780px;
    overflow: visible;
    padding-bottom: 0;
  }
  /* syncTaleColumnLayout() already skips adding `.side` under this width,
     but this is a plain safety net against a stale class surviving a
     resize (e.g. rotating a tablet) without a fresh render in between. */
  .mTaleCols.side {
    flex-direction: column;
    gap: 0;
  }
  .mTaleCols.side .mTaleTrCol {
    padding-top: 24px;
    border-left: none;
    padding-left: 0;
  }
  /* Same plain, non-sticky recipe #grammarIndex's own mobile override
     uses (see its comment) -- the earlier version of this rule kept the
     rail position:sticky with a blur/fade mask behind it, pinned at the
     top of the scroll as you read down a tale, but with scrolled text
     bleeding through above and below it the result read as a floating
     overlay rather than a clean sticky bar. User feedback, 2026-09-30:
     "should look like in the Grammar and get scrolled up ... instead of
     staying overlay" -- scrolls away with the rest of the page now, same
     as Grammar's, instead of staying pinned. */
  #media.indexed #mediaIndex {
    flex: 0 0 auto;
    width: 100%;
    max-width: 780px;
    overflow: visible;
    padding: 6px 14px 8px;
    margin-bottom: 4px;
    border-bottom: 1px solid var(--border);
  }
  /* Full width here, so the count sits beside the field the way it did in the
     head rather than under a 100%-wide input. */
  #mTocSlot #mediaSearchWrap {
    display: flex;
    align-items: center;
    gap: 10px;
    padding: 6px 0 0;
  }
  #mTocSlot #mediaSearch { width: auto; }
  #mTocSlot #mediaCount { display: inline; margin: 0; }
}

.mItem {
  position: relative;
  background: var(--card-tint);
  border: none;
  border-radius: 0 !important;
  box-shadow: var(--card-shadow);
  backdrop-filter: blur(var(--card-blur));
  -webkit-backdrop-filter: blur(var(--card-blur));
  padding: 14px 16px;
  margin-bottom: 10px;
  /* A long stack of blurred cards can't all stay composited at once during
     fast momentum scroll -- the browser drops a card's blur/shadow layer and
     redraws it a frame late, which reads as a flash of plain dark box at the
     scroll edge. Promoting each card to its own GPU layer up front (rather
     than only once the browser notices it needs one) keeps that redraw off
     the main thread, so it doesn't visibly lag behind the scroll. */
  will-change: transform;
  contain: layout paint;
}
.mItem::before {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}


/* .mBadges floats right so the title text wraps AROUND it on the line(s)
   beside it, then reclaims the FULL row width on any line below the
   float's own height — a flex row (the previous approach) reserved the
   same shrunk column for every wrapped line, even ones already below where
   the badges had ended, leaving blank space on the right for a title that
   only needed 2 lines and no badges-height column by its 2nd/3rd (user
   feedback, screenshot). flow-root (not the plain default block) is what
   actually CONTAINS the float within .mHead's own box, so mediaGloss/mMean
   sibling content right after .mHead doesn't wrap around the float's tail
   too — same reasoning the .mLidHead/.mPersoHead number badges below reuse
   for their own float. */
.mHead { display: flow-root; }

.mTitle {
  font-size: 1.02rem;
  font-weight: 700;
  margin: 0;
  line-height: 1.35;
  /* A long, unbroken single token (a German compound noun, an abbreviation's
     expansion, ...) has no ordinary break point of its own -- without this,
     it overflows .mTitle's box outright and renders on top of the floated
     .mBadges instead of wrapping under it (user feedback, screenshot: level
     badge overlapping the title text). */
  overflow-wrap: anywhere;
}

/* ---- media learning mode ------------------------------------------------
   One card, three controls — the Study screen's own shape, reused rather
   than invented fresh so switching between the two costs nothing to learn.
   #mediaHead (search, scope chips, note) is hidden by app.js while this is
   on; #mediaLearnBtn stays put in the bar above it, the one way back out. */
.mLearnHead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  max-width: 640px;
  margin: 0 auto 14px;
  padding: 4px 2px;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
}
.mLearnCount { font-size: 0.95rem; }
.mLearnLimit {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 0.82rem;
}
/* ::before renders nothing on a replaced <input>, same reason #sessionInput
   wraps -- the brush border lives on this wrapping span instead. */
.mLearnLimitInputWrap {
  display: inline-flex;
  position: relative;
}
.mLearnLimitInputWrap::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.mLearnLimit input {
  width: 3.4em;
  padding: 5px 6px;
  border: none;
  border-radius: 0;
  background: var(--card-bg);
  color: var(--text);
  font: inherit;
  text-align: center;
}
/* The spinners crowd a two/three-digit box and invite click-by-click
   changes, each of which reshuffles the deck — same reasoning as the
   Study session-size box. */
.mLearnLimit input::-webkit-outer-spin-button,
.mLearnLimit input::-webkit-inner-spin-button { -webkit-appearance: none; margin: 0; }
.mLearnLimit input { -moz-appearance: textfield; appearance: textfield; }

/* #mediaBody's own row (.mLayout) centres it with `justify-content: center`
   rather than stretching it, so it is content-sized — a `max-width` alone
   still lets a short front ("Anl.") shrink the whole card down to its own
   text, the exact inconsistency Study's #card avoids with a real `width`.
   `min(640px, 92vw)` is a width, not a cap, so the card holds this size
   regardless of what is actually inside it. */
.mLearnCard { width: min(640px, 92vw); margin: 0 auto; }
/* Revealed interview (perso) card: 50% wider to fit the model answers. */
.mLearnCard.wide { width: min(960px, 92vw); }
.mLearnGradeRow.wide { max-width: 960px; }
/* Unrevealed: a flashcard front, not a list row — the same big centred
   headline the Study card leads with, so a tap-to-reveal reads as the
   familiar gesture rather than a new one. Revealed, this wrapper carries no
   styling of its own; the reused item renderer's own .mItem card is the
   whole content and already looks right on its own. */
.mLearnFront {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  min-height: 160px;
  padding: 28px 22px;
  border: var(--card-border-w) solid var(--border);
  border-radius: var(--radius);
  background: var(--card-tint);
  backdrop-filter: blur(var(--card-blur));
  -webkit-backdrop-filter: blur(var(--card-blur));
  box-shadow: var(--card-shadow);
  font-family: var(--font-head);
  font-size: 1.5rem;
  font-weight: var(--head-weight);
  text-align: center;
  cursor: pointer;
}
/* The question text (+ its optional speak button) as their own row inside
   the now-column .mLearnFront, so adding .mLearnHintWrap below them (part of
   the same column) doesn't disturb their original side-by-side layout. */
.mLearnFrontQ {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 10px;
}

/* A lighter reveal than Show me, for items that carry a listening-for note
   (the `perso` interview schema's item.listen — see .mPersoListen's own
   callout in the browse view). Rendered inside the flashcard itself, right
   under the question — collapsed by default, and only ever toggles this one
   note rather than flipping the whole card the way Show me does. The accent
   rule on the revealed text echoes .mPersoListen so the two views read as
   the same note. */
.mLearnHintWrap { align-self: stretch; }
.mLearnHintWrap > .mLearnHintBtn { margin: 0 auto; }
.mLearnHintBtn {
  display: block;
  background: none;
  border: none;
  padding: 0;
  font-family: var(--font-body);
  font-size: 1rem;
  font-weight: 400;
  color: var(--muted);
  cursor: pointer;
}
.mLearnHintBtn:hover { color: var(--text); }
.mLearnHintBody {
  max-width: 560px;
  margin: 10px auto 0;
  padding: 2px 0 2px 11px;
  border-left: 2px solid var(--accent);
  text-align: left;
}
.mLearnHintText {
  margin: 0;
  font-size: 1rem;
  line-height: 1.45;
  font-family: var(--font-body);
  font-weight: 400;
  color: var(--text);
}
.mLearnHintGloss { font-family: var(--font-body); font-weight: 400; margin: 3px 0 0; font-size: 0.92rem; color: var(--muted); line-height: 1.4; }
.mLearnHintGloss.alt { margin-top: 1px; font-size: 0.9rem; }

/* Give-up affordance for a free-text front, sitting under the flashcard
   rather than inside it — pressing it is a deliberate second action, not
   something a stray tap on the card should trigger. */
.mLearnShowMe {
  display: block;
  margin: 12px auto 0;
  color: var(--muted);
}
/* The idiom/abbr free-text card's real know/don't-know pair (2026-09-16) —
   same colour convention as #btnKnown/#btnUnknown in Study: plain
   muted-on-neutral by default, filled Primary/Secondary pills under a
   Design (see the [data-design] #btnKnown/#btnUnknown comment above for
   why Secondary, not the fixed red, is the intended "don't know" colour).
   Needs an explicit width, not just max-width, to actually reach it --
   with only max-width, the auto left/right margins used to center the row
   consume the free space instead of the row growing into it, so the pair
   rendered much narrower than .mLearnCard above it despite sharing the same
   640px cap (user feedback, 2026-09-25: "should be wider"). */
.mLearnGradeRow { display: flex; gap: 12px; justify-content: center; width: 100%; max-width: 640px; margin: 14px auto 24px; }
.mLearnGradeBtn { flex: 1 1 0; justify-content: center; }
.mLearnGradeNo { color: var(--bad); }
.mLearnGradeYes { color: var(--accent); }
/* Same as Study's Know / Don't know: no fill, plain text with the sketched
   underline (.btnTxt) in the button's own colour. */
[data-design] .mLearnGradeYes,
[data-design] .mLearnGradeNo { background: transparent; color: var(--text); border-color: transparent; }
.mLearnGradeNo .btnTxt { --ul: var(--bad); }
.mLearnGradeYes .btnTxt { --ul: var(--accent); }

/* Tap-to-continue hint, once a card no longer has anything left to ask —
   text, not a button: the advance itself lives on the card's own click
   handler, so this only needs to say that pressing it does something. */
.mLearnNext {
  max-width: 640px;
  margin: 14px auto 24px;
  text-align: center;
  color: var(--muted);
  font-size: 0.88rem;
  cursor: pointer;
}

.mBadges { display: flex; align-items: center; gap: 6px; float: right; margin: 0.15em 0 6px 12px; }

.mBadge {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  text-transform: uppercase;
  color: var(--muted);
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 2px 8px;
  white-space: nowrap;
}

/* What it means: the line that carries the item, so it gets normal text
   weight and colour. Every following language is muted. */
.mMean { font-size: 0.93rem; line-height: 1.45; margin: 6px 0 0; }
.mMean.alt { color: var(--muted); font-size: 0.88rem; margin-top: 2px; }

/* The word-for-word reading rides on the same line as the meaning, in muted
   italic, because the joke in an idiom is the distance between the two. It is
   never the definition, so it never gets the reading colour — not even on the
   first line, where the meaning does. */
.mLitT { color: var(--muted); font-style: italic; }
.mMeanT + .mLitT::before {
  content: "·";
  margin: 0 0.5em;
  font-style: normal;
  opacity: 0.65;
}

/* A target-language word or phrase, quoted mid-sentence inside a note that is
   otherwise written in the study-from language — schließlich, sitting inside
   an English sentence about evtl. A plain font-style: italic on the running
   text reads as emphasis, the same shift a learner's eye gives any bolded
   word; the point here is the opposite one, that this fragment is not in the
   sentence's own language at all. A dedicated italic face marks that
   difference on sight, the way a dictionary sets a foreign headword in a
   different type than its own gloss. Lora only covers Latin script: a
   Cyrillic/CJK/Arabic/Thai target quoted this way still gets the isolation
   of its own <em>, it just falls through to the surrounding font, since
   neither italics nor Lora mean anything in those scripts. */
.mQuoteTarget {
  font-family: "Lora", var(--font-body);
  font-style: italic;
  font-weight: 500;
  font-size: 1em;
  letter-spacing: 0.005em;
}

/* A quoted word inside Meaning/Etymology prose that happens to name a real
   corpus headword (app.js: appendRich's `ks` branch, resolveQuoteWord) — the
   compound-word cross-reference from work-plan item 8: "Fachfrau compounds
   Fach ... with Frau ..." with Fach and Frau each opening the same popover a
   word-family row's book icon does. Same underline-not-colour cue as a
   linked synonym chip (.syn.synLinked), for the same reason: italic already
   carries one visual meaning (foreign-language quote) here, so "also a
   link" has to layer on top rather than replace it. */
.mQuoteTarget.mQuoteLinked {
  cursor: pointer;
  text-decoration: underline dotted color-mix(in srgb, var(--accent) 55%, transparent);
  text-underline-offset: 3px;
}
.mQuoteTarget.mQuoteLinked:hover, .mQuoteTarget.mQuoteLinked:focus-visible {
  color: var(--accent);
}

.mEx {
  font-size: 0.92rem;
  line-height: 1.45;
  margin: 9px 0 0;
  padding-left: 11px;
  border-left: 2px solid var(--border);
}

.mExGloss {
  font-size: 0.86rem;
  color: var(--muted);
  line-height: 1.4;
  margin: 3px 0 0;
  padding-left: 11px;
  border-left: 2px solid var(--border);
}

.mAnBox { display: flex; gap: 8px; margin-top: 9px; align-items: baseline; }
.mAnList { flex: 1; min-width: 0; }
.mAnLabel { color: var(--muted); margin: 0; font-size: 0.95rem; flex-shrink: 0; }
.mAn { font-size: 0.88rem; margin: 0; }
.mAn.alt { font-size: 0.85rem; color: var(--muted); margin-top: 1px; }

.mMeta { font-size: 0.83rem; color: var(--muted); margin: 4px 0 0; }

/* An abbreviation card. The short form and its expansion share the title
   line — "z. B. · zum Beispiel" — the same primary-then-muted pairing the
   idiom card uses for a meaning and its literal reading on one line, rather
   than a paragraph of its own: two short strings stacked as separate
   paragraphs read as duplicates of each other, not as one thing and its
   gloss. The short form keeps the bold weight and tabular numerals a code
   deserves — "z. B." and "u. a." must not be kerned into something that
   reads as a word; the expansion is muted and lighter, since it is the
   secondary half of the pair. The pronunciation hint below is a spelling of
   a sound, muted and italic so it is never mistaken for a second
   expansion. */
.mAbbrShortT { font-variant-numeric: tabular-nums; letter-spacing: 0.01em; }

.mAbbrFullT {
  font-weight: 400;
  color: var(--muted);
  font-size: 0.86em;
}
.mAbbrShortT + .mAbbrFullT::before {
  content: "·";
  margin: 0 0.5em;
  opacity: 0.65;
}

.mAbbrSayT {
  font-size: 0.86em;
  font-weight: 400;
  color: var(--muted);
  font-style: italic;
}
.mAbbrSayT::before {
  content: "·";
  margin: 0 0.5em;
  font-style: normal;
  opacity: 0.65;
}
.mAbbrSayLabel { font-style: normal; opacity: 0.8; }

.mAbbrNoteWrap {
  display: flex;
  align-items: center;
  gap: 8px;
  margin: 6px 0 0;
}
.mAbbrNoteIcon {
  flex-shrink: 0;
  color: var(--accent);
  opacity: 0.75;
}
.mAbbrNoteBody { flex: 1; min-width: 0; }

.mAbbrNote {
  font-size: 0.85rem;
  line-height: 1.4;
  margin: 0;
  color: var(--muted);
}
.mAbbrNote.alt { font-size: 0.83rem; margin-top: 1px; }

/* The dialogue title: German first, then each active study-from language's
   translation inline after a "·" — same rule/markup as .mSchrTitleDe/
   .mSchrTitleTr (Goethe-Schreiben), reused here under dialogue-specific
   class names since each media kind keeps its own namespaced classes. */
.mDlgTitleTr {
  font-weight: 400;
  color: var(--muted);
  font-size: 0.86em;
}
.mDlgTitleDe + .mDlgTitleTr::before,
.mDlgTitleTr + .mDlgTitleTr::before {
  content: "·";
  margin: 0 0.5em;
  opacity: 0.65;
}

/* Dialogue turns: speaker in a narrow left column so the lines align and the
   exchange reads as an exchange. */
.mTurn { display: flex; gap: 10px; margin-top: 8px; }
.mWho {
  flex: 0 0 auto;
  min-width: 74px;
  font-size: 0.82rem;
  font-weight: 700;
  color: var(--muted);
  padding-top: 1px;
}
.mLine { flex: 1; min-width: 0; }
.mDe { margin: 0; font-size: 0.94rem; line-height: 1.45; }
/* The translation for a one-sentence row (a dialogue turn, a Schreiben
   task/bullet) — appended inline right after the German sentence
   (appendInlineGloss) rather than stacked on its own line below the way
   mediaGloss's multi-line shape works. Colour-only: it inherits whatever
   font-size its German sentence is set in, so it reads as the same line. */
.mInlineGloss { color: var(--muted); }

/* ---- The songbook ----------------------------------------------------
   There are no cards here and nothing to expand: the five thousand titles are
   rows in the contents rail, and this is the pane the chosen one is read in. */
.mLyBody { padding: 0 0 14px; }

/* A stanza is the unit that matters: Spanish in the left column, the
   translation level with it in the right, and a real gap to the next stanza
   so the shape of the song survives — the estribillo is findable by eye. The
   columns are equal because a translation runs about as long as its line, and
   an uneven split makes the two stop looking like the same song. */
.mLyStanza {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 4px 28px;
  margin-top: 18px;
}
.mLyStanza:first-child { margin-top: 4px; }

.mLySide { min-width: 0; }
.mLyLine {
  margin: 0;
  font-size: 0.95rem;
  line-height: 1.55;
  /* A long line wraps rather than being cut, and the wrapped part is indented
     so it still reads as one line of verse and not as two. */
  padding-left: 1.1em;
  text-indent: -1.1em;
}
.mLyTr .mLyLine { color: var(--muted); }
.mLyPending { margin: 14px 0 0; color: var(--muted); }

/* Below the split the two columns would be four or five words wide, which is
   narrower than a tango line. Stack them instead, translation under stanza,
   and lean on the stanza gap to keep the pairs apart. */
@media (max-width: 640px) {
  .mLyStanza { grid-template-columns: 1fr; gap: 0; margin-top: 20px; }
  .mLyTr { margin-top: 6px; }
}

.mEmpty { text-align: center; color: var(--muted); margin-top: 40px; }

/* A finished Learn session (this session's cap reached, or the whole bank
   now learned) leaves on its own rather than waiting on a manual Quit/
   Start-new-session choice — see renderMediaLearn's `if (!st.deck.length)`
   branch in 11-lyrics-songbook-media.js. This is what plays while it does:
   a brief blur-and-fade rather than an abrupt cut back to the section's
   browsing list. Toggled by adding/removing the class, not an @keyframes,
   so the JS side can listen for `transitionend` and know exactly when it's
   safe to swap the view. */
#mediaBody.mLearnEnding {
  filter: blur(10px);
  opacity: 0;
  pointer-events: none;
  transition: filter 0.4s ease, opacity 0.4s ease;
}

/* ---- Wordlist ------------------------------------------------------------
   Every card you've studied — the same card shape as the idiom/LiD cards
   elsewhere in Media (word + level badge on top, meaning below, then the
   ladder), not a dense single-line strip that was the one thing in Media
   that didn't look like the rest of it. */
.wlToolbar {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin-bottom: 12px;
}

/* The direction toggle, then the four sort types — all one row, separated
   by bullets, unveiling inline rather than pushing a panel in underneath. */
.wlSortBar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  /* 12px, not 8px -- each button's own ::before brush border bleeds 3px
     outward per side, below; anything tighter lets neighbours' bled edges
     touch (same bug .mScopes hit, see its own comment). */
  gap: 12px;
}
.wlSortToggle,
.wlSortDirBtn,
.wlSortTypeBtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  position: relative;
  border: none;
  border-radius: 0 !important;
  background: var(--card-bg);
  color: var(--muted);
  font-size: 1rem;
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.wlSortToggle::before,
.wlSortDirBtn::before,
.wlSortTypeBtn::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.wlSortToggle:hover,
.wlSortDirBtn:hover,
.wlSortTypeBtn:hover { color: var(--text); }
.wlSortToggle.open { color: var(--accent); }

.wlSortTypes { display: flex; gap: 12px; }
.wlSortTypeBtn { font-size: 0.78rem; font-weight: 700; }
/* Underlined, not filled, when selected -- same --pencil-underline swap as
   .viewBtn.active (user feedback, 2026-09-19). Text stays --text -- only
   the underline carries --accent (user feedback, 2026-09-25, same
   #levelPanel .levelBtn.active precedent as #modePanel/.viewBtn above). */
.wlSortTypeBtn.on { background: var(--card-bg); color: var(--text); }
.wlSortTypeBtn.on::after {
  content: "";
  position: absolute;
  left: 8px;
  right: 8px;
  bottom: 4px;
  height: 5px;
  background-color: var(--accent);
  pointer-events: none;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 28px 6px;
          mask-size: 28px 6px;
  -webkit-mask-position: center;
          mask-position: center;
}

.wlDot { color: var(--muted); }

/* Level (cumulative CEFR cap, same rule .mLevel already uses in the idiom
   sections) and topic — appended into .wlSortBar itself, right after the
   sort controls, so the whole toolbar reads as one row. */
.wlLevelBar { display: flex; flex-wrap: wrap; gap: 12px; }

.wlTopicWrap { position: relative; }
.wlTopicBtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 2px;
  min-width: 34px;
  height: 34px;
  padding: 0 6px;
  position: relative;
  border: none;
  border-radius: 0 !important;
  background: var(--card-bg);
  color: var(--muted);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease;
}
.wlTopicBtn::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.wlTopicBtn:hover { color: var(--text); }
.wlTopicBtn.on { color: var(--accent); }
.wlTopicBtn svg { flex-shrink: 0; }

/* Same circular handle as the Wordlist topic filter — a toggle beside a
   dropdown, not a row of its own. Solid once learning mode is on, so the
   section reads as "in a different mode" at a glance, not just a pressed
   button. */
#mediaLearnBtn,
#mediaBookBtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  /* See .modeBtn's own gap:0 comment: the global button{} rule's gap:8px
     otherwise leaks in now that .mediaBtnLabel is a second flex child,
     reserving a phantom gap that shoves the icon off-centre even while
     the label sits collapsed to max-width:0. */
  gap: 0;
  min-width: 34px;
  height: 34px;
  flex: 0 0 auto;
  padding: 0;
  position: relative;
  border: none;
  border-radius: 0 !important;
  background: var(--card-bg);
  color: var(--muted);
  cursor: pointer;
  transition: background 0.15s ease, color 0.15s ease, padding 0.2s ease;
}
#mediaLearnBtn::before,
#mediaBookBtn::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
#mediaLearnBtn:hover, #mediaLearnBtn:focus-visible { color: var(--text); padding: 0 12px 0 9px; }
#mediaLearnBtn:focus { outline: none; }
#mediaLearnBtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
#mediaLearnBtn svg { width: 17px; height: 17px; flex-shrink: 0; }
#mediaLearnBtn[aria-pressed="true"] {
  color: #fff;
  background: var(--accent);
}
#mediaLearnBtn.hidden { display: none; }

#mediaBookBtn:hover, #mediaBookBtn:focus-visible { color: var(--text); padding: 0 12px 0 9px; }
#mediaBookBtn:focus { outline: none; }
#mediaBookBtn:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
#mediaBookBtn svg { width: 17px; height: 17px; flex-shrink: 0; }
#mediaBookBtn[aria-pressed="true"] {
  color: #fff;
  background: var(--accent);
}
#mediaBookBtn.hidden { display: none; }

/* The hover-revealed name beside the icon — see .modeBtnLabel for the same
   trick on the mode-switch row. Shared by both circular buttons since they
   never show at once. */
.mediaBtnLabel {
  max-width: 0;
  overflow: hidden;
  opacity: 0;
  white-space: nowrap;
  font-size: 0.82rem;
  font-weight: 600;
  margin-left: 0;
  /* Same slowed-down unfurl as .modeBtnLabel (user feedback) -- kept in
     sync so every icon-only hover-reveal button in the app stretches at
     the same deliberate pace. */
  transition: max-width 1.1s ease, opacity 0.8s ease, margin-left 1.1s ease;
}
#mediaLearnBtn:hover .mediaBtnLabel, #mediaLearnBtn:focus-visible .mediaBtnLabel,
#mediaBookBtn:hover .mediaBtnLabel, #mediaBookBtn:focus-visible .mediaBtnLabel {
  max-width: 160px;
  opacity: 1;
  margin-left: 6px;
}

/* Learn and the book view are two different ways of reading the same
   section — never both at once, so whichever one isn't active gets
   disabled rather than silently no-op'ing when the other is open. */
#mediaLearnBtn:disabled,
#mediaBookBtn:disabled {
  opacity: 0.4;
  cursor: not-allowed;
}
#mediaLearnBtn:disabled:hover,
#mediaBookBtn:disabled:hover { color: var(--muted); padding: 0; }
#mediaLearnBtn:disabled:hover .mediaBtnLabel,
#mediaBookBtn:disabled:hover .mediaBtnLabel { max-width: 0; opacity: 0; margin-left: 0; }

.wlRow { transition: border-color 0.15s ease, background 0.15s ease; }
.wlRow:hover, .wlRow.open {
  border-color: color-mix(in srgb, var(--accent) 45%, var(--border));
}
/* The lazy-render tail marker for renderWordlistRowsChunked() — invisible,
   just needs real layout height for its IntersectionObserver to fire. */
.wlSentinel { height: 1px; }

.wlHeadBtn {
  display: block;
  width: 100%;
  padding: 0;
  border: none;
  background: none;
  color: inherit;
  font: inherit;
  text-align: left;
  cursor: pointer;
}

/* One paragraph: word, then a bullet, then its translations, wrapping
   together as plain text — not word and translation as two independent
   flex columns each wrapping inside its own box, which is what a long
   word (e.g. "der Verwandtenbesuch") used to turn into: a cramped,
   rigidly two-column look instead of just flowing onto a second line like
   any other long line of text. .wlTopRight floats right instead, so it
   stays pinned at the row's top-right corner without needing flex at all
   — the text simply wraps around it (it has to come FIRST in DOM order
   for that, see wordlistRow()). */
.wlTopRow { display: block; }
.wlTopRight {
  display: flex;
  align-items: center;
  column-gap: 8px;
  float: right;
  margin-left: 8px;
}

.wlBadge { flex-shrink: 0; }
.wlDot { color: var(--muted); }
.wlTr {
  color: var(--muted);
  font-size: 0.93rem;
  line-height: 1.45;
}

/* Everything below the head row, revealed on expand — the study card's own
   translations/forms/examples/stats, then the reference toggles. */
.wlExpand {
  margin-top: 10px;
  padding-top: 10px;
}
.wlExpand[hidden] { display: none; }
.wlReading { color: var(--muted); font-size: 0.85rem; margin-bottom: 6px; }
.wlSyn {
  display: flex;
  flex-wrap: wrap;
  gap: 4px;
  align-items: baseline;
  margin-bottom: 10px;
  font-size: 0.82rem;
}
.wlSyn .synLabel { font-size: inherit; }
.wlSyn .syn { font-size: inherit; color: var(--muted); }
.wlSyn .synDot { font-size: inherit; }
/* Smaller pips than the study card's own — a whole column of these next to
   running text needs to read as a glance, not as a second thing to parse. */
.wlLadder { display: flex; align-items: center; gap: 8px; }
.wlLadder .csBar { gap: 3px; }
.wlLadder .csSeg { width: 12px; height: 4px; }
.wlLadder .csSeg.csLate { width: 7px; }
.wlLadder .csMeta {
  font-size: 0.74rem;
  white-space: nowrap;
}
/* The 💡/📖 toggles, same look as the study card's own #refBtns, opening an
   inline section below instead of a side panel. */
.wlRefRow {
  display: flex;
  gap: 8px;
  margin-top: 12px;
  padding-top: 10px;
  border-top: 1px solid var(--border);
}
.wlRefBtn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: 34px;
  height: 34px;
  padding: 0;
  border: 0;
  border-radius: 50%;
  background: none;
  color: var(--muted);
  opacity: 0.5;
  font-size: 1.15rem;
  cursor: pointer;
  transition: opacity 0.15s ease, color 0.15s ease, background 0.15s ease;
}
.wlRefBtn:hover { opacity: 1; color: var(--text); }
.wlRefBtn.active {
  opacity: 1;
  color: var(--accent);
  background: color-mix(in srgb, var(--accent) 12%, transparent);
}
/* Bulb and book side by side once both are open — each hidden body
   collapses out of the flex row entirely (display: none, not just empty),
   so one open + one closed still reads as a single full-width column. */
.wlRefBodies {
  display: flex;
  gap: 20px;
  margin-top: 10px;
}
.wlRefBody {
  flex: 1 1 0;
  min-width: 0;
  font-size: 0.86rem;
  color: var(--muted);
}
.wlRefBody[hidden] { display: none; }
.wlRefBody .refSec { margin-top: 14px; }
.wlRefBody .refSec:first-child { margin-top: 0; }
/* The divider belongs between the two panels, not on the right one
   unconditionally — the sibling selector only matches when BOTH are
   visible (an immediately-preceding, also-not-hidden left body), so a
   book-only or bulb-only row stays a single plain column. */
.wlRefBody-left:not([hidden]) + .wlRefBody-right:not([hidden]) {
  border-left: 1px solid var(--border);
  padding-left: 20px;
}
@media (max-width: 700px) {
  /* Not enough width for two real columns at phone/narrow-panel size — same
     "drop below instead of squeezing" idea as Study's own two side cards,
     just at a tighter breakpoint since a Wordlist row is already narrower
     than the full page Study's cards compare against. */
  .wlRefBodies { flex-direction: column; gap: 0; }
  .wlRefBody-left:not([hidden]) + .wlRefBody-right:not([hidden]) {
    border-left: 0;
    padding-left: 0;
    border-top: 1px solid var(--border);
    padding-top: 14px;
    margin-top: 14px;
  }
}

/* ---- Cheat sheet ("шпаргалка") --------------------------------------------
   A dense, scannable word list — as many newspaper-style columns as the
   panel fits, each word paired with its gloss, muted and small since the
   point is a glance-able whole screen, not a readable card. CSS multi-column
   (not Grid) is the right tool here: the browser works out the column count
   from the available width on its own, and column-rule gives the vertical
   dividers for free — the same thing Grid's column-gap/border tricks exist
   to approximate. */
/* Width is set inline per render (renderCheatsheetList) to exactly fit
   however many 200px columns the row count actually calls for, then
   centred with this auto margin — a short, topic-filtered list gets a
   compact, centred block of columns instead of stretching a handful of
   columns edge to edge across a full-width panel. A large list's width
   still comes out at (near enough) the full available width on its own,
   so it packs as many columns as fit exactly like before. */
.cshList {
  column-width: 200px;
  column-gap: 22px;
  column-rule: 1px solid var(--border);
  max-width: 100%;
  margin: 0 auto;
}
.cshRow {
  break-inside: avoid;
  padding: 3px 0;
  font-size: 0.86rem;
  line-height: 1.4;
  display: flex;
  align-items: baseline;
  overflow: hidden;
}
.cshWord { color: var(--text); font-weight: 600; cursor: pointer; flex: 0 0 auto; }
.cshDot { color: var(--muted); flex: 0 0 auto; white-space: pre; }
/* A gloss too long for the column wraps by default, growing that one row
   to two lines and breaking the dense, scannable point of the sheet —
   truncated with an ellipsis instead, same call as a Wordlist row. */
.cshTr {
  color: var(--muted);
  flex: 1 1 auto;
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* Full width: the whole point of the columns layout is using the screen,
   not the ~780px measure ordinary reading sections keep for line length. */
#media.cheatsheet .mLayout { max-width: none; }

/* A section's standing note, printed once above the list. Quiet — it is a
   provenance caveat, not a warning, and it must not compete with the first
   question. */
.mNote {
  font-size: 0.82rem;
  line-height: 1.5;
  color: var(--muted);
  border-left: 2px solid var(--border);
  padding: 2px 0 2px 10px;
  margin: 0 0 14px;
}

/* Slices of a section — the Bundesland row over the naturalisation catalogue.
   It scrolls sideways rather than wrapping into four lines of chips, and it
   stays under the search box while the list moves, so the state you are on is
   always on screen. */
/* overflow-x: auto quietly makes overflow-y auto as well, so a chip's top
   border gets shaved unless the row is padded enough to clear it. */
.mScopes {
  display: flex;
  /* Wider than a plain flex gap needs to be: each chip's own ::before
     brush border bleeds 3px outward past its box (inset: -3px, below) so
     two neighbours only 6px apart had their outward-bled edges touching —
     read as one doubled, stuck-together line rather than two separate
     chips (user feedback, 2026-09-19). 12px leaves real daylight between
     the two bled edges. */
  gap: 12px;
  overflow-x: auto;
  overscroll-behavior-x: contain;
  padding: 5px 1px 7px;
  scrollbar-width: none;
}
.mScopes::-webkit-scrollbar { display: none; }
.mScopes.hidden { display: none; }

/* A chip sliced in half by the edge of the row reads as a rendering fault
   rather than as "there is more this way". It dissolves instead, and only on
   the side there is actually more on — a fade at the left when you are already
   at the start would be a lie about the row. */
.mScopes.fadeR {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent);
          mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent);
}
.mScopes.fadeL {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 40px);
          mask-image: linear-gradient(to right, transparent, #000 40px);
}
.mScopes.fadeL.fadeR {
  -webkit-mask-image: linear-gradient(to right,
    transparent, #000 40px, #000 calc(100% - 40px), transparent);
          mask-image: linear-gradient(to right,
    transparent, #000 40px, #000 calc(100% - 40px), transparent);
}

.mScope {
  flex: 0 0 auto;
  font: inherit;
  font-size: 0.82rem;
  color: var(--muted);
  background: var(--card-bg);
  position: relative;
  border: none;
  border-radius: 0 !important;
  padding: 5px 12px;
  cursor: pointer;
  white-space: nowrap;
}
.mScope::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
  /* A whole row of these side by side repeats the same ink far more often,
     per screen inch, than any standalone bordered button does -- at the
     shared button opacity that repetition read as a heavier, more
     aggressive black grid than a lone pill ever does (user feedback,
     2026-09-19: "too aggressive... make it gray"). Dimmed here only, not
     in --brush-h/-v themselves, so isolated buttons/cards keep their own
     already-approved weight. */
  opacity: 0.55;
}

.mScope:hover { color: var(--text); }
.mScope.on {
  color: var(--text);
  font-weight: 600;
  background: color-mix(in srgb, var(--accent) 12%, var(--card-bg));
}
/* Outline-on-selected kept Herbarium-only -- see #settingsBtn.open's own
   comment above. */
[data-design="editorial"] .mScope.on {
  border-color: var(--accent);
}

/* A cumulative row lights every rung up to the one you chose, so the rung you
   actually chose has to be told apart from the ones it drags along. */
.mLevel { font-variant-numeric: tabular-nums; letter-spacing: 0.02em; }
.mLevel.on { background: color-mix(in srgb, var(--accent) 8%, var(--card-bg)); }
.mLevel.cap {
  background: var(--accent);
  border-color: var(--accent);
  color: #fff;
}
/* ---- Leben in Deutschland ---------------------------------------------- */

/* .mLidNum floats left (question number) so .mLidQ's text wraps around it
   on the first line(s) and reclaims the full row width below it, same
   reasoning/fix as .mHead/.mBadges above (user feedback, screenshot: a long
   question left blank space under the number on its wrapped lines). */
.mLidNum {
  float: left;
  font-size: 0.72rem;
  font-weight: 700;
  color: var(--muted);
  font-variant-numeric: tabular-nums;
  border: 1px solid var(--border);
  border-radius: 999px;
  padding: 2px 8px;
  margin: 2px 10px 6px 0;
}

.mLidQ { font-size: 0.99rem; overflow-wrap: anywhere; }
.mLidQG { font-size: 0.87rem; color: var(--muted); line-height: 1.4; margin: 4px 0 0; }
.mLidQG.alt { font-size: 0.84rem; margin-top: 1px; }

/* Persistent per-item mastery ladder (QUIZ_PROGRESS_KINDS: lid/idioms/abbr —
   buildQuizMasteryLadder, 10-media-wordlist.js). Same look as .wlLadder (the
   Wordlist's own compact spaced-repetition ladder) — small pips + a label,
   in one row — but 3 segments for the quiz's own simpler "3-in-a-row" rule
   instead of the 7-stage SRS ladder. Always a child of .mBadges (renderLid
   builds a ladder-only .mBadges when the item carries no lvl/reg of its
   own) so it sits to the right of the level/register badges, in that same
   floated row — not a row of its own below the head (user feedback,
   screenshot). Replaces the old plain "–/✓" mLidLearn badge. */
.mQuizLadder {
  display: flex;
  align-items: center;
  gap: 6px;
}
.mQuizLadder .csBar { gap: 3px; }
.mQuizLadder .csSeg { width: 12px; height: 4px; }
.mQuizLadder .csMeta {
  /* The label is always part of the ladder; the whole ladder (bar + label)
     only appears while its card is hovered (always visible on touch). */
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.03em;
  white-space: nowrap;
}
.mQuizLadder { opacity: 0; transition: opacity 0.15s ease; }
.mItem:hover .mQuizLadder, .mItem:focus-within .mQuizLadder { opacity: 1; }
@media (hover: none) { .mQuizLadder { opacity: 1; } }

/* Picture beside the answers, not on top of them. A third of the width is
   enough to read an outline map or recognise a building, and it leaves the
   four options where the eye already is. */
.mLidSplit {
  display: flex;
  align-items: flex-start;
  gap: 14px;
  margin-top: 11px;
}
.mLidSplit .mLidFig { flex: 0 0 33%; margin: 0; }
/* Beside a picture the options only get a third of the row, too narrow for
   even two columns — one option per row keeps every row full on its own.
   The three-class second selector is not decorative: .mOptsNarrow (below)
   ties this rule on specificity, and sits later in the file, so without a
   selector that outranks it a long-text question next to a picture would
   silently get two cramped columns instead of one full-width column. */
.mLidSplit .mOpts,
.mLidSplit .mOpts.mOptsNarrow { flex: 1 1 auto; grid-template-columns: 1fr; min-width: 0; margin-top: 0; }

.mLidFig { margin: 10px 0 0; }
.mLidFig img {
  display: block;
  width: 100%;
  max-height: 300px;
  object-fit: contain;
  object-position: top;
  border-radius: 10px;
  border: 1px solid var(--border);
  background: #fff;
  cursor: zoom-in;
}
.mLidFig figcaption { font-size: 0.72rem; color: var(--muted); margin-top: 4px; }

/* Click-to-zoom overlay, shared by every zoomable image. Fixed + centred
   flex so the image scales to whichever viewport dimension is tighter,
   never upscaled past its own natural size. */
#imgLightbox {
  position: fixed;
  inset: 0;
  z-index: 50;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(0, 0, 0, 0.85);
  cursor: zoom-out;
}
#imgLightbox img {
  max-width: 100%;
  max-height: 100%;
  width: auto;
  height: auto;
  border-radius: 8px;
  box-shadow: 0 8px 40px rgba(0, 0, 0, 0.5);
}

/* In-app replacement for window.confirm — the browser's own dialog carries
   the page origin instead of the app's name and cannot take the app's
   colours, light or dark. Same fixed + centred overlay idiom as the image
   lightbox above, but its own card rather than a bare image. */
#confirmDialog {
  position: fixed;
  inset: 0;
  z-index: 90;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  background: rgba(0, 0, 0, 0.55);
}
.dlgCard {
  width: 100%;
  max-width: 360px;
  background: var(--card-bg);
  border: 1px solid var(--border);
  border-radius: var(--radius-sm);
  box-shadow: 0 12px 34px rgba(0, 0, 0, 0.3);
  padding: 20px;
}
.dlgCard p {
  margin: 0 0 18px;
  color: var(--text);
  font-size: 0.94rem;
  line-height: 1.5;
}
.dlgBtns {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
}
.dlgBtns button {
  padding: 10px 18px;
  font-size: 0.9rem;
}
.dlgOk { border-color: color-mix(in srgb, var(--bad) 45%, var(--border)); color: var(--bad); }
.dlgOk:hover { background: color-mix(in srgb, var(--bad) 10%, transparent); }

/* Whole-app "you've been hydrating too fast" notice (see
   showHydrateRateLimitOverlay, 00-core-state.js) -- blurs and dims
   everything behind it rather than a small card over a dark scrim like
   #confirmDialog, since the point is the entire app pausing, not just one
   decision. Highest z-index in the app so it can never be hidden behind an
   already-open dropdown/panel. */
#hydrateLimitOverlay {
  position: fixed;
  inset: 0;
  z-index: 99;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  text-align: center;
  background: color-mix(in srgb, var(--bg) 55%, transparent);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
}
#hydrateLimitOverlay.hidden { display: none; }
.hlCard p {
  margin: 0 0 10px;
  color: var(--text);
  font-size: 1.3rem;
  font-weight: 600;
}
.hlCount {
  font-size: 2.6rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}

/* The "read this section as one text" book view (mediaBookBtn) — renders
   inline in #mediaBody exactly like Learn mode does (#media.learning above),
   not a modal: a reading column at 80% of the actual screen width (not 80%
   of the ordinary 780px list column — the .mLayout cap is lifted here so
   the percentage means something), scrolling with the rest of #media (see
   the comment at #media.learning .mLayout for why #media itself scrolls). */
#media.bookOpen .mLayout { flex: 1; min-height: 0; max-width: none; }
.mBookPane {
  width: 80%;
  margin: 0 auto;
  padding: 4px 0 40px;
}
.mBookBody { color: var(--text); }
.mBookBody p {
  margin: 0 0 1em;
  font-size: 1rem;
  line-height: 1.7;
}
.mBookBody h3 {
  margin: 1.4em 0 0.6em;
  font-size: 1.05rem;
  font-weight: 700;
  color: var(--muted);
  text-align: center;
}
.mBookBody h3:first-child { margin-top: 0; }
.mBookLoading { color: var(--muted); font-style: italic; text-align: center; }
/* Reading-mode date strip (buildMediaBookTimeline, 10-media-wordlist.js) --
   the most load-bearing years in this section's own narrative, at a glance
   above the running text. Horizontal like .mScopes (a country's history
   does not fit four lines of chips any better than a scope list does), but
   a connected line rather than pills -- a timeline reads as a sequence, not
   a set of independent choices. */
.mBookTimeline { margin: 0 0 1.6em; }
.mBookTimelineRow {
  display: flex;
  overflow-x: auto;
  /* Same overflow-x-implies-overflow-y-auto gotcha as #topleft above --
     explicit so this strip can't be vertically drag-scrolled either. */
  overflow-y: hidden;
  overscroll-behavior-x: contain;
  scrollbar-width: none;
  padding: 6px 4px 10px;
}
.mBookTimelineRow::-webkit-scrollbar { display: none; }
/* Same "only fade the side there is actually more on" convention as
   .mScopes.fadeL/.fadeR above. */
.mBookTimelineRow.fadeR {
  -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent);
          mask-image: linear-gradient(to right, #000 calc(100% - 40px), transparent);
}
.mBookTimelineRow.fadeL {
  -webkit-mask-image: linear-gradient(to right, transparent, #000 40px);
          mask-image: linear-gradient(to right, transparent, #000 40px);
}
.mBookTimelineRow.fadeL.fadeR {
  -webkit-mask-image: linear-gradient(to right,
    transparent, #000 40px, #000 calc(100% - 40px), transparent);
          mask-image: linear-gradient(to right,
    transparent, #000 40px, #000 calc(100% - 40px), transparent);
}
.mBookTimelineItem {
  flex: 0 0 auto;
  width: 148px;
  padding: 0 14px 0 0;
  position: relative;
}
.mBookTimelineItem::before {
  content: "";
  position: absolute;
  top: 6px;
  left: 0;
  right: 14px;
  height: 2px;
  background: var(--border);
}
.mBookTimelineItem::after {
  content: "";
  position: absolute;
  top: 2px;
  left: 0;
  width: 10px;
  height: 10px;
  border-radius: 50%;
  background: var(--accent);
}
.mBookTimelineItem:last-child { padding-right: 0; }
.mBookTimelineItem:last-child::before { display: none; }
.mBookTimelineYear {
  display: block;
  margin-top: 18px;
  font-size: 0.8rem;
  font-weight: 700;
  font-variant-numeric: tabular-nums;
  color: var(--accent);
}
.mBookTimelineLabel {
  display: block;
  margin-top: 2px;
  font-size: 0.78rem;
  line-height: 1.3;
  color: var(--muted);
}
/* Back-to-top: a direct child of <body>, clear of every view's own scroll
   container, so `fixed` always anchors to the viewport corner regardless of
   which view is open. app.js toggles `.hidden` on/off scroll. */
#toTopBtn {
  position: fixed;
  right: max(18px, env(safe-area-inset-right));
  bottom: max(18px, env(safe-area-inset-bottom));
  z-index: 40;
  width: 44px;
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0;
  border-radius: 0 !important;
  border: none;
  background: var(--card-tint);
  backdrop-filter: blur(var(--card-blur));
  -webkit-backdrop-filter: blur(var(--card-blur));
  color: var(--muted);
  box-shadow: var(--card-shadow);
  cursor: pointer;
  transition: color 0.15s ease, transform 0.1s ease-out;
}
#toTopBtn::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
#toTopBtn:hover { color: var(--text); }
#toTopBtn:active { transform: scale(0.92); }
#toTopBtn svg { width: 20px; height: 20px; display: block; }

/* Four short options — "14/16/18/20", four country names — belong side by
   side; four full sentences belong stacked. Sizing that used to be a JS
   headcount (are every option AND every gloss under N characters?) rather
   than a layout: one long gloss out of four options was enough to knock the
   whole question back to full-width rows, which is what made "Le
   Royaume-Uni" next to "The United Kingdom" print four tall boxes instead of
   a compact grid. A flex-wrap row lets each option claim only the width its
   own content needs and wrap on its own — no threshold to get wrong, and it
   degrades the same way for a mix of short and long options instead of
   picking one look for all four. */
/* A grid, not a wrapping flex row: with four options, flex-wrap fits as many
   as the width allows and leaves the rest — three across, one stranded alone
   on its own row. Fixed column counts per breakpoint keep every row full:
   four across, two-and-two, or one-per-row, never a lonely leftover. */
.mOpts { display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px; margin-top: 11px; }
.mOpt {
  min-width: 0;
  max-width: 100%;
}
/* Four full-sentence options crammed into quarter-width columns wrap to three
   or four lines each and read as a wall of text — two columns gives them
   room to breathe. renderLid adds this class when the longest option is long
   (see app.js). Two classes beats the one-class base rule on specificity, so
   it wins at any width without needing source-order games — except inside
   the two breakpoints below, where the narrower viewport itself decides the
   column count regardless of content length, so both classes are listed
   there explicitly to override it back down. */
.mOpts.mOptsNarrow { grid-template-columns: repeat(2, 1fr); }

/* Even two columns can be too tight: an option long enough to wrap to two
   lines next to a one-line option leaves the short cell with dead space
   below it, and the pair reads as lopsided rather than merely dense.
   renderLid adds this class instead of mOptsNarrow when the longest option
   clears a higher bar (see app.js) — one option per row, so every row's
   height answers to its own content instead of its row-mate's. Placed after
   .mOptsNarrow on purpose: equal specificity (two classes each), so source
   order decides and this wins for the cards that need it. */
.mOpts.mOptsSingle { grid-template-columns: 1fr; }

/* Below 900px four columns run out of room for real answer text; two-and-two
   keeps every row full. Placed after the base rule on purpose — equal
   specificity means source order decides, and a copy sitting earlier in the
   file (e.g. inside an unrelated breakpoint block above) would quietly lose
   to the unconditional four-column rule above. mOptsSingle is repeated here
   so it still wins down to 1fr instead of being dragged back to two columns. */
@media (max-width: 900px) {
  .mOpts,
  .mOpts.mOptsNarrow { grid-template-columns: repeat(2, 1fr); }
  .mOpts.mOptsSingle { grid-template-columns: 1fr; }
}

/* Four pictures to choose between sit in a grid instead of a stack. */
.mOptsPic {
  display: grid;
  grid-template-columns: repeat(4, 1fr);
  gap: 12px;
}
/* The catalogue's pictures are scans at wildly different aspect ratios — a
   tall coat of arms next to a wide map. A fixed box with `contain` keeps the
   four tiles the same size, so no option is bigger, and therefore louder,
   than the others. */
/* min-width: 0 resets the flex-wrap sizing meant for text options — a grid
   track already has its own width from grid-template-columns, and letting
   the 150px flex min-width leak in here would force four tracks wider than
   some viewports can fit. */
.mOptsPic .mOpt { padding: 8px; align-items: center; justify-content: space-between; min-width: 0; }
.mOptsPic img {
  display: block;
  width: 100%;
  height: 128px;
  object-fit: contain;
  border-radius: 6px;
}
.mOptTag { font-size: 0.72rem; font-weight: 700; color: var(--muted); margin-top: 6px; }

.mOpt {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: 2px;
  font: inherit;
  font-size: 0.92rem;
  line-height: 1.4;
  text-align: start;
  color: var(--text);
  background: var(--card-bg);
  position: relative;
  border: none;
  border-radius: 0 !important;
  padding: 9px 12px;
  cursor: pointer;
}
.mOpt::before {
  content: "";
  position: absolute;
  inset: -3px;
  pointer-events: none;
  background-repeat: repeat-x, repeat-x, repeat-y, repeat-y;
  background-position: top left, bottom left, top left, top right;
  background-size: 40px 8px, 40px 8px, 8px 40px, 8px 40px;
  background-image: var(--brush-h), var(--brush-h), var(--brush-v), var(--brush-v);
  -webkit-mask-image: var(--brush-corner-mask);
          mask-image: var(--brush-corner-mask);
  -webkit-mask-composite: source-in;
  mask-composite: intersect;
}
.mOpt:disabled { cursor: default; }

/* A long compound like "Premierministerin/Premierminister" has no space for
   the browser to break on, so without this it either overflows the card or
   gets cut mid-word with no visual cue. overflow-wrap allows the break;
   hyphens adds the "-" that makes it read as one, so it looks deliberate.
   min-width:0 is load-bearing, not decorative: .mOpt is a column flexbox, and
   a flex item's default min-width is its content's unwrapped width — without
   overriding it, overflow-wrap never even gets the chance to fire, and the
   text spills out past its grid column into the neighbouring option. */
.mOptText {
  display: block;
  width: 100%;
  min-width: 0;
  overflow-wrap: break-word;
  -webkit-hyphens: auto;
  hyphens: auto;
}

.mOptGlossWrap { display: block; width: 100%; }
.mOptGloss { font-size: 0.83rem; color: var(--muted); line-height: 1.35; margin: 1px 0 0; }

/* After the press: the right answer is named, the rest recede, and a wrong
   pick is marked as well so you can see what you actually chose. The tick and
   the cross carry the verdict on their own — colour alone would leave a
   red-green blind learner reading four identical boxes. */
.mOpt.right,
.mOpt.wrong { position: relative; padding-inline-end: 34px; opacity: 1; }

.mOpt.right::after,
.mOpt.wrong::after {
  content: "";
  position: absolute;
  inset-inline-end: 11px;
  top: 8px;
  width: 15px;
  height: 15px;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
  -webkit-mask-size: contain;
          mask-size: contain;
  -webkit-mask-position: center;
          mask-position: center;
}
.mOpt.right::after {
  background-color: var(--accent);
  -webkit-mask-image: var(--check-mark-mask);
          mask-image: var(--check-mark-mask);
}
.mOpt.wrong::after {
  background-color: var(--bad);
  -webkit-mask-image: var(--cross-mark-mask);
          mask-image: var(--cross-mark-mask);
}

.mOptsPic .mOpt.right,
.mOptsPic .mOpt.wrong { padding-inline-end: 8px; }
.mOptsPic .mOpt.right::after,
.mOptsPic .mOpt.wrong::after { top: 6px; inset-inline-end: 8px; }

/* Real, solid colour borders here, not the hand-drawn brush -- correctness
   feedback needs an actual green/red, which (like --accent generally) can't
   be baked into the fixed-ink brush SVG; the brush ::before is suppressed
   for these two states so it doesn't double up with this one. */
.mOpt.right,
.mOpt.wrong { border-style: solid; border-width: 2px; }
.mOpt.right::before,
.mOpt.wrong::before { content: none; }
.mOpt.right {
  border-color: var(--accent);
  padding-block: 8px;
  background: color-mix(in srgb, var(--accent) 16%, var(--card-bg));
  font-weight: 600;
}
.mOpt.wrong {
  border-color: var(--bad);
  padding-block: 8px;
  background: color-mix(in srgb, var(--bad) 14%, var(--card-bg));
}
.mOpt.wrong .mOptText { text-decoration: line-through; }
.mOpt.off { opacity: 0.5; }

.mLidWhy {
  margin-top: 11px;
  padding-left: 11px;
  border-left: 2px solid var(--accent);
}
.mLidWhyDe { margin: 0; font-size: 0.9rem; line-height: 1.45; }
.mLidWhyG { margin: 3px 0 0; font-size: 0.85rem; color: var(--muted); line-height: 1.4; }
.mLidWhyG.alt { margin-top: 1px; font-size: 0.82rem; }

/* The interview card for questions about the candidate. No options to press,
   so the whole card is visible at once and the two blocks have to be told
   apart by eye alone: a label above each, and a rule down the side in a
   different colour — accent for what is being assessed, border-grey for the
   model answer, which is the lesser of the two despite being the longer. */
/* Reuses .mLidNum's own float:left (see the "Leben in Deutschland" section
   above) for the same wrap-around-the-number-then-reclaim-full-width fix. */
.mPersoQ { font-size: 0.99rem; overflow-wrap: anywhere; }

.mPersoBlock {
  margin-top: 11px;
  padding-left: 11px;
  border-left: 2px solid var(--border);
}
.mPersoListen { border-left-color: var(--accent); }
.mPersoLabel {
  margin: 0 0 3px;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.mPersoText { margin: 0; font-size: 0.9rem; line-height: 1.45; }
.mPersoGloss { margin: 3px 0 0; font-size: 0.85rem; color: var(--muted); line-height: 1.4; }
.mPersoGloss.alt { margin-top: 1px; font-size: 0.82rem; }

/* Loud on purpose. Reciting the model answer is the way this card is misused,
   and the warning has to survive being skim-read. */
.mPersoWarn {
  margin: 10px 0 0;
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--bad);
}
.mPersoWarn::before { content: "⚠ "; }

/* The topic title: German first, then each active study-from language's
   name inline after a "·" — the exact shape renderAbbr already uses for
   "z. B. · zum Beispiel" (mAbbrShortT/mAbbrFullT), reused here since it's
   the same pattern (a short target-language label plus its gloss). */
.mSchrTitleTr {
  font-weight: 400;
  color: var(--muted);
  font-size: 0.86em;
}
.mSchrTitleDe + .mSchrTitleTr::before,
.mSchrTitleTr + .mSchrTitleTr::before {
  content: "·";
  margin: 0 0.5em;
  opacity: 0.65;
}

/* The Goethe-Schreiben card: an exam prompt (task+points, accent rule, the
   way mPersoListen marks "what's being assessed") followed by up to three
   model texts (grey rule, same shape as mPersoBlock/mPersoText/mPersoGloss —
   deliberately identical so the two exam-prep card kinds read as one family).
   With 2+ versions, .mSchrTabs (styled above under "level switch") replaces
   each version's own inline label and only the selected one's .mSchrVersion
   stays visible (see 14b-goethe-schreiben.js); a lone version keeps its
   numbered label and optional style descriptor inline instead. */
.mSchrTabs { margin-top: 11px; }
.mSchrTask {
  margin-top: 4px;
  padding-left: 11px;
  border-left: 2px solid var(--accent);
}
.mSchrPoints { margin: 8px 0 0; padding: 0; list-style: none; }
.mSchrPoint { margin: 6px 0 0; }
.mSchrPointWho {
  margin: 0 0 2px;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--muted);
}
.mSchrVersion {
  margin-top: 11px;
  padding-left: 11px;
  border-left: 2px solid var(--border);
}
/* Right after the tab strip, that 11px default would stack on top of the
   tabs' own 11px gap from the task above — tighten it so the tabs and the
   content they control read as one connected control-plus-pane unit. */
.mSchrTabs + .mSchrVersion { margin-top: 8px; }
.mSchrVersionHead { display: flex; align-items: baseline; gap: 6px; flex-wrap: wrap; }
.mSchrLabel {
  margin: 0 0 3px;
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--muted);
}
.mSchrStyle { margin: 0 0 3px; font-size: 0.72rem; color: var(--muted); font-style: italic; }
.mSchrText { margin: 0; font-size: 0.9rem; line-height: 1.45; }
/* A version's German text and its translation are each split into one <p>
   per paragraph (see appendSchrParas) — space consecutive ones apart, but
   only from their own kind, so the translation block still reads as a
   distinct group right after the German one rather than one long run. */
.mSchrText + .mSchrText { margin-top: 8px; }
.mSchrGloss { margin: 3px 0 0; font-size: 0.85rem; color: var(--muted); line-height: 1.4; }
.mSchrGloss + .mSchrGloss { margin-top: 8px; }
.mSchrGloss.alt { margin-top: 1px; font-size: 0.82rem; }
.mSchrGloss.alt + .mSchrGloss.alt { margin-top: 8px; }
/* Task/bullet translations use the shared .mInlineGloss (see .mDe above) —
   mSchrGloss's stacked shape is for the longer model-text versions only. */
.mSchrWarn {
  margin: 10px 0 0;
  font-size: 0.8rem;
  line-height: 1.45;
  color: var(--bad);
}
.mSchrWarn::before { content: "⚠ "; }

@media (max-width: 640px) {
  /* Bumped well past the old flat 60px (user feedback, repeated: the Next
     button still read as flush against the bottom of the screen) and now
     also grows with env(safe-area-inset-bottom) -- iOS Safari's own
     collapsible toolbar/home-indicator area isn't reserved layout space the
     way a notch is, so a flat px value alone doesn't reliably clear it. */
  #media { padding: 6px 12px calc(110px + env(safe-area-inset-bottom)); }
  .mItem { padding: 12px 13px; border-radius: 12px; }
  .mWho { min-width: 0; flex-basis: 62px; }
  .mOptsPic { grid-template-columns: repeat(2, 1fr); }
  .mOpts,
  .mOpts.mOptsNarrow { grid-template-columns: 1fr; }
  /* Too narrow for two columns: the picture goes back on top, but capped so it
     is a picture and not a page. */
  .mLidSplit { flex-direction: column; }
  .mLidSplit .mLidFig { flex-basis: auto; width: 100%; }
  .mLidSplit .mLidFig img { max-height: 210px; }
  /* The 80% reading column (see .mBookPane above) is deliberate on a wide
     screen, where the gutters keep a line from running edge to edge — on a
     phone the same 80% just wastes the screen on two empty margins, so the
     book view goes full width here instead (#media's own 12px padding just
     above is still there, so text isn't glued to the screen edge). */
  .mBookPane { width: 100%; }
}

/* Pastel swatches keep their soft look in dark mode (user request 2026-09-29):
   just a gentle dim of the light hex, no saturation boost. Must stay last. */
:root[data-theme="dark"][data-a1="peach"] { --accent: #d08f4d; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="peach"] { --accent: #d08f4d; }
}
:root[data-theme="dark"][data-a1="apricot"] { --accent: #d4a85c; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="apricot"] { --accent: #d4a85c; }
}
:root[data-theme="dark"][data-a1="butter"] { --accent: #d1bb78; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="butter"] { --accent: #d1bb78; }
}
:root[data-theme="dark"][data-a1="cream"] { --accent: #d5c38c; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="cream"] { --accent: #d5c38c; }
}
:root[data-theme="dark"][data-a1="sage"] { --accent: #749067; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="sage"] { --accent: #749067; }
}
:root[data-theme="dark"][data-a1="powder"] { --accent: #90bace; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="powder"] { --accent: #90bace; }
}
:root[data-theme="dark"][data-a1="sky"] { --accent: #7b9ec1; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="sky"] { --accent: #7b9ec1; }
}
:root[data-theme="dark"][data-a1="periwinkle"] { --accent: #7888db; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="periwinkle"] { --accent: #7888db; }
}
:root[data-theme="dark"][data-a1="lilac"] { --accent: #ad9dc4; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="lilac"] { --accent: #ad9dc4; }
}
:root[data-theme="dark"][data-a1="lavender"] { --accent: #baafce; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="lavender"] { --accent: #baafce; }
}
:root[data-theme="dark"][data-a1="orchid"] { --accent: #9f78b6; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="orchid"] { --accent: #9f78b6; }
}
:root[data-theme="dark"][data-a1="mauve"] { --accent: #8c6490; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="mauve"] { --accent: #8c6490; }
}
:root[data-theme="dark"][data-a1="blossom"] { --accent: #c87488; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="blossom"] { --accent: #c87488; }
}
:root[data-theme="dark"][data-a1="petal"] { --accent: #d4aab8; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="petal"] { --accent: #d4aab8; }
}
:root[data-theme="dark"][data-a1="slate"] { --accent: #3f454b; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="slate"] { --accent: #3f454b; }
}
:root[data-theme="dark"][data-a1="seafoam"] { --accent: #91b8a9; }
@media (prefers-color-scheme: dark) {
  :root:not([data-theme])[data-a1="seafoam"] { --accent: #91b8a9; }
}
:root[data-a1="seafoam"] { --accent: #a9d6c4; }

/* ---- hover = glow, not underline (user request 2026-09-29) ---------------
   The pencil-underline mask now marks ONLY selected/open/active states.
   A plain hover (on something not already selected) suppresses that
   underline and lights the label with a soft accent glow instead. Must
   stay last so it wins the cascade over the per-component hover rules. */
#settingsTray .langDropBtn:hover:not(.open) .settingLbl::after,
#settingsTray #themeToggle:hover .settingLbl::after,
#settingsTray .settingToggle:hover:not(.active):not([aria-pressed="true"]) .settingLbl::after,
#settingsTray .settingDanger:hover .settingLbl::after,
.ldOpt:hover:not([aria-checked="true"]):not(.current) .ldEndo::before,
.viewBtn:hover:not(.active) .viewBtnLabel::after,
#modePanel .modeBtn:hover:not(.active) .modeBtnLabel::after,
#levelPanel .levelBtn:hover:not(.active) .levelBtnLabel::after,
.formShowMe:hover::after,
.gTocLink:hover:not(.on) .gTocTitle::after,
.mTocSong:hover:not(.on) .mTocName .gTocUL {
  transform: scaleX(0);
}
.settingLbl, .ldEndo, .viewBtnLabel, .modeBtnLabel, .levelBtnLabel,
.formShowMe, .gTocTitle, .mTocName {
  transition: text-shadow 0.2s ease;
}
#settingsTray .langDropBtn:hover:not(.open) .settingLbl,
#settingsTray #themeToggle:hover .settingLbl,
#settingsTray .settingToggle:hover:not(.active):not([aria-pressed="true"]) .settingLbl,
.ldOpt:hover:not([aria-checked="true"]):not(.current) .ldEndo,
.viewBtn:hover:not(.active) .viewBtnLabel,
#modePanel .modeBtn:hover:not(.active) .modeBtnLabel,
#levelPanel .levelBtn:hover:not(.active) .levelBtnLabel,
.formShowMe:hover,
.gTocLink:hover:not(.on) .gTocTitle,
.mTocSong:hover:not(.on) .mTocName {
  text-shadow: 0 0 12px color-mix(in srgb, var(--accent) 45%, transparent);
}
#settingsTray .settingDanger:hover .settingLbl {
  text-shadow: 0 0 12px color-mix(in srgb, var(--bad) 45%, transparent);
}
.ldCopyPrimaryBtn:hover,
.wordPreviewRefLink:hover {
  text-decoration: none;
  text-shadow: 0 0 12px color-mix(in srgb, var(--accent) 45%, transparent);
}

/* ---- borders: barely-visible glowing line (user request 2026-09-29) -------
   Replaces the hand-wobbled brush-tile borders everywhere. Each bordered
   component's ::before is now a plain box with a 1px hairline and a soft
   halo, both derived from --text so they follow light/dark and any Design.
   Must stay last. */
#settingsBtn::before,
#accountBtn::before,
.langDropBtn::before,
#viewNav::before,
#cshLevelNav::before,
.mSchrTabs::before,
#schrAufgabeNav::before,
.mTaleTrTabs::before,
#card::before,
#btnBack::before,
#btnUnknown::before,
#btnKnown::before,
#btnForward::before,
#btnToCurrent::before,
.lsChip::before,
#sessionInputWrap::before,
.gTableWrap::before,
.gHints::before,
.mSheet::before,
.mItem::before,
.mLearnLimitInputWrap::before,
.wlSortToggle::before,
.wlSortDirBtn::before,
.wlSortTypeBtn::before,
.wlTopicBtn::before,
#mediaLearnBtn::before,
#mediaBookBtn::before,
.mScope::before,
#toTopBtn::before,
.mOpt::before {
  inset: 0 !important;
  background: none !important;
  -webkit-mask-image: none !important;
          mask-image: none !important;
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--text) 16%, transparent),
              0 0 7px color-mix(in srgb, var(--text) 9%, transparent);
}
#mediaSearch {
  box-shadow: 0 0 0 1px color-mix(in srgb, var(--text) 16%, transparent),
              0 0 7px color-mix(in srgb, var(--text) 9%, transparent);
}
/* contain:paint boxes clip anything drawn outside them -- glow inward. */
#card::before, .mSheet::before, .mItem::before {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 16%, transparent),
              inset 0 0 7px color-mix(in srgb, var(--text) 9%, transparent);
}

/* Right-to-left targets (Hebrew, Arabic, Persian, Pashto...) in the Abbreviations,
   Idioms and Wordlist cards: the text itself still runs right-to-left (the
   dir="auto" on each element handles that), but every line starts at the LEFT
   edge like the rest of the card instead of hugging the right one. A plain
   `text-align: start` on a dir="auto" RTL element resolves to the right, so the
   alignment is pinned to left explicitly and inherited by the whole card. */
.mIdiom, .mAbbr, .wlRow { text-align: left; }
.mIdiom [dir="auto"], .mAbbr [dir="auto"], .wlRow [dir="auto"] { text-align: left; }

/* Growing standalone answer box (a <textarea>, see makeClozeInput(grow)):
   one row that widens as you type, then wraps and gets taller. */
textarea.clozeInput {
  display: block;
  resize: none;
  overflow: hidden;
  box-sizing: border-box;
  line-height: 1.3;
  white-space: pre-wrap;
  overflow-wrap: anywhere;
  min-height: 0;
}

/* Dropdown lists mark the selection with the pencil underline alone -- no
   trailing checkmark (user feedback). Where a row has a leading flag or topic
   icon, that icon gets a loose hand-drawn ring in the primary colour instead. */
.ldSel,
#modePanel .modeBtn.active::after,
#levelPanel .levelBtn.active::after { display: none; }
.ldOpt[aria-checked="true"] .ldFlag,
.ldOpt[aria-checked="true"] .ldTopicEmoji { position: relative; }
.ldOpt[aria-checked="true"] .ldFlag::after,
.ldOpt[aria-checked="true"] .ldTopicEmoji::after {
  content: "";
  position: absolute;
  left: 50%;
  top: 50%;
  width: 1.85em;
  height: 1.85em;
  transform: translate(-50%, -50%) rotate(-8deg);
  background-color: var(--accent);
  pointer-events: none;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Cpath d='M20 3 C31 2 38 10 37 21 C36 31 28 38 19 37 C9 36 3 28 4 18 C5 9 13 3 24 4 C27 4.5 29.5 5.5 31 7' stroke='%23fff' stroke-width='1' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Cpath d='M20 3 C31 2 38 10 37 21 C36 31 28 38 19 37 C9 36 3 28 4 18 C5 9 13 3 24 4 C27 4.5 29.5 5.5 31 7' stroke='%23fff' stroke-width='1' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* Translations into the other study-from languages: plain words separated by
   bullets in one wrapping row, no badge chrome (only the target-language
   answer keeps its badge). */
/* Inline flow, not a flex row of atomic items: a long translation wraps
   inside itself so each line uses the card's full width. */
.trRow.trRowOther { display: block; width: 100%; text-align: center; overflow-wrap: anywhere; line-height: 1.45; }
.trRow.trRowOther > span.trPlain,
.trRow.trRowOther > span.trDot { display: inline; }
.trRow.trRowOther > span.trDot { margin: 0 0.6em; }
.trRow.trRowOther > span.trPlain {
  background: none;
  border: 0;
  border-radius: 0;
  padding: 0;
  color: var(--text);
}
.trRow.trRowOther > span.trDot { background: none; border: 0; padding: 0; }


/* Form badges (gender/plural flags etc.) live inside #word, ahead of the
   headword, on the same line; hidden with the rest of the answer until reveal. */
#word .formChip {
  display: inline-block;
  vertical-align: middle;
  margin-inline-end: 10px;
  font-family: var(--font-body, inherit);
  font-size: 0.8rem;
  font-weight: 400;
  line-height: 1.4;
}
#card.answer-hidden #word .formChip { display: none; }

/* Our own hand-drawn flags (app-src/00b-hand-flags.js) replace flag emoji. */
.hdFlag {
  display: inline-block;
  width: 1.32em;
  height: 0.88em;
  vertical-align: -0.14em;
  background-repeat: no-repeat;
  background-position: center;
  background-size: 100% 100%;
}

/* Wordlist: form badges sit inline in front of the headword. */
.wlWord .wlFormsInline {
  display: inline-flex;
  vertical-align: middle;
  margin: 0 10px 0 0;
  gap: 6px;
  font-size: 0.8rem;
  font-weight: 400;
}

/* Dropdowns hug their content. */
#levelPanel { width: max-content; min-width: 0; }
#modePanel { width: max-content; min-width: 0; }
/* Smaller flags (and so their rings) inside dropdown rows. */
.ldOpt .hdFlag { font-size: 0.8rem; }

/* Grammar examples: two columns when the section is wide enough. */
.gSection { container-type: inline-size; }
@container (min-width: 620px) {
  .gExGrid { display: grid; grid-template-columns: 1fr 1fr; column-gap: 28px; }
  /* Odd count: the last example spans both columns. */
  .gExGrid > .gExample:last-child:nth-child(odd) { grid-column: 1 / -1; }
}

/* Centre the drawn flag in its row slot (and so inside its ring). */
.ldFlag { display: inline-flex; align-items: center; justify-content: center; align-self: center; }
.ldFlag .hdFlag, .ldOpt .hdFlag { vertical-align: middle; display: block; }

.mMeanInline { color: var(--muted); }
.mMeanInline > span:first-child { color: var(--text); }

/* Tables: thin hairline grid instead of the thick brush tiles, plus a rule
   under the table title. */
table.gTable th, table.gTable td,
.gTableScroll > table.gTable tbody tr:last-child th,
.gTableScroll > table.gTable tbody tr:last-child td,
.gTableScroll > table.gTable th:last-child,
.gTableScroll > table.gTable td:last-child {
  background-image: none !important;
  border-bottom: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
  border-right: 1px solid color-mix(in srgb, var(--text) 14%, transparent);
}
.gTableScroll > table.gTable tbody tr:last-child th,
.gTableScroll > table.gTable tbody tr:last-child td { border-bottom: 0; }
.gTableScroll > table.gTable th:last-child,
.gTableScroll > table.gTable td:last-child { border-right: 0; }
.gCaption { border-bottom: 1px solid color-mix(in srgb, var(--text) 14%, transparent); }

/* Top-bar controls: the outline was drawn OUTSIDE each box, and the scrolling
   top row clips anything outside it vertically, leaving only the side lines.
   Drawn inward instead so the whole outline survives. */
#settingsBtn::before,
#accountBtn::before,
.langDropBtn::before,
#viewNav::before,
#sessionInputWrap::before {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 16%, transparent),
              inset 0 0 7px color-mix(in srgb, var(--text) 9%, transparent) !important;
}

/* Form markers before the headword read as plain grey text at the word's own
   size -- no badge box, no underline. */
#word .formChip, .wlWord .wlFormsInline .formChip {
  background: none !important;
  border: 0 !important;
  box-shadow: none !important;
  padding: 0 !important;
  border-radius: 0;
  color: var(--muted);
  font-family: inherit;
  font-size: inherit;
  font-weight: inherit;
  text-decoration: none;
  margin-inline-end: 0.3em;
}
#word .formChip b, .wlWord .wlFormsInline .formChip b { font-weight: inherit; margin-inline-end: 0.2em; }
.wlWord .wlFormsInline { font-size: inherit; margin-right: 0; gap: 0.3em; }

/* Same inward outline for every other top-row control (CEFR level strip,
   Schreiben tabs, Aufgabe toggle, tale-translation tabs, Learn/Book/search
   buttons, scope pills), which the scrolling row was clipping too. */
#cshLevelNav::before,
.mSchrTabs::before,
#schrAufgabeNav::before,
.mTaleTrTabs::before,
#mediaLearnBtn::before,
#mediaBookBtn::before,
.mScope::before,
.wlSortToggle::before,
.wlSortDirBtn::before,
.wlSortTypeBtn::before,
.wlTopicBtn::before,
.mLearnLimitInputWrap::before {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 16%, transparent),
              inset 0 0 7px color-mix(in srgb, var(--text) 9%, transparent) !important;
}

/* Contents rails (Grammar topics, Media sections) get the outline back. */
#grammarIndex,
#media.indexed #mediaIndex {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 16%, transparent),
              inset 0 0 7px color-mix(in srgb, var(--text) 9%, transparent);
}

/* Grammar contents: per-line underline bars (see paintGrammarTocUnderlines),
   replacing the single full-width ::after box. */
.gTocTitle::after { display: none; }
.gTocLink:hover .gTocTitle .gTocUL,
.gTocLink.on .gTocTitle .gTocUL { transform: scaleX(1); }

/* Grammar: topic sheets and the contents rail wear a real hairline border
   (more robust than an inset shadow on these blurred panels). */
.gTopic, #grammarIndex, #media.indexed #mediaIndex {
  border: 1px solid color-mix(in srgb, var(--text) 16%, transparent) !important;
  box-shadow: 0 0 7px color-mix(in srgb, var(--text) 9%, transparent) !important;
}

/* Multi-line labels: per-line bars replace the single pseudo-element bar. */
.ulMulti::before, .ulMulti::after { display: none !important; }
.ldOpt:hover .ldEndo.ulMulti .gTocUL,
.ldOpt[aria-checked="true"] .ldEndo.ulMulti .gTocUL,
.ldSingle.current .ldEndo.ulMulti .gTocUL,
#settingsTray .langDropBtn:hover .settingLbl.ulMulti .gTocUL,
#settingsTray #themeToggle:hover .settingLbl.ulMulti .gTocUL,
#settingsTray .settingToggle:hover .settingLbl.ulMulti .gTocUL,
#settingsTray .langDropBtn.open .settingLbl.ulMulti .gTocUL,
#settingsTray .settingToggle.active .settingLbl.ulMulti .gTocUL,
#settingsTray .settingToggle[aria-pressed="true"] .settingLbl.ulMulti .gTocUL,
#modePanel .modeBtn:hover .modeBtnLabel.ulMulti .gTocUL,
#modePanel .modeBtn:focus-visible .modeBtnLabel.ulMulti .gTocUL,
#modePanel .modeBtn.active .modeBtnLabel.ulMulti .gTocUL,
#levelPanel .levelBtn:hover .levelBtnLabel.ulMulti .gTocUL,
#levelPanel .levelBtn.active .levelBtnLabel.ulMulti .gTocUL,
.viewBtn:hover .viewBtnLabel.ulMulti .gTocUL,
.viewBtn.active .viewBtnLabel.ulMulti .gTocUL,
.formShowMe:hover.ulMulti .gTocUL { transform: scaleX(1); }

/* Form marker and headword share one baseline (no flex/middle offset). */
.wlWord .wlFormsInline, #word .formChip, .wlWord .wlFormsInline .formChip {
  display: inline;
  vertical-align: baseline;
  line-height: inherit;
}
.wlWord .wlFormsInline .formChip { font-size: inherit; font-weight: inherit; }
.wlWord .wlFormsInline { margin-right: 0.3em; }

/* Study card outline: #card is the scroll container, so a ::before outline
   (absolute, inset:0) only spans the first screenful and the outline ended
   part-way down long cards. An inset shadow on #card itself stays fixed to
   its visible box instead. */
#card::before { display: none !important; }
#card {
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--text) 16%, transparent),
              inset 0 0 7px color-mix(in srgb, var(--text) 9%, transparent) !important;
}

/* Revealed card: the asked word steps back, the target-language answer badge
   steps forward. */
#card:not(.answer-hidden) #word { font-size: 1.5rem; margin-bottom: 14px; }
#card:not(.answer-hidden) #translations .deBadge { font-size: 1.7rem; padding: 10px 20px; }

/* Answer badge: a little smaller, no glow, and the revealed block eases open. */
#card:not(.answer-hidden) #translations .deBadge {
  font-size: 1.4rem;
  padding: 8px 16px;
  box-shadow: none;
}
@keyframes revealExpand {
  from { opacity: 0; transform: translateY(-6px) scale(0.94); }
  to   { opacity: 1; transform: none; }
}
#card:not(.answer-hidden) #translations .deBadge { animation: revealExpand 0.32s cubic-bezier(.2,.8,.2,1) both; }
#card:not(.answer-hidden) #translations,
#card:not(.answer-hidden) #examples { animation: revealExpand 0.28s ease-out both; }
@media (prefers-reduced-motion: reduce) {
  #card #translations, #card #translations .deBadge, #card #examples { animation: none !important; }
}

/* New card entrance (class toggled in paintCard). */
@keyframes cardEnter {
  from { opacity: 0; transform: translateX(26px) scale(0.985); }
  to   { opacity: 1; transform: none; }
}
#card.cardEnter { animation: cardEnter 0.34s cubic-bezier(.2,.8,.2,1) both; }
@media (prefers-reduced-motion: reduce) { #card.cardEnter { animation: none; } }

/* Sidecards: same surface as the study card, wider, tucked in close to it. */
@media (min-width: 1181px) {
  #deck { grid-template-columns: minmax(0, 1fr) auto auto minmax(0, 1fr); align-content: center; }
  /* Sized by what is inside, never so wide that the card + panel group (centred)
     leaves less than 5% of the screen free on either side: the card can give
     way down to its 340px minimum, so the panel may take 90vw - 340px - gap. */
  #refPanelFamily, #refPanelForms {
    /* max-content would collapse to the min-width: the panel is a size
       container (@container rules below), which has no intrinsic width. */
    width: min(720px, calc(90vw - 348px));
    min-width: 360px;
    margin-left: 8px;
  }
}
.refPanel {
  background: var(--card-tint);
  backdrop-filter: blur(var(--card-blur));
  -webkit-backdrop-filter: blur(var(--card-blur));
}

/* Both side-card handles live together on the right, Word family beside the book. */
#refBtns { justify-content: flex-end; gap: 8px; }

/* Active side-card handle: a hand-drawn ring (same as the selected flag in
   dropdowns) instead of a filled disc. */
.refBtn { position: relative; }
.refBtn.active { background: none; }
.refBtn.active::after {
  content: "";
  position: absolute;
  inset: -1px;
  background-color: var(--accent);
  transform: rotate(-8deg);
  pointer-events: none;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Cpath d='M20 3 C31 2 38 10 37 21 C36 31 28 38 19 37 C9 36 3 28 4 18 C5 9 13 3 24 4 C27 4.5 29.5 5.5 31 7' stroke='%23fff' stroke-width='1' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Cpath d='M20 3 C31 2 38 10 37 21 C36 31 28 38 19 37 C9 36 3 28 4 18 C5 9 13 3 24 4 C27 4.5 29.5 5.5 31 7' stroke='%23fff' stroke-width='1' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* Wordlist entry handles: same ring for the open one. */
.wlRefBtn { position: relative; }
.wlRefBtn.active { background: none; }
.wlRefBtn.active::after {
  content: "";
  position: absolute;
  inset: -1px;
  background-color: var(--accent);
  transform: rotate(-8deg);
  pointer-events: none;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Cpath d='M20 3 C31 2 38 10 37 21 C36 31 28 38 19 37 C9 36 3 28 4 18 C5 9 13 3 24 4 C27 4.5 29.5 5.5 31 7' stroke='%23fff' stroke-width='1' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Cpath d='M20 3 C31 2 38 10 37 21 C36 31 28 38 19 37 C9 36 3 28 4 18 C5 9 13 3 24 4 C27 4.5 29.5 5.5 31 7' stroke='%23fff' stroke-width='1' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

/* Sidecards hug their content and sit as one block centred on the study card:
   the card spans every row, so spare height goes to the outer 1fr rows, not
   into the panels. */
@media (min-width: 1181px) {
  #deck #card { grid-row: 1 / 5; }
  #refPanelFamily, #refPanelForms { align-self: center; }
}

/* Study card outline: same quiet 1px line the sidecards use, no glow. */
#card {
  box-shadow: inset 0 0 0 var(--card-border-w, 1px) color-mix(in srgb, var(--border) 72%, transparent) !important;
}

/* Sidecard body text matches the translations on the main card. */
.refPanel, .refPanel .refFlowTr { font-size: 1.05rem; }
.refPanel .refHead { font-size: 0.78rem; }

/* Sidecard outline as an inset line (not a border): while a panel is fading
   at a scroll edge its mask is clipped to the padding box, which used to
   swallow the border entirely. */
.refPanel {
  border: 0 !important;
  box-shadow: inset 0 0 0 var(--card-border-w, 1px) color-mix(in srgb, var(--border) 72%, transparent) !important;
}

/* Sidecard text = the size of the example translations on the main card. */
.refPanel, .refPanel .refFlowTr { font-size: 0.92rem; }
.refPanel .refHead { font-size: 0.72rem; }

/* Wide enough that the two side-card handles (2 x 34px + gap) clear the
   centred progress ladder by at least one button's width. */
@media (min-width: 1181px) {
  #deck #card { min-width: min(520px, 100%); }
}

/* Word family: short rows, even count -> two per line when the panel is wide. */
.refPanel { container-type: inline-size; }


/* An ON toggle in the settings tray rings its icon, like the selected flag in
   dropdowns. Icon centre sits at (19px, 18px) of the row. */
#settingsTray .settingToggle { position: relative; }
#settingsTray .settingToggle.active::before,
#settingsTray .settingToggle[aria-pressed="true"]::before {
  content: "";
  position: absolute;
  left: 4px;
  top: 3px;
  width: 30px;
  height: 30px;
  background-color: var(--accent);
  transform: rotate(-8deg);
  pointer-events: none;
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Cpath d='M20 3 C31 2 38 10 37 21 C36 31 28 38 19 37 C9 36 3 28 4 18 C5 9 13 3 24 4 C27 4.5 29.5 5.5 31 7' stroke='%23fff' stroke-width='1' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Cpath d='M20 3 C31 2 38 10 37 21 C36 31 28 38 19 37 C9 36 3 28 4 18 C5 9 13 3 24 4 C27 4.5 29.5 5.5 31 7' stroke='%23fff' stroke-width='1' fill='none' stroke-linecap='round'/%3E%3C/svg%3E");
  -webkit-mask-size: 100% 100%;
          mask-size: 100% 100%;
  -webkit-mask-repeat: no-repeat;
          mask-repeat: no-repeat;
}

@media (min-width: 1181px) {
  #deck #card { min-width: min(560px, 100%); }
}

/* A plain px min-width: the earlier min(560px, 100%) resolved 100% against an
   auto-sized grid track (cyclic), collapsing to the content width. */
@media (min-width: 1181px) {
  #deck #card { min-width: 560px; }
}


/* ---- Design panel: gradient layer + animation toggle ---------------------
   #bgGradient sits under #bgPattern (earlier in the DOM, same z-index:-1) so
   a texture can overlay any gradient. Its image is set inline from JS
   (applyGradient); with no data-bggradient on <html> it paints nothing. */
#bgGradient {
  position: fixed;
  inset: 0;
  z-index: -1;
  pointer-events: none;
  display: none;
  background-repeat: no-repeat;
  background-size: 100% 100%;
  opacity: 0.8;
}
:root[data-theme="dark"] #bgGradient { opacity: 0.95; }
@media (prefers-color-scheme: dark) { :root:not([data-theme="light"]) #bgGradient { opacity: 0.95; } }
:root[data-bggradient] #bgGradient { display: block; }
/* Animated: the layers are drawn twice as large and slowly pan across. */
:root[data-bganim] #bgGradient {
  background-size: 200% 200%;
  animation: bgGradDrift 24s ease-in-out infinite;
}
@keyframes bgGradDrift {
  0%   { background-position: 0% 0%; }
  25%  { background-position: 100% 30%; }
  50%  { background-position: 100% 100%; }
  75%  { background-position: 20% 80%; }
  100% { background-position: 0% 0%; }
}
@media (prefers-reduced-motion: reduce) { :root[data-bganim] #bgGradient { animation: none; } }
.bgGradBtn { background-size: 100% 100%; background-color: var(--card-bg); }
#backgroundPanel .ldAnimToggle { margin-top: 6px; }
#backgroundPanel .ldDesignSub { margin-top: 14px; }

/* ---- six more textures (8-per-row grid in the Design panel) ---- */
:root[data-bgpattern="hearts"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M12 20 C3 13 5 6 10 6 C11.5 6 12 7.5 12 8 C12 7.5 12.5 6 14 6 C19 6 21 13 12 20 Z' fill='white'/%3E%3Cpath d='M34 40 C27 34.5 28.5 29 32 29 C33.2 29 34 30 34 30.5 C34 30 34.8 29 36 29 C39.5 29 41 34.5 34 40 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M12 20 C3 13 5 6 10 6 C11.5 6 12 7.5 12 8 C12 7.5 12.5 6 14 6 C19 6 21 13 12 20 Z' fill='white'/%3E%3Cpath d='M34 40 C27 34.5 28.5 29 32 29 C33.2 29 34 30 34 30.5 C34 30 34.8 29 36 29 C39.5 29 41 34.5 34 40 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 44px 44px;
          mask-size: 44px 44px;
}
[data-bgswatch="hearts"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M12 20 C3 13 5 6 10 6 C11.5 6 12 7.5 12 8 C12 7.5 12.5 6 14 6 C19 6 21 13 12 20 Z' fill='white'/%3E%3Cpath d='M34 40 C27 34.5 28.5 29 32 29 C33.2 29 34 30 34 30.5 C34 30 34.8 29 36 29 C39.5 29 41 34.5 34 40 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M12 20 C3 13 5 6 10 6 C11.5 6 12 7.5 12 8 C12 7.5 12.5 6 14 6 C19 6 21 13 12 20 Z' fill='white'/%3E%3Cpath d='M34 40 C27 34.5 28.5 29 32 29 C33.2 29 34 30 34 30.5 C34 30 34.8 29 36 29 C39.5 29 41 34.5 34 40 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 24px 24px;
          mask-size: 24px 24px;
}
:root[data-bgpattern="drops"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M11 4 C15 11 17 14 17 17 A6 6 0 0 1 5 17 C5 14 7 11 11 4 Z' fill='white'/%3E%3Cpath d='M30 24 C32.6 28.5 34 30.5 34 32.5 A4 4 0 0 1 26 32.5 C26 30.5 27.4 28.5 30 24 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M11 4 C15 11 17 14 17 17 A6 6 0 0 1 5 17 C5 14 7 11 11 4 Z' fill='white'/%3E%3Cpath d='M30 24 C32.6 28.5 34 30.5 34 32.5 A4 4 0 0 1 26 32.5 C26 30.5 27.4 28.5 30 24 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 40px 40px;
          mask-size: 40px 40px;
}
[data-bgswatch="drops"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M11 4 C15 11 17 14 17 17 A6 6 0 0 1 5 17 C5 14 7 11 11 4 Z' fill='white'/%3E%3Cpath d='M30 24 C32.6 28.5 34 30.5 34 32.5 A4 4 0 0 1 26 32.5 C26 30.5 27.4 28.5 30 24 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M11 4 C15 11 17 14 17 17 A6 6 0 0 1 5 17 C5 14 7 11 11 4 Z' fill='white'/%3E%3Cpath d='M30 24 C32.6 28.5 34 30.5 34 32.5 A4 4 0 0 1 26 32.5 C26 30.5 27.4 28.5 30 24 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 22px 22px;
          mask-size: 22px 22px;
}
:root[data-bgpattern="crosses"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28'%3E%3Cpath d='M8 8L20 20M20 8L8 20' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28'%3E%3Cpath d='M8 8L20 20M20 8L8 20' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  -webkit-mask-size: 28px 28px;
          mask-size: 28px 28px;
}
[data-bgswatch="crosses"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28'%3E%3Cpath d='M8 8L20 20M20 8L8 20' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28'%3E%3Cpath d='M8 8L20 20M20 8L8 20' stroke='white' stroke-width='2' stroke-linecap='round'/%3E%3C/svg%3E");
  -webkit-mask-size: 16px 16px;
          mask-size: 16px 16px;
}
:root[data-bgpattern="diamonds"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M10 3L16 11L10 19L4 11Z' fill='white'/%3E%3Cpath d='M30 22L34 28L30 34L26 28Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M10 3L16 11L10 19L4 11Z' fill='white'/%3E%3Cpath d='M30 22L34 28L30 34L26 28Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 40px 40px;
          mask-size: 40px 40px;
}
[data-bgswatch="diamonds"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M10 3L16 11L10 19L4 11Z' fill='white'/%3E%3Cpath d='M30 22L34 28L30 34L26 28Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='40' height='40'%3E%3Cpath d='M10 3L16 11L10 19L4 11Z' fill='white'/%3E%3Cpath d='M30 22L34 28L30 34L26 28Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 22px 22px;
          mask-size: 22px 22px;
}
:root[data-bgpattern="moons"] #bgPattern {
  background-color: color-mix(in srgb, var(--accent) 14%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M20 3 A12 12 0 0 0 20 27 A7 12 0 0 1 20 3 Z' fill='white'/%3E%3Cpath d='M38 28 A7 7 0 0 0 38 42 A4 7 0 0 1 38 28 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M20 3 A12 12 0 0 0 20 27 A7 12 0 0 1 20 3 Z' fill='white'/%3E%3Cpath d='M38 28 A7 7 0 0 0 38 42 A4 7 0 0 1 38 28 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 44px 44px;
          mask-size: 44px 44px;
}
[data-bgswatch="moons"] {
  background-color: color-mix(in srgb, var(--accent) 60%, transparent);
  -webkit-mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M20 3 A12 12 0 0 0 20 27 A7 12 0 0 1 20 3 Z' fill='white'/%3E%3Cpath d='M38 28 A7 7 0 0 0 38 42 A4 7 0 0 1 38 28 Z' fill='white'/%3E%3C/svg%3E");
          mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='44' height='44'%3E%3Cpath d='M20 3 A12 12 0 0 0 20 27 A7 12 0 0 1 20 3 Z' fill='white'/%3E%3Cpath d='M38 28 A7 7 0 0 0 38 42 A4 7 0 0 1 38 28 Z' fill='white'/%3E%3C/svg%3E");
  -webkit-mask-size: 24px 24px;
          mask-size: 24px 24px;
}
:root[data-bgpattern="pinstripes"] #bgPattern {
  background-image: repeating-linear-gradient(0deg, color-mix(in srgb, var(--accent) 12%, transparent) 0 1px, transparent 1px 9px);
}
[data-bgswatch="pinstripes"] {
  background-image: repeating-linear-gradient(0deg, color-mix(in srgb, var(--accent) 55%, transparent) 0 1.2px, transparent 1.2px 5px);
}
/* Design panel grids: every section lays out 8 to a row. */
#backgroundPanel .ldSlotStrip { display: grid; grid-template-columns: repeat(8, 22px); gap: 12px; }
#backgroundPanel .ldAnimToggle { padding: 8px 10px; }

/* Tales, side by side: a two-row grid (heading row, text row) instead of two
   independent columns, so the first paragraph starts on the same horizontal
   level on both sides however many lines either title wraps to. The column
   wrappers dissolve (display: contents) and their children are placed by hand;
   the divider moves onto the right-hand cells. */
@media (min-width: 901px) {
  .mTaleCols.side {
    display: grid;
    grid-template-columns: 1fr 1fr;
    grid-template-rows: auto 1fr;
    column-gap: 20px;
    row-gap: 0;
    align-items: start;
  }
  .mTaleCols.side .mTaleOrigCol,
  .mTaleCols.side .mTaleTrCol { display: contents; }
  .mTaleCols.side .mTaleOrigCol > .mSheetTitle { grid-column: 1; grid-row: 1; }
  .mTaleCols.side .mTaleOrigCol > .mTaleBody { grid-column: 1; grid-row: 2; }
  .mTaleCols.side .mTaleTrCol > .mTaleTrTitleRow,
  .mTaleCols.side .mTaleTrCol > .mSheetTitle,
  .mTaleCols.side .mTaleTrCol > .mTaleTrTabs,
  .mTaleCols.side .mTaleTrCol > .mTaleTrLabel {
    grid-column: 2; grid-row: 1;
    border-left: 1px solid var(--border);
    padding-left: 20px;
    margin-bottom: 0;
  }
  .mTaleCols.side .mTaleTrCol > .mTaleBody,
  .mTaleCols.side .mTaleTrCol > .mTaleTrPending {
    grid-column: 2; grid-row: 2;
    align-self: stretch;
    border-left: 1px solid var(--border);
    padding-left: 20px;
  }
}

.btnTxt { position: relative; display: inline-block; --ul: var(--accent); }
#btnUnknown .btnTxt { --ul: var(--bad); }
#btnUnknown.verdict-warn .btnTxt { --ul: #d97706; }
#btnUnknown.verdict-safe .btnTxt { --ul: var(--accent); }
.btnTxt::before, .btnTxt::after {
  content: "";
  position: absolute;
  left: 0; right: 0;
  height: 14px;
  background-color: var(--ul);
  pointer-events: none;
  -webkit-mask-image: var(--pencil-underline);
          mask-image: var(--pencil-underline);
  -webkit-mask-repeat: repeat-x;
          mask-repeat: repeat-x;
  -webkit-mask-size: 36px 14px;
          mask-size: 36px 14px;
}
.btnTxt::before { bottom: -10px; }
.btnTxt::after { bottom: -13px; left: 5px; right: -3px; opacity: 0.75; -webkit-mask-position: 11px 0; mask-position: 11px 0; }

/* Media topics filter: sits under the search bar, after the level chips. */
/* Level chips and the Topics dropdown share one row. */
#mediaFilterRow { display: flex; align-items: center; gap: 12px; }
#mediaFilterRow > .mScopes { flex: 0 1 auto; min-width: 0; }
#mediaFilterRow > #mediaTopicDrop { margin: 0; flex: 0 0 auto; }
#mediaFilterRow > #mediaTopicDrop:not(.hidden) { display: block; width: max-content; }

/* Secondary (other-language) translations read as muted. */
.trRow.trRowOther > span.trPlain { color: var(--muted); }


/* Grammar main section: flag tabs pinned to the card's top-right. */
.gTopic .gIntroTabs { position: absolute; top: 26px; right: 18px; margin: 0; }
.gTopic.hasFlagTabs h2 { padding-right: 190px; }
@media (max-width: 640px) {
  .gTopic.hasFlagTabs h2 { padding-right: 0; }
  .gTopic .gIntroTabs { position: static; margin: 6px 0 0; }
}

/* Study-from cap reached: the languages that can no longer be added are muted. */
.ldOpt.capped { opacity: 0.45; }
.ldOpt.capped .ldEndo { color: var(--muted); }

/* Revealed Cards card whose prompt was a translation: that translation now
   leads the translations list, so the big copy on top goes away. */
#card.prompt-merged:not(.answer-hidden) #word { display: none; }

/* ---- Word graphs (Media) -------------------------------------------------
   Landing page: a search box and the list of graphs. Opening one covers the
   the Media panel (.wgPanel); words are HTML so the Word-family card's own
   .segWord/.seg-* underlines and colours apply unchanged, edges are an SVG
   under them in the same --seg-* palette. */
#mediaBody.wgBody { align-items: stretch; justify-content: flex-start; }
.wgHome { width: 100%; margin: 0 auto; padding: 4px 4px 40px; box-sizing: border-box; }
.wgSearch { display: block; position: sticky; top: 0; z-index: 2; padding: 6px 0 12px; }
.wgSearchInput {
  width: 100%; box-sizing: border-box; font: inherit; font-size: 1.05rem;
  padding: 12px 16px; border-radius: var(--radius-sm); border: 1px solid var(--border);
  background: var(--card-bg); color: var(--text);
}
.wgSearchInput:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
.wgFamGrid { display: grid; grid-template-columns: repeat(auto-fill, minmax(96px, 1fr)); gap: 8px; }
.wgFam {
  display: flex; align-items: baseline; justify-content: space-between; gap: 8px;
  padding: 7px 10px; border-radius: var(--radius-sm); border: 1px solid var(--border);
  background: var(--card-bg); color: var(--text); font: inherit; cursor: pointer; text-align: start;
}
.wgFam:hover { border-color: var(--accent); }
.wgFamStem { font-size: 0.95rem; font-weight: 400; }
.wgFamN { font-size: 0.68rem; color: var(--muted); }
.wgHit {
  display: grid; grid-template-columns: auto auto 1fr auto; align-items: baseline; gap: 4px 10px;
  width: 100%; padding: 11px 14px; margin-bottom: 8px; border-radius: var(--radius-sm);
  border: 1px solid var(--border); background: var(--card-bg); color: var(--text);
  font: inherit; cursor: pointer; text-align: start;
}
.wgHit:hover { border-color: var(--accent); }
.wgHitWord { font-weight: 400; font-size: 1.05rem; }
.wgHitGloss { color: var(--muted); font-size: 0.9rem; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.wgHitStem { color: var(--muted); font-size: 0.8rem; }
.wgChip { font-size: 0.68rem; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--border); color: var(--muted); }

/* the graph itself: inside the Media panel, on the page's own background.
   While a graph is open #media stops scrolling and hands its whole height to
   the panel (same trick as the songbook frame). */
#media.wgOpen { overflow: hidden; padding-bottom: 12px; }
#media.wgOpen #mediaHead { max-width: none; }
#media.wgOpen .mLayout { flex: 1 1 auto; min-height: 0; max-width: none; display: flex; flex-direction: column; }
#media.wgOpen #mediaBody { flex: 1 1 auto; min-height: 0; overflow: hidden; display: flex; flex-direction: column; }
.wgPanel { flex: 1 1 auto; min-height: 0; display: flex; flex-direction: column; width: 100%; }
#wgTopTitle { font-size: 1rem; font-weight: 400; white-space: nowrap; align-self: center; }
#wgTopTitle.hidden, #wgBackWrap.hidden { display: none; }
#wgBackWrap { display: inline-flex; align-items: center; }
#wgBack { display: inline-flex; align-items: center; justify-content: center; height: 34px; width: 28px; padding: 0; border: 0; background: transparent; color: var(--text); cursor: pointer; }
#wgBack:hover { color: var(--accent); }
#wgBack svg { display: block; width: 18px; height: 18px; }
.wgDock {
  position: absolute; left: 0; right: 0; bottom: 0; z-index: 3; pointer-events: none;
  display: flex; flex-wrap: wrap-reverse; align-items: flex-end; justify-content: space-between; gap: 8px;
  padding: 0 12px 10px;
}
.wgDock > * { pointer-events: auto; }
.wgLegendHost { margin-inline-start: auto; background: var(--card-bg); padding: 6px 10px; }
.wgLegendHost:empty { display: none; }
.wgLegendHost .segLegend { margin-top: 0; font-size: 0.74rem; flex-direction: column; gap: 3px; }
.wgViewport {
  position: relative; flex: 1 1 auto; min-height: 240px; overflow: hidden; touch-action: none;
  cursor: grab; user-select: none; -webkit-user-select: none;
}
.wgViewport.wgDrag { cursor: grabbing; }
.wgWorld { position: absolute; left: 0; top: 0; width: 0; height: 0; transform-origin: 0 0; }
.wgEdges { position: absolute; left: 0; top: 0; overflow: visible; pointer-events: none; }
.wgPipesG { opacity: 0.5; }
.wgPipesX { display: none; }
.wgPipe { fill: none; stroke: var(--muted); stroke-width: calc(1.5px * var(--wgi, 1)); stroke-linecap: round; stroke-linejoin: round; }
.wgPipe.wgOff { display: none; }
.wgPipe.wgPipe2 { stroke-width: calc(1px * var(--wgi, 1)); opacity: 0.7; }
.wgEdgePath.wgTick2 { stroke-width: calc(1.4px * var(--wgi, 1)); opacity: 0.7; }
.wgEdgePath { fill: none; stroke: currentColor; stroke-width: calc(2.2px * var(--wgi, 1)); stroke-linecap: round; stroke-linejoin: round; }
.wgEdgePath.wgDash { stroke-dasharray: calc(8px * var(--wgi, 1)) calc(8px * var(--wgi, 1)); }
.wgEdgePath.seg-r { color: var(--muted); }
.wgHi { display: none; }
.wgEdge.wgOn .wgHi { display: block; stroke-width: calc(2.8px * var(--wgi, 1)); }
.wgHasSel .wgEdge:not(.wgOn) { opacity: 0.35; }
.wgEdgeLabels, .wgNodes { position: absolute; left: 0; top: 0; width: 0; height: 0; }
.wgEdgeLabel {
  position: absolute; transform: translate(-50%, -50%); font-size: 12px; font-weight: 600; max-width: 96px; overflow: hidden; text-overflow: ellipsis;
  padding: 0 4px; border-radius: 6px; background: var(--page-bg, var(--bg)); white-space: nowrap; pointer-events: none;
  opacity: 0.9;
}
.wgHasSel .wgEdgeLabel:not(.wgOn) { opacity: 0.18; }
.wgFar .wgEdgeLabel { display: none; }
.wgNode {
  position: absolute; box-sizing: border-box; padding: 12px 14px 12px; white-space: nowrap;
  font-size: 17px; line-height: 1.25; border-radius: 12px; border: 1px solid var(--border);
  background: var(--card-bg); cursor: pointer; box-shadow: 0 1px 4px rgba(0, 0, 0, 0.08);
}
.wgNode:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
.wgNode .wgArt { color: var(--muted); font-size: 0.82em; }
.wgNode .segDot { display: none; }
.wgArch { opacity: 0.72; border-style: dashed; }
.wgHasSel .wgNode:not(.wgOn) { opacity: 0.35; }
.wgNode.wgSel { background: var(--card-bg); box-shadow: 0 0 0 2px var(--accent); }
.wgNode.wgOn:not(.wgSel) { background: color-mix(in srgb, var(--card-bg) 70%, transparent); }
.wgStem { padding: 4px 10px 22px; font-size: 24px; font-weight: 700; background: transparent; border: 0; box-shadow: none; }
.wgStem .btnTxt { --ul: var(--text); }
.wgAxis { stroke: var(--border); stroke-width: 1.5px; stroke-dasharray: 6 8; vector-effect: non-scaling-stroke; }
.wgCorner {
  position: absolute; font-size: 15px; white-space: nowrap; letter-spacing: 0.04em; text-transform: uppercase;
  color: var(--muted); opacity: 0.7; pointer-events: none;
}
.wgZoom { position: absolute; right: 12px; top: 50%; transform: translateY(-50%); display: flex; flex-direction: column; gap: 6px; z-index: 2; }
.wgZoomBtn { display: inline-flex; align-items: center; justify-content: center; padding: 0; width: 38px; height: 38px; font-size: 1.25rem; }
.wgZoomBtn svg { display: block; }
.wgDetail {
  flex: 0 1 560px; min-width: 0; cursor: default; padding: 12px 16px 14px;
  border: 1px solid var(--border); background: var(--card-bg);
  max-height: 34vh; overflow: auto; user-select: text; -webkit-user-select: text;
}
.wgDetail[hidden] { display: none; }
.wgDetailTop { display: flex; align-items: center; gap: 10px; font-size: 1.35rem; font-weight: 600; }
.wgDetailGloss { margin-top: 4px; font-size: 1.02rem; }
.wgDetailHint { margin-top: 2px; font-size: 0.85rem; color: var(--muted); }
.wgDetailFrom { margin-top: 8px; font-size: 0.9rem; color: var(--muted); display: flex; align-items: center; flex-wrap: wrap; gap: 4px; }
.wgFromBtn { font: inherit; padding: 2px 8px; border-radius: 8px; border: 1px solid var(--border); background: transparent; color: var(--text); cursor: pointer; }
.wgArrow { margin: 0 4px; }
.wgFromMe { color: var(--text); font-weight: 600; }

.wgMeasure { visibility: hidden; }
/* book icon (Word-family preview popover) -- only once zoomed in */
.wgNode .wgBook { display: none; margin-inline-start: 4px; }
.wgZoomed .wgNode .wgBook { display: inline-flex; vertical-align: middle; margin-top: -8px; margin-bottom: -8px; }   /* never taller than the text line, so words do not shift when it appears */

/* legend key: fold-away button (phones only) */
.wgLegendBtn { display: none; }
@media (max-width: 700px) {
  /* controls sit in a row along the bottom edge, the key folds behind a button */
  .wgZoom { top: auto; bottom: 10px; left: 12px; right: auto; transform: none; flex-direction: row; }
  .wgDock { padding-bottom: 56px; }
  .wgLegendHost { position: absolute; right: 12px; bottom: 10px; margin: 0; padding: 0; background: transparent; display: flex; flex-direction: column-reverse; align-items: flex-end; gap: 6px; }
  .wgLegendBtn {
    display: inline-flex; align-items: center; justify-content: center; padding: 0;
    width: 36px; height: 36px;
  }
  .wgLegendHost .segLegend { display: none; background: var(--card-bg); padding: 6px 10px; }
  .wgLegendHost.wgLgOpen .segLegend { display: flex; }
}

/* names the outside parent of a trunk of cross-block words; click selects it */
.wgCableChip {
  position: absolute; transform: translate(-50%, -100%) scale(var(--wgi, 1)); transform-origin: 50% 100%;
  padding: 0 2px; border: 0; background: transparent; color: var(--muted);
  font: inherit; font-size: 12px; white-space: nowrap; cursor: pointer;
}
.wgCableChip:hover { color: var(--text); text-decoration: underline; }

/* legend swatches: the sketched stroke, drawn tall enough to read as a bold line */
.wgLegendHost .segSwatch { width: 32px; height: 8px; -webkit-mask-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%228%22%20viewBox%3D%220%200%2040%208%22%3E%3Cpath%20d%3D%22M0%204.00%20Q11.20%204.00%2020.00%203.17%20Q28.80%204.00%2040.00%204.00%22%20stroke%3D%22%23fff%22%20stroke-width%3D%223.4%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22/%3E%3Cpath%20d%3D%22M0%204.00%20Q11.20%204.00%2020.00%203.17%20Q28.80%204.00%2040.00%204.00%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222.4%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20opacity%3D%220.7%22%20transform%3D%22translate%280%200.6%29%22/%3E%3C/svg%3E'); mask-image: url('data:image/svg+xml,%3Csvg%20xmlns%3D%22http://www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%228%22%20viewBox%3D%220%200%2040%208%22%3E%3Cpath%20d%3D%22M0%204.00%20Q11.20%204.00%2020.00%203.17%20Q28.80%204.00%2040.00%204.00%22%20stroke%3D%22%23fff%22%20stroke-width%3D%223.4%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22/%3E%3Cpath%20d%3D%22M0%204.00%20Q11.20%204.00%2020.00%203.17%20Q28.80%204.00%2040.00%204.00%22%20stroke%3D%22%23fff%22%20stroke-width%3D%222.4%22%20fill%3D%22none%22%20stroke-linecap%3D%22round%22%20opacity%3D%220.7%22%20transform%3D%22translate%280%200.6%29%22/%3E%3C/svg%3E'); -webkit-mask-size: 100% 100%; mask-size: 100% 100%; }

/* sketched tick / cross / warning glyphs (glyphIcon): ride the text line, a touch bolder than the scribble default */
.gIcon { display: inline-block; vertical-align: -0.14em; width: 1.05em; height: 1.05em; }
.gIcon g { stroke-width: 1.9; }

/* phone vertical tree: edge labels sit in a short stub, keep them small */
.wgOutline .wgEdgeLabel { max-width: 62px; font-size: 11px; }
