/* css/style.css */
/* ============================================================
   Deutsch für Tanya

   THEMES. Five palettes; the picker in the header sets
   data-theme on <html> and js/theme.js remembers the choice.

   To add one, copy a block below, change the name in the
   selector, and add that name to THEMES in js/theme.js.
   Every theme must set the whole list — a missing var falls
   back to Sand and the result looks broken, not merely off.
   ============================================================ */

:root{
  --sand:#FCD0A1;              --sand-rgb:252 208 161;
  --sand-deep:#F7BE83;         --sand-wash:#FAEFE3;
  --flame:#F15025;             --flame-rgb:241 80 37;
  --flame-dark:#C63C17;        --flame-pale:#FFB59C;   --on-flame:#2B1B17;
  --mauve:#A690A4;             --mauve-rgb:166 144 164;
  --liver:#5E4B56;             --liver-rgb:94 75 86;   --liver-deep:#4A3A44;
  --mint:#BAFFDF;

  /* Right and wrong are not theme accents. --mint is a decorative colour
     and lands on orange or cyan in some themes, which meant 'Correct!'
     could appear in whatever hue the palette happened to use. Green means
     right and red means wrong in every theme, no exceptions — that reading
     is universal and a theme should not be able to override it.

     Two shades of each: the bright one for text and borders on the dark
     cards, the deep one for text on paper. */
  /* Approved inks, by the surface they sit on. See the table above the
     surfaces block. Each points at a token every theme redefines, so one
     declaration here is correct in all nine.

       ink-*     the darkest, for anything that must be read
       soft-*    secondary lines, still above the floor
       accent-*  emphasis. NOT --flame or --flame-dark; both fail. */
  --ink-on-light:    var(--ink);          /* 11.79 worst */
  --soft-on-light:   var(--ink-soft);     /*  5.42 worst */
  --accent-on-light: var(--ink);          /* flame-dark is 2.95 — banned */
  --ink-on-dark:     var(--on-dark);      /*  8.09 worst */
  --soft-on-dark:    var(--sand);         /*  6.19 worst */
  --accent-on-dark:  var(--flame-pale);   /*  5.46 worst; flame is 3.33 */
  --ink-on-accent:   var(--on-flame);     /*  4.65 worst */
  --ink-on-sand:     var(--liver-deep);   /*  7.41 worst — --sand is a light
                                             chip in every theme, including
                                             the dark ones where it doubles
                                             as the soft ink, so --ink is
                                             wrong on it (1.54 in ember) */

  /* Grammatical gender, colour-coded. Tokens rather than literals so the
     dark themes can lift them; a fixed mid-tone cannot serve both. */
  --gen-m:#285F90;             --gen-f:#97385B;        --gen-n:#43592B;
  /* MEASURED, NOT PICKED BY EYE. Contrast against the off-white strip
     (#FAF7F2), WCAG AA for text needs 4.5:
         blue   #2B6CB0  5.07   Grok's value, and it passes
         orange #A85A0B  4.76   Grok's #DD6B20 was 3.17 and FAILED
         green  #2F855A  4.25 FAILED -> #2E7D4F 4.72
     Orange on a light background is the hard one: both the value Grok
     proposed and my first attempt (#C0700F, 3.53) were unreadable by
     the standard. #A85A0B is a deeper amber that clears it and still
     reads unmistakably orange — and being browner than --flame
     (#F15025) is a bonus, since flame is the app accent in 133 rules. */
  --role-subj:#2B6CB0;         --role-verb:#A85A0B;    --role-obj:#2E7D4F;
  --role-adj:#7A4BA8;          --role-adv:#0F7C82;
  --mark-neg:#B3243C;          --mark-agree:#2E7D4F;
  --role-subj-rgb:43 108 176;      --role-verb-rgb:168 90 11;
  --role-obj-rgb:46 125 79;       --role-adj-rgb:122 75 168;
  --role-adv-rgb:15 124 130;
  --mark-neg-rgb:179 36 60;       --mark-agree-rgb:46 125 79;
  --strip-bg:#FAF7F2;          --strip-ink:#2B2228;    --strip-edge:#E3DAD0;

  /* --shade-rgb does two different jobs and they disagree in a dark theme.
     As an inset tint it must lighten a dark panel, so it flips to white.
     As the scrim behind a modal, or as a shadow, it must stay black — or
     opening 'How to play' at night floodlights the page. This is the
     second job, and it never flips. */
  --scrim-rgb:0 0 0;

  --yes:#6FE39B;               --yes-rgb:111 227 155;
  --yes-deep:#1D7A45;          --on-yes:#0E2E1B;
  --no:#FF8B7B;                --no-rgb:255 139 123;
  --no-deep:#B32A16;           --on-no:#3A0F09;

  --page:#D5C8BA;              --paper:#FFF5E8;      --paper-rgb:255 245 232;
  --ink:#3C2F36;               --ink-rgb:60 47 54;      --ink-soft:#6B5860;
  --on-dark:#F9EDDF;           --shade-rgb:0 0 0;

  --r-sm:8px;
  --r-md:14px;
  --r-lg:22px;

  --shadow-card:0 2px 0 var(--sand-deep), 0 12px 28px -18px rgb(var(--ink-rgb) / .55);
  --shadow-lift:0 3px 0 var(--sand-deep), 0 18px 34px -18px rgb(var(--ink-rgb) / .5);

  --ff-display:'Lora', Georgia, 'Times New Roman', serif;
  --ff-ui:'Manrope', system-ui, -apple-system, 'Segoe UI', sans-serif;
}

/* --- themes ------------------------------------------------ */
html[data-theme="mint"]{
  --sand:#BAFFDF;              --sand-rgb:186 255 223;
  --sand-deep:#86DDB6;         --sand-wash:#E3FAED;
  --flame:#F15025;             --flame-rgb:241 80 37;
  --flame-dark:#C63C17;        --flame-pale:#FFB59C;   --on-flame:#2B1B17;
  --mauve:#8CA79B;             --mauve-rgb:140 167 155;
  --liver:#4A5952;             --liver-rgb:74 89 82;   --liver-deep:#37433D;
  --mint:#FCD0A1;
  --page:#BFD7C9;              --paper:#E8FFF4;      --paper-rgb:232 255 244;
  --ink:#27332D;               --ink-rgb:39 51 45;      --ink-soft:#52645B;
  --on-dark:#D9FFEA;           --shade-rgb:0 0 0;
}

html[data-theme="plum"]{
  --sand:#DCC9D8;              --sand-rgb:220 201 216;
  --sand-deep:#C0A5BB;         --sand-wash:#FAE3F5;
  --flame:#F15025;             --flame-rgb:241 80 37;
  --flame-dark:#C63C17;        --flame-pale:#FFB59C;   --on-flame:#2B1B17;
  --mauve:#8B7288;             --mauve-rgb:139 114 136;
  --liver:#54404E;             --liver-rgb:84 64 78;   --liver-deep:#3D2E39;
  --mint:#BAFFDF;
  --page:#C6B4C2;              --paper:#FFE8F6;      --paper-rgb:255 232 246;
  --ink:#2E2330;               --ink-rgb:46 35 48;      --ink-soft:#665268;
  --on-dark:#F4E4EF;           --shade-rgb:0 0 0;
}

html[data-theme="orchid"]{
  --sand:#FF9FE5;              --sand-rgb:255 159 229;
  --sand-deep:#FF58D2;         --sand-wash:#FAE3F8;
  --flame:#FF858D;             --flame-rgb:255 133 141;
  --flame-dark:#FF434F;        --flame-pale:#FFB8BC;   --on-flame:#2B1719;
  --mauve:#42769A;             --mauve-rgb:66 118 154;
  --liver:#3C2F3B;             --liver-rgb:60 47 59;   --liver-deep:#251D25;
  --mint:#D2EAB8;
  --page:#D2BCD0;              --paper:#FFE8FD;      --paper-rgb:255 232 253;
  --ink:#2C1C2A;               --ink-rgb:44 28 42;      --ink-soft:#6C5169;
  --on-dark:#F2E6EF;           --shade-rgb:0 0 0;
}

html[data-theme="moss"]{
  --sand:#FF9FE5;              --sand-rgb:255 159 229;
  --sand-deep:#FF58D2;         --sand-wash:#EEFAE3;
  --flame:#FF858D;             --flame-rgb:255 133 141;
  --flame-dark:#FF434F;        --flame-pale:#FFB8BC;   --on-flame:#2B1719;
  --mauve:#42769A;             --mauve-rgb:66 118 154;
  --liver:#364823;             --liver-rgb:54 72 35;   --liver-deep:#212D16;
  --mint:#D2EAB8;
  --page:#C7D2BC;              --paper:#F4FFE8;      --paper-rgb:244 255 232;
  --ink:#242C1C;               --ink-rgb:36 44 28;      --ink-soft:#5F6C51;
  --on-dark:#F2E6EF;           --shade-rgb:0 0 0;
}

html[data-theme="lagoon"]{
  --sand:#E1D89F;              --sand-rgb:225 216 159;
  --sand-deep:#D0C269;         --sand-wash:#E3F8FA;
  --flame:#FF858D;             --flame-rgb:255 133 141;
  --flame-dark:#FF434F;        --flame-pale:#FFB8BC;   --on-flame:#2B1719;
  --mauve:#397D85;             --mauve-rgb:57 125 133;
  --liver:#234548;             --liver-rgb:35 69 72;   --liver-deep:#162B2D;
  --mint:#B8E6EA;
  --page:#BCD0D2;              --paper:#E8FDFF;      --paper-rgb:232 253 255;
  --ink:#1C2A2C;               --ink-rgb:28 42 44;      --ink-soft:#51696C;
  --on-dark:#F2F0E6;           --shade-rgb:0 0 0;
}

html[data-theme="tobacco"]{
  --sand:#E1D89F;              --sand-rgb:225 216 159;
  --sand-deep:#D0C269;         --sand-wash:#FAF7E3;
  --flame:#FF858D;             --flame-rgb:255 133 141;
  --flame-dark:#FF434F;        --flame-pale:#FFB8BC;   --on-flame:#2B1719;
  --mauve:#856F39;             --mauve-rgb:133 111 57;
  --liver:#483E23;             --liver-rgb:72 62 35;   --liver-deep:#2D2616;
  --mint:#B8E6EA;
  --page:#D2CFBC;              --paper:#FFFCE8;      --paper-rgb:255 252 232;
  --ink:#2C271C;               --ink-rgb:44 39 28;      --ink-soft:#6C6451;
  --on-dark:#F2F0E6;           --shade-rgb:0 0 0;
}

html[data-theme="citrus"]{
  --sand:#D4CB92;              --sand-rgb:212 203 146;
  --sand-deep:#C0B35F;         --sand-wash:#FAECE3;
  --flame:#51CB20;             --flame-rgb:81 203 32;
  --flame-dark:#3A9217;        --flame-pale:#8EE76A;   --on-flame:#1D2B17;
  --mauve:#397D85;             --mauve-rgb:57 125 133;
  --liver:#483323;             --liver-rgb:72 51 35;   --liver-deep:#2D2016;
  --mint:#C6EAB8;
  --page:#D2C5BC;              --paper:#FFF1E8;      --paper-rgb:255 241 232;
  --ink:#2C221C;               --ink-rgb:44 34 28;      --ink-soft:#6C5C51;
  --on-dark:#F2F0E6;           --shade-rgb:0 0 0;
}

html[data-theme="grove"]{
  --sand:#D4CB92;              --sand-rgb:212 203 146;
  --sand-deep:#C0B35F;         --sand-wash:#E9FAE3;
  --flame:#F96900;             --flame-rgb:249 105 0;
  --flame-dark:#B74D00;        --flame-pale:#FFA360;   --on-flame:#2B2017;
  --mauve:#856F39;             --mauve-rgb:133 111 57;
  --liver:#2E4823;             --liver-rgb:46 72 35;   --liver-deep:#1C2D16;
  --mint:#C6EAB8;
  --page:#C2D2BC;              --paper:#EEFFE8;      --paper-rgb:238 255 232;
  --ink:#202C1C;               --ink-rgb:32 44 28;      --ink-soft:#596C51;
  --on-dark:#F2F0E6;           --shade-rgb:0 0 0;
}

/* ------------------------------------------------------------------
   DARK THEMES

   The nine above are light-page. These three invert it: a dark page, a
   darker panel, and light ink. Nothing in the stylesheet needed changing
   for them, which is the point of the alias block in :root — every
   approved pair names a token that a theme redefines, so --ink-on-light
   is 'the ink that goes on --paper' rather than literally a dark colour.
   In a dark theme --paper is a deep tone and --ink is nearly white, and
   the pair still clears the floor.

   --shade-rgb flips to white here: the tints that darken a light card
   have to lighten a dark one, or every inset panel disappears into it.
   ------------------------------------------------------------------ */

html[data-theme="midnight"]{
  --gen-m:#8FB6E8;                --gen-f:#F09CBC;        --gen-n:#9FD8A6;
  --role-subj:#8FC4F0;         --role-verb:#E8A64B;    --role-obj:#7FD3A3;
  --role-adj:#C9A6E8;          --role-adv:#6FD3D8;
  --mark-neg:#FF9AA8;          --mark-agree:#7FD3A3;
  --role-subj-rgb:143 196 240;      --role-verb-rgb:232 166 75;
  --role-obj-rgb:127 211 163;       --role-adj-rgb:201 166 232;
  --role-adv-rgb:111 211 216;
  --mark-neg-rgb:255 154 168;       --mark-agree-rgb:127 211 163;
  --strip-bg:#221C21;          --strip-ink:#F2EBE6;    --strip-edge:#3A3138;
  --sand:#9FB4D8;              --sand-rgb:159 180 216;
  --sand-deep:#6E86AE;         --sand-wash:#1B2233;
  --flame:#FF7A59;             --flame-rgb:255 122 89;
  --flame-dark:#C9502F;        --flame-pale:#FFB49E;   --on-flame:#231009;
  --mauve:#7B7FA8;             --mauve-rgb:123 127 168;
  --liver:#141A28;             --liver-rgb:20 26 40;   --liver-deep:#0E121C;
  --mint:#9FE6C8;
  --page:#0B0E16;              --paper:#161C2A;      --paper-rgb:22 28 42;
  --ink:#EAEFF8;               --ink-rgb:234 239 248;   --ink-soft:#A9B6CC;
  --on-dark:#EAEFF8;           --shade-rgb:255 255 255;
}

html[data-theme="ember"]{
  --gen-m:#93BEE8;                --gen-f:#F5A3B8;        --gen-n:#A9D6A2;
  --role-subj:#8FC4F0;         --role-verb:#E8A64B;    --role-obj:#7FD3A3;
  --role-adj:#C9A6E8;          --role-adv:#6FD3D8;
  --mark-neg:#FF9AA8;          --mark-agree:#7FD3A3;
  --role-subj-rgb:143 196 240;      --role-verb-rgb:232 166 75;
  --role-obj-rgb:127 211 163;       --role-adj-rgb:201 166 232;
  --role-adv-rgb:111 211 216;
  --mark-neg-rgb:255 154 168;       --mark-agree-rgb:127 211 163;
  --strip-bg:#221C21;          --strip-ink:#F2EBE6;    --strip-edge:#3A3138;
  --sand:#E5B98F;              --sand-rgb:229 185 143;
  --sand-deep:#B98A5E;         --sand-wash:#2A1F1A;
  --flame:#FF8A4C;             --flame-rgb:255 138 76;
  --flame-dark:#C25C25;        --flame-pale:#FFC09B;   --on-flame:#261206;
  --mauve:#9C7F72;             --mauve-rgb:156 127 114;
  --liver:#1F1712;             --liver-rgb:31 23 18;   --liver-deep:#150F0B;
  --mint:#B8E6C6;
  --page:#120C09;              --paper:#241A14;      --paper-rgb:36 26 20;
  --ink:#F7EBE0;               --ink-rgb:247 235 224;   --ink-soft:#C3AB99;
  --on-dark:#F7EBE0;           --shade-rgb:255 255 255;
}

html[data-theme="pine"]{
  --gen-m:#93C4EE;                --gen-f:#F2A2BC;        --gen-n:#A8DEB2;
  --role-subj:#8FC4F0;         --role-verb:#E8A64B;    --role-obj:#7FD3A3;
  --role-adj:#C9A6E8;          --role-adv:#6FD3D8;
  --mark-neg:#FF9AA8;          --mark-agree:#7FD3A3;
  --role-subj-rgb:143 196 240;      --role-verb-rgb:232 166 75;
  --role-obj-rgb:127 211 163;       --role-adj-rgb:201 166 232;
  --role-adv-rgb:111 211 216;
  --mark-neg-rgb:255 154 168;       --mark-agree-rgb:127 211 163;
  --strip-bg:#221C21;          --strip-ink:#F2EBE6;    --strip-edge:#3A3138;
  --sand:#A9C8A6;              --sand-rgb:169 200 166;
  --sand-deep:#7CA178;         --sand-wash:#18261C;
  --flame:#FFA24C;             --flame-rgb:255 162 76;
  --flame-dark:#C2702A;        --flame-pale:#FFCB99;   --on-flame:#221405;
  --mauve:#7E9481;             --mauve-rgb:126 148 129;
  --liver:#101B14;             --liver-rgb:16 27 20;   --liver-deep:#0A120D;
  --mint:#BCEBCB;
  --page:#080F0A;              --paper:#142117;      --paper-rgb:20 33 23;
  --ink:#E9F4EB;               --ink-rgb:233 244 235;   --ink-soft:#A7BFAC;
  --on-dark:#E9F4EB;           --shade-rgb:255 255 255;
}





/* ============================================================
   TYPE TABLE AND CONTEXTUAL INK
   ============================================================

   Two problems this fixes, both caused by the same thing: a
   component deciding for itself.

   1. INVISIBLE TEXT. The surface table below the themes was
      right, and 156 declarations went around it and named
      --ink, --on-dark or --sand at the point of use. A token
      chosen for a light card is wrong on a dark one, so the
      same declaration produced dark-on-plum in one place and
      cream-on-beige in another.

      Fixed by making the ink CONTEXTUAL. Every surface sets
      --fg, --fg-soft and --fg-accent for what is inside it.
      A component writes color:var(--fg) and is correct on any
      surface, in any theme, without knowing which it is on.

      The old --ink-on-light family still resolves and is kept
      so nothing breaks, but it is no longer the thing to
      reach for. --fg is.

   2. FONT DRIFT. 221 font shorthands and 97 font-family
      declarations, each an independent choice. That is why
      "Section 1 · Sentences" is serif and "Unit 1 · 10 comics"
      is sans — same role, two answers, decided months apart.

      Fixed by a table of ROLES. A component picks a role, not
      a font. Eight roles, and every screen's text is one of
      them.

   RULE FROM HERE: no component may name a colour or a font
   family. It names a role and a surface. If something does not
   fit a role, add a role here and measure it, do not invent
   one inline.
   ------------------------------------------------------------ */

:root{
  /* --- the eight roles -------------------------------------
     Values are the shorthand: weight, size / line-height, family.
     Tracking is separate because the font shorthand cannot hold it.

       t-title    the one big heading on a screen
       t-section  a band heading inside a screen
       t-card     a card or list-row title
       t-sub      the line under a title. NOT a caption size.
       t-eyebrow  the uppercase label above a block
       t-body     everything else that is read
       t-de       German being taught. Its own role, deliberately:
                  it must be the most legible thing on the screen
                  and it must never shrink to fit a layout.
       t-btn      anything you press
     -------------------------------------------------------- */
  --t-title:   700 clamp(1.7rem,6vw,2.4rem)/1.15 var(--ff-display);
  --t-title-ls:-.01em;

  --t-section: 700 clamp(1.15rem,4vw,1.4rem)/1.25 var(--ff-display);
  --t-section-ls:0;

  --t-card:    600 1.08rem/1.3 var(--ff-ui);
  --t-card-ls: 0;

  --t-sub:     400 1rem/1.45 var(--ff-ui);
  --t-sub-ls:  0;

  --t-eyebrow: 700 .82rem/1.2 var(--ff-ui);
  --t-eyebrow-ls:.11em;

  --t-body:    400 1rem/1.55 var(--ff-ui);
  --t-body-ls: 0;

  --t-de:      500 clamp(1.45rem,5.2vw,2.1rem)/1.4 var(--ff-display);
  --t-de-ls:   0;

  --t-btn:     700 1rem/1 var(--ff-ui);
  --t-btn-ls:  .01em;
}

/* --- type variants ------------------------------------------
   Set data-type on <html>, the way data-theme is set. Four
   variants x twelve palettes = forty-eight combinations, and
   none of them can produce a contrast failure, because the
   ink is contextual and the variant only moves family and
   scale.

   These work on the WHOLE stylesheet, not just the role
   classes below. They redefine --ff-display, --ff-ui and the
   root font size, which every one of the 318 existing type
   declarations already reads. So a variant changes every
   screen without a single per-component edit, and without
   moving any size relative to any other.

     editorial  the default. Lora headings, Manrope for
                everything else.
     plain      no serif anywhere. For small screens, and for
                anyone who finds a serif harder at 15px.
     large      editorial at 110%. Every rem scales together,
                so nothing reflows unevenly.
     loud       plain at 112%, with the soft ink lifted to
                full ink so secondary text stops being
                secondary. For low light, or across a room.
   ----------------------------------------------------------- */

html[data-type="plain"]{
  --ff-display:'Manrope', system-ui, -apple-system, 'Segoe UI', sans-serif;
}

html[data-type="large"]{ font-size:110%; }

html[data-type="loud"]{
  --ff-display:'Manrope', system-ui, -apple-system, 'Segoe UI', sans-serif;
  font-size:112%;
}

/* The role table, restated per variant where the role itself
   should change shape rather than just scale. */
html[data-type="plain"],
html[data-type="loud"]{
  --t-title:   800 clamp(1.65rem,5.6vw,2.25rem)/1.2 var(--ff-ui);
  --t-title-ls:-.02em;
  --t-section: 700 clamp(1.1rem,3.8vw,1.32rem)/1.3 var(--ff-ui);
  --t-section-ls:-.01em;
  --t-body:    450 1rem/1.6 var(--ff-ui);
  /* the German stays serif in every variant. It is the material,
     not the furniture, and should not look like the app. */
  --t-de:      500 clamp(1.45rem,5.2vw,2.1rem)/1.4 'Lora', Georgia, serif;
}

/* --- role classes -------------------------------------------
   For markup to use directly. Colour comes from the surface,
   never from these.
   ----------------------------------------------------------- */
.t-title  { font:var(--t-title);   letter-spacing:var(--t-title-ls) }
.t-section{ font:var(--t-section); letter-spacing:var(--t-section-ls) }
.t-card   { font:var(--t-card);    letter-spacing:var(--t-card-ls) }
.t-sub    { font:var(--t-sub);     letter-spacing:var(--t-sub-ls) }
.t-eyebrow{ font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
            text-transform:uppercase }
.t-body   { font:var(--t-body);    letter-spacing:var(--t-body-ls) }
.t-de     { font:var(--t-de);      letter-spacing:var(--t-de-ls) }
.t-btn    { font:var(--t-btn);     letter-spacing:var(--t-btn-ls) }

/* --- contextual ink -----------------------------------------
   The page itself. --ink flips light in the three dark themes,
   so this single pairing is correct in all twelve.
   ----------------------------------------------------------- */
:root{
  --fg:       var(--ink);
  --fg-soft:  var(--ink-soft);
  --fg-accent:var(--ink);
}

/* Light surfaces. Same pairing as the page, restated so a
   nested surface cannot inherit a dark one by accident. */
.surf-paper,
.surf-wash{
  --fg:       var(--ink);
  --fg-soft:  var(--ink-soft);
  --fg-accent:var(--ink);
}

/* Dark surfaces. --flame is banned as text at 3.33; the pale
   is 5.46 worst case and is the only accent allowed here. */
.surf-deep,
.card,
.pg-recent,
.lp-clock-bar,
button.ref-group.open{
  --fg:       var(--on-dark);
  --fg-soft:  var(--sand);
  --fg-accent:var(--flame-pale);
}

/* The accent fill. Its own ink, and no soft tier — a muted
   tone on flame does not clear the floor in any theme, so
   secondary text simply may not go here. */
.surf-accent{
  --fg:       var(--on-flame);
  --fg-soft:  var(--on-flame);
  --fg-accent:var(--on-flame);
}

/* .card was a dark gradient panel that never declared an ink,
   so everything inside it inherited the page's dark text and
   vanished. It is in the dark list above now. This is the fix
   for the hub hero and the comic reader panel. */
.card{ color:var(--fg) }
.surf-deep{ color:var(--fg) }

/* The loud variant lifts secondary text to primary. Per surface,
   so the dark ones lift to their own ink and not the page's. */
html[data-type="loud"]{ --fg-soft:var(--ink) }
html[data-type="loud"] .surf-paper,
html[data-type="loud"] .surf-wash{ --fg-soft:var(--ink) }
html[data-type="loud"] .surf-deep,
html[data-type="loud"] .card,
html[data-type="loud"] .pg-recent,
html[data-type="loud"] .lp-clock-bar,
html[data-type="loud"] button.ref-group.open{ --fg-soft:var(--on-dark) }

/* --- the type-variant picker in the header ------------------- */
.typeswitch{ display:flex; gap:2px; padding:3px; border-radius:999px;
  background:rgb(var(--shade-rgb) / .18) }
.typeswitch button{
  border:0; background:transparent; color:var(--fg-soft);
  font:700 .8rem/1 var(--ff-ui); letter-spacing:.04em;
  padding:7px 10px; border-radius:999px; cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.typeswitch button[aria-pressed="true"]{
  background:var(--sand); color:var(--ink-on-sand);
}

/* THE BACK-ARROW ROW. Third axis of the same decision, so it is built
   exactly like `.typeswitch` rather than inventing a look — see the note
   in js/back.js. The glyphs run larger than the `Aa` marks because an
   arrow at .8rem is a hairline, and because the swatch here IS the thing
   being chosen: she is judging the mark itself, not a label for it. */
.backswitch{ display:flex; gap:2px; padding:3px; border-radius:999px;
  background:rgb(var(--shade-rgb) / .18) }
.backswitch button{
  border:0; background:transparent; color:var(--fg-soft);
  font:700 1.05rem/1 var(--ff-ui);
  padding:7px 10px; border-radius:999px; cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.backswitch button[aria-pressed="true"]{
  background:var(--sand); color:var(--ink-on-sand);
}
.backswitch button svg{ width:20px; height:20px; display:block }


*{box-sizing:border-box}

html,body{margin:0;padding:0}

body{
  min-height:100%;
  /* The tour highlight (.bt-lit) has a wide glow (box-shadow spread ~22px)
     that, on a target near the right or bottom edge, can extend the
     scrollable area and add a stray scrollbar / black gutter. Steven, 11
     Sep: "put a boundary around the glow so it CANNOT extend past the edges
     of the screen." Clipping horizontal overflow at the root stops the
     sideways bleed without affecting vertical scrolling of real content. */
  overflow-x:clip;
  background:
    radial-gradient(1100px 520px at 82% -12%, rgb(var(--sand-rgb) / .30), transparent 70%),
    radial-gradient(760px 460px at 4% 104%, rgb(var(--mauve-rgb) / .20), transparent 68%),
    var(--page);
  color:var(--fg);
  font-family:var(--ff-ui);
  font-size:17px;
  line-height:1.5;
  -webkit-text-size-adjust:100%;
}

button{font-family:inherit;font-size:inherit;cursor:pointer}

:focus-visible{
  outline:3px solid var(--flame);
  outline-offset:3px;
}

/* ---------- top bar ---------- */

.topbar{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:10px 16px;
  flex-wrap:wrap;
  /* 18px top and bottom was 36px of nothing above a screen whose first
     real thing already sat two thirds of the way down. */
  padding:10px clamp(16px,4vw,40px);
  max-width:900px;
  margin:0 auto;
}

/* ROOM FOR THE BACK BUTTON, WHICH FLOATS OVER THIS HEADER.

   Steven, 09 Sep: "The back button is still over the DE orange thing —
   need to shift it over so the back button does not overlap."

   `.backlink` is `position:fixed` at `top:8px` and `left:clamp(16px,4vw,
   40px)` — pinned to the VIEWPORT, and at the top of the page that band
   is the topbar. So on every screen with a back button it lands squarely
   on `.brand-mark`, the 38px orange DE tile.

   UNCONDITIONAL, AND THAT IS DELIBERATE. `body:has(.backlink)` would
   shift it only where a button exists, and the note further down this
   file records why that was rejected once already: `:has()` is missing
   from Safari before 15.4 and Firefox before 121, and the fallback is the
   collision it was meant to fix. A layout that depends on a recent
   selector is broken on someone's phone.

   So the brand sits 52px in on every screen, hub included. The cost is a
   little air to the left of the logo where no button is; the alternative
   is the logo hidden behind an arrow on all forty sub-screens.

   52px is the measured clearance: the button is 45px wide (17px font,
   19px icon at 1.1em, 13px padding each side) and starts at the same
   `clamp()` the topbar's own padding uses, so its left edge and the
   brand's are already aligned. */
.brand{display:flex;align-items:center;gap:10px;min-width:0;padding-left:52px}
@media (min-width:980px){
  /* From here `main` and the topbar are centred at 900px, so the
     viewport-anchored button sits outside them and the gap is for
     nothing. Same breakpoint as `.practice-title`. */
  .brand{ padding-left:0 }
}

/* THE LANGUAGE SWITCH, which used to be a static logo tile.

   See the note by `paintLang` in app.js. Two stacked lines now: the
   language she is LEARNING on top, her own underneath.

   42x42, back near the original 38. `langShort` is capped at three
   characters in every language now, so the widest stacked pair is three
   over three and the badge no longer has to be wide enough for "Engl."
   over "Russ." — see the note by `langShort_de` in i18n.js.

   It is a <button> now, so it needs the button reset: browsers give one
   their own font, border and background, none of which the old <span>
   had to override. */
.brand-mark{
  flex:none;
  position:relative;
  width:44px;min-height:44px;
  display:grid;place-items:center;
  border:0;
  border-radius:11px;
  background:var(--flame);
  color:var(--ink-on-accent);
  font-family:var(--ff-ui);
  letter-spacing:.04em;
  box-shadow:0 3px 0 var(--flame-dark);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.brand-mark:active{ transform:translateY(1px); box-shadow:0 2px 0 var(--flame-dark) }
.brand-mark:focus-visible{ outline:3px solid var(--fg); outline-offset:2px }

/* ONE BADGE READS, THE OTHER SITS ON IT.

   Steven, 09 Sep: "bigger version = L2, tiny version in bottom corner =
   L1. L1 barely has to be readable, the L2 is the main one."

   Stacking them evenly made two labels of equal weight and the eye had to
   pick. The target language is what she checks; her own is a reminder
   that it is a switch at all and what it switches FROM. So the target
   fills the tile and the native rides the corner, deliberately small.

   Absolutely placed rather than in the flow, so the target stays
   optically centred in the tile instead of being pushed up by a second
   line. */
.bm-to{
  font:700 1.02rem/1 var(--ff-ui);
}
.bm-from{
  position:absolute; right:2px; bottom:1px;
  padding:1px 3px;
  border-radius:4px;
  background:rgb(var(--shade-rgb) / .30);
  font:700 .48rem/1 var(--ff-ui);
  letter-spacing:.02em;
  opacity:.9;
}

.brand-text{display:flex;flex-direction:column;line-height:1.22;min-width:0}
.brand-text strong{
  font-family:var(--ff-display);
  font-weight:600;
  font-size:1.15rem;
  color:var(--fg);
}
.brand-text em{
  font-style:normal;
  font-size:0.92rem;
  letter-spacing:.05em;
  text-transform:uppercase;
  color:var(--fg-accent);
  font-weight:600;
}

/* ---------- L1 → L2 ----------

   Was three buttons — РУС DEU ENG — permanently in the header, naming only
   the interface language and never the one she is learning. Now one string
   naming both, written in L1.

   The old buttons were 31px tall, under the 40px every other control in
   this header meets. This one is a single target and gets the full height. */


/* The eye should land on the two languages, not the connector: lighter,
   smaller, and given room on both sides so it reads as a relation rather
   than a third word. */
/* L2 is the point of the app, so it carries the accent and L1 does not. */

/* ---------- the two-step picker ---------- */
.lg-wrap{
  position:fixed; inset:0; z-index:770
  /* 770, not 120 — above the tour scrim (700), below the
     bubble (900). the language picker. A panel below the scrim gets
     dimmed during a tour and its contents cannot be lit, because a
     child cannot escape its ancestor’s stacking context. */;
  display:grid; place-items:center;
  padding:20px;
  background:rgb(var(--ink-rgb) / .55);
}
.lg-panel{
  width:min(360px, 100%);
  max-height:86vh; overflow-y:auto;
  padding:20px;
  border-radius:var(--r-md);
  box-shadow:0 18px 48px rgb(var(--ink-rgb) / .35);
}
.lg-step{
  margin:0 0 4px;
  font-size:.8rem; font-weight:700;
  letter-spacing:.08em; text-transform:uppercase;
  color:var(--fg-soft);
}
.lg-q{ margin:0 0 14px; font-size:1.2rem; line-height:1.25 }
.lg-opts{ display:flex; flex-direction:column; gap:6px }

.lg-opt{
  display:flex; align-items:center; justify-content:space-between; gap:10px;
  min-height:48px;                     /* a list read with a thumb */
  padding:10px 14px;
  border-radius:var(--r-sm);
  border:1.5px solid rgb(var(--mauve-rgb) / .35);
  background:transparent; color:var(--fg);
  font:600 1rem/1.2 var(--ff-ui);
  text-align:left; cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.lg-opt:hover:not(:disabled){ background:rgb(var(--sand-rgb) / .35) }
.lg-opt.is-on{ border-color:var(--flame); background:rgb(var(--sand-rgb) / .28) }
/* A language with no course behind it. Shown rather than hidden, because
   "coming" and "does not exist" are different promises. */
.lg-opt.is-soon{ opacity:.45; cursor:default }
.lg-opt-note{
  font-size:.78rem; font-weight:600;
  letter-spacing:.06em; text-transform:uppercase;
  color:var(--fg-soft);
}
.lg-note{
  margin:14px 0 0;
  font-size:.86rem; line-height:1.4;
  color:var(--fg-soft);
}
.lg-x{ margin-top:14px; width:100% }

/* ---------- layout ---------- */

main{
  max-width:900px;
  margin:0 auto;
  /* The 72px was chosen against a browser with no toolbar. On iOS Safari
     the bottom bar floats over the page, so the last card on any screen
     lost its final line — 'Shopping Mall in Pink…' and '6 panels' were
     both half-eaten on the comic index. env() adds the real inset where
     there is one and resolves to 0 everywhere else, so no other browser
     changes. */
  padding:8px clamp(16px,4vw,40px)
          calc(72px + env(safe-area-inset-bottom, 0px));
}

/* And the sides, for a phone held in landscape where the notch or the
   rounded corner eats into the text column. */
@supports (padding: env(safe-area-inset-left)){
  main{
    padding-left:max(clamp(16px,4vw,40px), env(safe-area-inset-left));
    padding-right:max(clamp(16px,4vw,40px), env(safe-area-inset-right));
  }
}

.eyebrow{
  font-size:0.85rem;
  font-weight:700;
  letter-spacing:.16em;
  text-transform:uppercase;
  color:var(--fg-accent);
  margin:0 0 10px;
}

h1{
  font-family:var(--ff-display);
  font-weight:600;
  font-size:clamp(1.9rem,5.5vw,2.6rem);
  line-height:1.12;
  color:var(--fg);
  margin:0 0 8px;
}

/* The global h2 is a light-surface colour. Any h2 inside the dark card —
   the end-screen title among them — inherited it and measured 1.30:1, so
   the card overrides it below. */
h2{
  font-family:var(--ff-display);
  font-weight:600;
  font-size:1.28rem;
  color:var(--fg);
  margin:0;
}

.lede{
  margin:0 0 34px;
  max-width:46ch;
  color:var(--fg-soft);
}

.hub-section{margin-bottom:40px}
.hub-head{display:flex;align-items:baseline;justify-content:space-between;gap:12px;margin-bottom:14px}
.hub-count{font-size:0.94rem;font-weight:600;color:var(--fg-accent)}

/* TWO PER ROW ON A PHONE, ALWAYS.

   This was `repeat(auto-fill, minmax(215px,1fr))` at every width, and on a
   phone that can only ever produce ONE column: 390px less the 16px page
   padding each side is 358, and two 215px columns plus the 14px gap need
   444. So every tile in every section got a full row to itself and the hub
   was twice as long as it needed to be — Steven counted the scrolling
   before I did.

   `repeat(2, minmax(0,1fr))` rather than a smaller `auto-fill` floor,
   because a floor low enough to give two columns at 320px gives FOUR on a
   tablet, and 215px is a good tile on a wide screen. Two is stated for the
   phone and the original rule takes over above it.

   `minmax(0,1fr)` and not `1fr`: a grid track's default minimum is its
   content, so one long unbroken word — a German compound, which this app is
   full of — would push the column wider than its share and break the row. */
.tiles{
  display:grid;
  grid-template-columns:repeat(2, minmax(0,1fr));
  gap:14px;
}
@media (min-width:620px){
  .tiles{ grid-template-columns:repeat(auto-fill,minmax(215px,1fr)) }
}


.tile{
  position:relative;
  text-align:left;
  /* A LONG GERMAN COMPOUND MUST BREAK RATHER THAN PUSH THE PAGE SIDEWAYS.

     `minmax(0,1fr)` on the grid track stops the COLUMN growing to fit its
     content, but it does nothing about the content itself: at two columns
     on a 320px phone a tile is 137px wide, and `Haushaltsgegenstände`
     simply rendered past its own edge and past the viewport, giving the
     whole page a horizontal scrollbar. Both halves are needed. */
  min-width:0;
  overflow-wrap:break-word;
  border:1.5px solid rgb(var(--mauve-rgb) / .42);
  background:var(--paper);
  border-radius:var(--r-lg);
  padding:20px 18px 18px;
  box-shadow:var(--shadow-card);
  transition:transform .16s ease, box-shadow .16s ease;
  color:inherit;
}
.tile:hover:not(:disabled){transform:translateY(-3px);box-shadow:var(--shadow-lift)}
.tile:disabled{opacity:.5;cursor:not-allowed;box-shadow:none}

/* The one worth starting next. Deliberately quiet: the flame border and a
   wash of the same colour, no animation and no badge. It is a suggestion,
   not an instruction, and every other tile has to stay just as tappable. */
/* The OPEN unit (the one she tapped) lights with a filled accent so it's
   clearly the selected one — distinct from .is-next (a mere suggestion) and
   from .is-done. Fixes: tapping a unit did nothing visible and unit 1 (the
   suggested one) looked permanently selected. Steven, Sep. */
.tile.is-open{
  /* The OPEN (tapped) unit — strong, clearly selected: thick flame border and
     a solid-ish flame fill, distinct from the faint .is-next suggestion. */
  border-color:var(--flame);
  border-width:3px;
  background:linear-gradient(rgb(var(--flame-rgb) / .28), rgb(var(--flame-rgb) / .28)), var(--paper);
}

.tile.is-next{
  /* The SUGGESTED next unit — a quiet hint, NOT a selected state. A dashed
     accent border and no fill, so it never competes with .is-open (the unit
     she actually tapped). Steven, Sep: before, is-next and is-open looked the
     same and unit 1 (suggested) stayed lit alongside the tapped unit. */
  border-style:dashed;
  border-color:rgb(var(--flame-rgb) / .55);
}
/* When the suggested unit is ALSO the open one, open wins (solid, filled). */
.tile.is-next.is-open{
  border-style:solid;
  border-color:var(--flame);
}

.tile-glyph{font-size:1.6rem;line-height:1;display:block;margin-bottom:12px}
.tile-name{
  font-family:var(--ff-display);
  font-size:1.16rem;
  font-weight:600;
  color:var(--fg);
  display:block;
}
.tile-sub{display:block;font-size:.86rem;color:var(--fg-accent);font-weight:600;margin-top:3px}
.tile-de{
  display:inline-block;
  margin-top:12px;
  font-size:0.9rem;
  font-weight:700;
  color:var(--fg-accent);
  border-top:1.5px dotted rgb(var(--flame-rgb) / .45);
  padding-top:8px;
}

/* ---------- AND TIGHTER, ON A PHONE ONLY ----------

   Two per row halved the hub. This is the rest of it: a 159px tile at
   390px spends 38px on a glyph sitting alone on its own line, and 38px of
   padding, for 24px of name. Measured, not guessed.

   PLACED AFTER THE RULES IT OVERRIDES, and that is not cosmetic: the
   first attempt sat above `.tile{padding:20px 18px 18px}`, which is equal
   specificity and later in the file, so it lost the cascade and the tiles
   shrank by 8px instead of 24. Third time this file has done that.

   Scoped under 620px deliberately. On a wide screen a 215px tile has the
   room and the generous version looks better; on a phone the same spacing
   is what makes eight sections feel endless. Nothing here changes `display`
   — `.tile` is reused in the songbook and elsewhere this file cannot see,
   and a layout change would reach all of it. Whitespace only. */
/* The button that opens the rest of a capped section. Full width so it
   reads as the end of the grid rather than a ninth tile, and quiet so it
   never competes with the things it is holding back. */
.sec-more{
  display:block;
  width:100%;
  margin-top:12px;
  min-height:44px;
}

@media (max-width:619px){
  .tile{ padding:14px 14px 13px }
  .tile-glyph{ font-size:1.35rem; margin-bottom:5px }
  .tile-de{ padding-top:6px; font-size:0.82rem }
  .tiles{ gap:10px }
}


/* ---------- buttons ---------- */

.btn{
  border:0;
  border-radius:999px;
  padding:13px 24px;
  font-weight:700;
  letter-spacing:.01em;
  transition:transform .12s ease, filter .12s ease;
}
.btn:active:not(:disabled){transform:translateY(2px)}
.btn:disabled{opacity:.45;cursor:not-allowed}

.btn-primary{background:var(--flame);color:var(--on-flame);box-shadow:0 3px 0 var(--flame-dark)}
.btn-primary:hover:not(:disabled){filter:brightness(1.06)}

.btn-ghost{
  background:transparent;
  color:var(--fg);
  border:1.5px solid rgb(var(--mauve-rgb) / .6);
  padding:12px 20px;
}
/* On the dark card this was --liver text on a --liver background — exactly
   the same colour, 1.0:1, in every theme. The button was there and could be
   clicked; it simply could not be seen. */
.card .btn-ghost,
.es-done .btn-ghost,
.gn-splash .btn-ghost{
  color:var(--fg);
  border-color:var(--soft-on-dark);
}
.card .btn-ghost:hover,
.es-done .btn-ghost:hover,
.gn-splash .btn-ghost:hover{
  background:rgb(var(--sand-rgb) / .16);
  border-color:var(--sand);
}
.btn-ghost:hover:not(:disabled){background:rgb(var(--sand-rgb) / .4)}

.btn-quiet{
  background:none;border:0;padding:8px 2px;
  color:var(--fg-accent);font-weight:600;font-size:.88rem;
  text-decoration:underline;text-underline-offset:3px;
}
.btn-quiet:hover{color:var(--fg-accent)}

/* ---------- practice screen ---------- */

.practice-top{
  display:flex;
  align-items:center;
  justify-content:space-between;
  gap:14px;
  margin-bottom:16px;
}
/* ROOM FOR THE BACK BUTTON, ON EVERY SCREEN THAT HAS ONE.

   Steven, 09 Sep: "every section you move into with the BACK button, can
   we move the titles forward just a tiny bit so the back button doesn't
   overlap them?"

   `.backlink` is `position:fixed` and aligned to the VIEWPORT edge, not
   the content column — so it takes up no space in the `.practice-head`
   it is a child of, and the title beside it starts at the column's left
   edge and slides underneath.

   ONE RULE RATHER THAN FORTY. `.practice-title` is the header of every
   activity in the app — forty files use it — so the offset belongs here
   and not in each screen. A comic-specific version of this fix went into
   `.cm-bar-t` earlier the same day; that one stays because the comic
   builds its own bar rather than using `.practice-head`.

   ONLY WHERE IT IS NEEDED. `main` is 900px and centred, so from about
   980px up the content column already begins well right of the button
   and the offset would be a gap for nothing. Below that the two overlap,
   which is why this is the phone-and-tablet case.

   MEASURED, NOT GUESSED, and the first attempt was too generous.

   `main` uses `padding:8px clamp(16px,4vw,40px)` and the button uses
   `left:clamp(16px,4vw,40px)` — THE SAME EXPRESSION. So the title's left
   edge already sits exactly where the button begins, at every width, and
   the offset only has to clear the button's own WIDTH rather than its
   position as well.

   That width is fixed: font-size `calc(.86rem + 3px)` is 17px, the svg is
   1.1em so 19px, plus 13px padding either side — 45px, the same on a
   phone and a tablet because none of it scales with the viewport. Hence
   one value rather than the two breakpoints this had, and 52px rather
   than 62 and 76.

   Steven, 09 Sep: "we turned it into an icon so its footprint is pretty
   limited for all versions of it." Right on both counts — small, and the
   same size whichever of the four arrows is chosen. */
.practice-title{min-width:0; padding-left:52px}
@media (min-width:980px){
  /* Content column is clear of the viewport-anchored button from here. */
  .practice-title{ padding-left:0 }
}
.practice-title h2{font-size:1.1rem;line-height:1.25}
.practice-title p{margin:2px 0 0;font-size:0.94rem;color:var(--fg-accent);font-weight:600}

/* ---------- THE STREAK RIDES THE CARD'S TOP EDGE ----------

   `.card` is `overflow:hidden`, so a badge positioned to straddle its top
   edge from inside gets cut in half — and `.card` is shared by every
   activity, so unclipping it to suit one screen would reach all of them.
   The wrapper carries the badge over the card from outside instead.

   `right` rather than `left`: the eye is reading the German, which starts
   at the left, and a number sitting on top of the first word is in the way
   of the one thing on the screen that matters.

   `.fb-cardwrap .fb-streak` AND NOT `.fb-streak`, and the descendant is
   load-bearing. `.run-streak` sets its own background and colour three
   thousand lines below this, so a single-class rule here loses the cascade
   however carefully it is written and the badge renders in the run-line's
   translucent mauve, invisible against the card. Specificity rather than
   position, because position is what has failed three times already in
   this file.

   `.run-streak.is-hot` is deliberately left to win: at a streak of three
   the badge turns flame, and that should still happen here. */
.fb-cardwrap{ position:relative }
.fb-cardwrap .fb-streak{
  position:absolute;
  top:0; right:18px;
  transform:translateY(-50%);
  z-index:3;
  padding:6px 13px;
  border-radius:999px;
  /* Opaque, and the PAGE's paper rather than the card's: it has to read as
     sitting on top of the card, which means not sharing its colour. */
  background:var(--paper);
  color:var(--fg);
  border:1.5px solid rgb(var(--mauve-rgb) / .45);
  box-shadow:0 3px 10px rgb(var(--ink-rgb) / .28);
  white-space:nowrap;
}

.progress{
  display:flex;align-items:center;gap:10px;
  font-size:0.94rem;font-weight:700;color:var(--fg);
  white-space:nowrap;
}
.progress-track{
  width:clamp(60px,18vw,120px);height:7px;
  background:rgb(var(--liver-rgb) / .16);
  border-radius:999px;overflow:hidden;
}
.progress-fill{
  display:block;height:100%;width:0;
  background:var(--flame);
  border-radius:999px;
  transition:width .3s ease;
}

/* the card: signature element — cream-on-dark flashcard */
.card{
  background:
    linear-gradient(165deg, var(--liver) 0%, var(--liver-deep) 100%);
  border-radius:26px;
  padding:clamp(22px,5vw,38px);
  box-shadow:0 22px 44px -26px rgb(var(--ink-rgb) / .85);
  position:relative;
  overflow:hidden;
}
.card::after{
  content:"";
  position:absolute;
  left:-40px;
  right:-40px;
  bottom:-130px;
  height:230px;
  background:radial-gradient(closest-side, rgb(var(--sand-rgb) / .17), transparent);
  pointer-events:none;
}

.card-tools{
  display:flex;
  align-items:center;
  gap:10px;
  margin-bottom:22px;
}

.speak{
  display:inline-flex;
  align-items:center;
  gap:9px;
  background:rgb(var(--sand-rgb) / .14);
  border:1.5px solid rgb(var(--sand-rgb) / .4);
  color:var(--sand);
  border-radius:999px;
  padding:10px 18px;
  font-weight:700;
  font-size:.9rem;
  transition:background .15s;
}
.speak:hover{background:rgb(var(--sand-rgb) / .28)}
.speak.is-speaking{background:var(--sand);color:var(--liver);border-color:var(--sand)}
.speak-icon{font-size:1.05rem;line-height:1}

/* ==================================================================
   APPROVED TEXT COLOURS, BY SURFACE

   Measured with paint-audit.py against all nine themes. A cell is the
   worst ratio across them; WCAG AA asks 4.5:1 for body text.

   ink            paper    wash    card     row  flame-tint  accent
   --------------------------------------------------------------------
   --ink          11.79   11.21    1.00    1.05     1.19      3.58
   --ink-soft      5.42    5.18    1.61    1.84     1.42      1.77
   --liver         7.05    6.74    1.30    1.36     1.00      2.08
   --liver-deep    9.84    9.35    1.00    1.05     1.11      2.91
   --on-dark       1.03    1.00    9.20   10.49     8.09      1.85
   --sand          1.09    1.04    7.41    8.41     6.19      1.26
   --sand-deep     1.54    1.46    5.22    5.62     4.14      1.01
   --flame         1.92    1.83    2.91    3.33     2.61      1.00
   --flame-pale    1.38    1.32    6.07    6.95     5.46      1.39
   --flame-dark    2.95    2.82    2.00    2.29     1.79      1.46
   --mint          1.02    1.00    7.22    8.27     6.49      1.60
   --yes           1.38    1.32    6.46    7.40     5.81      1.33
   --yes-deep      4.62    4.43    1.93    2.21     1.73      1.51
   --no            1.96    1.88    4.54    5.20     4.08      1.03
   --no-deep       5.56    5.33    1.60    1.84     1.44      1.81
   --mauve         2.47    2.36    2.95    3.18     2.34      1.21
   --paper         1.00    1.04    9.84   11.22     8.66      1.92
   --on-flame     13.47   12.89    1.03    1.01     1.22      4.65

   Three tokens are not usable as text on ANY surface in every theme:
   --flame (best 3.33), --flame-dark (best 2.95), --mauve (best 3.18).
   They are fills and borders, not inks.

   Use the aliases below rather than picking from the table by hand. They
   are declared once in :root and resolve per theme, because each one
   points at a token every theme already redefines.
   ================================================================== */

/* ------------------------------------------------------------------
   CONTRAST-APPROVED SURFACES

   Four surfaces, and only four. Each sets its background and its text
   colour together, so the two cannot drift apart — the bug this exists
   to prevent was a control designed for the dark card being dropped onto
   a white one, where its text measured 1.06:1 and simply was not there.

   Every pair below was measured against all nine themes. The number is
   the worst case across them; WCAG asks 4.5:1 for body text.

     .surf-paper    --ink on --paper           11.79
                    --ink-soft (.muted)         5.42
     .surf-wash     --ink on --sand-wash       11.21
                    --ink-soft (.muted)         5.18
     .surf-deep     --on-dark on --liver-deep   9.20
                    --sand (.muted)             7.41
     .surf-accent   --on-flame on --flame       4.65

   Rejected, and not available: --flame-dark on a light surface. It
   measures 2.95 in orchid. If a light surface needs emphasis, use
   --ink with weight, not a hue.

   Do not invent a fifth surface inline. Add it here, measure it against
   every theme, and only then use it. A colour pair chosen at the point
   of use is a colour pair nobody has checked. */

.surf-paper { background:var(--paper);      color:var(--ink); }
.surf-wash  { background:var(--sand-wash);  color:var(--ink); }
.surf-deep  { background:var(--liver-deep); color:var(--on-dark); }
.surf-accent{ background:var(--flame);      color:var(--on-flame); }

.surf-paper .muted,
.surf-wash  .muted{ color:var(--fg-soft); }
.surf-deep  .muted{ color:var(--fg-soft); }
.surf-accent .muted{ color:var(--on-flame); opacity:.8; }

.mode-toggle{margin-left:auto;display:flex;gap:2px;padding:3px;border-radius:999px;background:rgb(var(--shade-rgb) / .22)}
.mode-toggle button{
  border:0;background:transparent;color:var(--fg-soft);
  font-size:.9rem;font-weight:700;letter-spacing:.03em;
  padding:9px 14px;border-radius:999px;cursor:pointer;
}
.mode-toggle button[aria-pressed="true"]{background:var(--sand);color:var(--liver)}

/* The same control on a light surface. Picked up from the surface class
   rather than from a modifier on the toggle, so putting a toggle inside
   a .surf-paper block cannot get it wrong.
     unselected  --ink-soft on --ink at 8%   4.66 worst
     selected    --paper on --ink           11.79 worst */
.surf-paper .mode-toggle,
.surf-wash  .mode-toggle{ background:rgb(var(--ink-rgb) / .08); }
.surf-paper .mode-toggle button,
.surf-wash  .mode-toggle button{ color:var(--fg-soft); }
.surf-paper .mode-toggle button[aria-pressed="true"],
.surf-wash  .mode-toggle button[aria-pressed="true"]{
  background:var(--ink); color:var(--paper);
}

.sentence{
  font-family:var(--ff-display);
  font-size:clamp(1.5rem,5.2vw,2.15rem);
  line-height:1.55;
  color:var(--fg);
  margin:0;
  word-break:break-word;
}

.slot{
  display:inline-block;
  min-width:3.4em;
  text-align:center;
  padding:0 .28em;
  border-bottom:3px solid var(--sand);
  color:var(--fg-soft);
  font-style:italic;
}
.slot.filled{
  border-bottom-color:var(--mint);
  color:var(--mint);
  font-style:normal;
}
.reveal{color:var(--mint)}

/* The German above this scales from 1.5rem to 2.15rem, so a flat 1rem here
   looked fine on a phone and tiny on a desktop — the gap widened with the
   window. It scales too now, at roughly two thirds the size: clearly the
   supporting line, still readable across the room. */
.translation{
  margin:16px 0 0;
  font-size:clamp(1.02rem, 3.2vw, 1.32rem);
  line-height:1.45;
  color:var(--fg-soft);
  font-weight:500;
}

.figure{
  margin:0 0 20px;
  display:flex;
  justify-content:center;
}
.figure img{
  display:block;
  width:auto;
  max-width:100%;
  max-height:230px;
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .18);
}
/* Every picture is a 2:3 vertical crop off a 3x3 sheet. A sprite therefore
   fills the width it is given and takes its height from the aspect ratio —
   never a fixed pixel height inside a wider box, which leaves dead paper
   beside the image, and never a square. Size is controlled by capping the
   grid that holds them, so the pictures scale with the layout instead of
   being pinned small. */
/* THE BASE RULE FOR A SPRITE IS NOT HERE. It used to be, written as
   `.sp` — a class nothing in the app has ever carried, because
   sprite.js emits `sp`. So this rule matched nothing for its whole life
   and a second, live `.sp` base rule was written further down this file.

   Removed rather than renamed: two base rules for one class, one of them
   with a translucent background, is exactly the specificity trap the
   notes at the top of this file record biting twice. The live one is
   opaque and is the one that has actually been in effect. */
.figure .sp{
  width:clamp(190px, 46vw, 300px);
}
@media (min-width:561px){
  .figure img{max-height:430px}
  .figure .sp{width:clamp(230px, 24vw, 300px)}
}

/* answers */

.answers{margin-top:26px}

/* One word does not need the whole width.

   This was already auto-fit, and it was failing by ONE PIXEL. Two 150px
   tracks plus an 11px gap needs 311px; on a 393pt phone, after the page
   and card padding, the row measures about 310. So every option got its
   own line and the four choices filled the screen.

   130px moves the threshold to 271, which gives two columns on every
   iPhone from the 13 mini (375pt) up. A 320pt SE leaves a 237px row and
   stays single-column on purpose: dropping the floor to 110 would fit two
   there, and then a ten-letter word like `Hafermilch` wraps inside its own
   button, which is worse than a tall list.

   `min(130px, 100%)` is the guard against the opposite failure: in a
   container narrower than the floor, a bare 130px track overflows instead
   of collapsing to one column.

   A long option still gets a full row — see `.option.is-wide`, set by
   fill-blank from the length of the word. Two columns is right for
   `Haben · Das · Kleid`, and wrong for a whole clause. */
.options{
  display:grid;
  grid-template-columns:repeat(auto-fit,minmax(min(130px,100%),1fr));
  gap:11px;
}
.option.is-wide{ grid-column:1 / -1 }
.option{ overflow-wrap:break-word }
.option{
  border:1.5px solid rgb(var(--sand-rgb) / .38);
  background:rgb(var(--paper-rgb) / .94);
  color:var(--ink-on-light);
  border-radius:var(--r-md);
  padding:15px 12px;
  font-family:var(--ff-display);
  font-size:1.1rem;
  font-weight:500;
  transition:transform .12s, background .15s, border-color .15s;
}
.option:hover:not(:disabled){transform:translateY(-2px);border-color:var(--sand)}
.option.is-right{background:var(--yes);border-color:var(--yes);color:var(--on-yes);font-weight:600}
.option.is-wrong{background:rgb(var(--no-rgb) / .18);border-color:rgb(var(--no-rgb) / .8);color:var(--soft-on-dark);text-decoration:line-through}
.option:disabled{cursor:default}

.typebox{display:flex;gap:10px;flex-wrap:wrap}
.typebox input{
  flex:1 1 200px;
  min-width:0;
  font-family:var(--ff-display);
  font-size:1.15rem;
  padding:14px 16px;
  border-radius:var(--r-md);
  border:1.5px solid rgb(var(--sand-rgb) / .45);
  background:rgb(var(--paper-rgb) / .96);
  color:var(--ink);
}
.typebox input::placeholder{color:var(--fg-accent);font-style:italic}

/* This sits under a sentence that scales up to 2.15rem, so a flat .98rem
   shrank away beside it on anything bigger than a phone. */
.feedback{
  margin-top:18px;
  min-height:1.5em;
  font-size:clamp(1.05rem, 3.4vw, 1.3rem);
  line-height:1.4;
  font-weight:600;
  color:var(--fg-soft);
}
.feedback.right{color:var(--yes)}
.feedback.close{color:var(--fg-soft)}
.feedback.wrong{color:var(--no)}
.feedback strong{font-family:var(--ff-display);font-weight:600}

.card-foot{
  display:flex;
  align-items:center;
  gap:12px;
  flex-wrap:wrap;
  margin-top:24px;
  padding-top:20px;
  border-top:1.5px dotted rgb(var(--sand-rgb) / .3);
}
.card-foot .btn-quiet{color:var(--fg-soft)}
.card-foot .btn-quiet:hover{color:var(--fg-soft)}
.card-foot .spacer{margin-left:auto}

/* ---------- done screen ---------- */

.done{
  background:var(--paper);
  border:1.5px solid rgb(var(--mauve-rgb) / .42);
  border-radius:26px;
  padding:clamp(26px,6vw,44px);
  box-shadow:var(--shadow-card);
  text-align:center;
}
.done-badge{
  display:inline-block;
  background:var(--mint);
  color:var(--liver-deep);
  font-weight:700;
  font-size:0.92rem;
  letter-spacing:.12em;
  text-transform:uppercase;
  padding:8px 16px;
  border-radius:999px;
  margin-bottom:16px;
}
.card h2,
.done h2,
.es-done h2{color:var(--fg)}
.done h2{font-size:1.6rem;margin-bottom:8px}
.done p{margin:0 0 26px;color:var(--fg-soft)}
.done-actions{display:flex;gap:12px;justify-content:center;flex-wrap:wrap}

/* Back was borderless text in the same muted tone as body copy, which made
   the one control that gets used on every screen the hardest thing on the
   page to find. It is a real button now: an outlined pill with the ink
   colour, so it reads as something you press rather than a caption. */
/* BACK MUST ALWAYS BE REACHABLE.

   It used to sit in the page header and scroll away with it. On a long
   screen — a 97-line song, a 27-face board, the word list — there was then
   NO way back short of scrolling to the top, and the browser's own Back
   button leaves the site because this is one page.

   STICKY DID NOT WORK AND THIS IS WHY.

   `position:sticky` only holds while the element's PARENT is still in view.
   Every activity puts the back link inside its own header — `.practice-head`
   or similar — which is only as tall as the header itself. So the moment
   the header scrolled off, the parent went with it and took the back link
   along. On a phone it looked fine because the screens are short; on a
   desktop, scrolling a song or the word list left NO way back at all.

   The previous comment claimed this was checked. It checked for an
   `overflow` ancestor, which is a different failure, and never tested the
   parent height — which is the one that was actually wrong.

   So it is FIXED to the viewport, not sticky within a parent. It cannot be
   scrolled away because it is no longer part of the scrolling content.

   A placeholder keeps its space in the flow, or every screen's title jumps
   up underneath it.

   The background is opaque rather than translucent, or the text it floats
   over shows through and both become unreadable. */
.backlink{
  position:fixed;
  top:calc(8px + env(safe-area-inset-top, 0px));
  /* THE VIEWPORT EDGE, NOT THE CONTENT COLUMN. It was aligned to `main`
     (900px centred) so it would sit against the text it belongs to. On a
     wide window that puts it a third of the way across the screen, where
     it stops reading as persistent navigation and starts looking like
     page content. Steven asked for it back on the side, 08 Sep. */
  left:clamp(16px, 4vw, 40px);
  z-index:40;
  display:inline-flex;align-items:center;gap:6px;
  background:var(--paper);
  box-shadow:0 3px 14px rgb(var(--scrim-rgb) / .3);
  border:1.5px solid rgb(var(--mauve-rgb) / .55);
  border-radius:999px;
  /* Symmetric now. The 15px/12px split was for a chevron followed by a
     word — the extra right padding balanced the text. With a single
     arrow in there, uneven padding puts the glyph visibly off-centre. */
  padding:8px 13px;

  /* The glyph is inline SVG now, not a character. An SVG with a
     viewBox and no width fills its container, so without the rule
     below the arrow expands to the whole content box. */
  color:var(--ink);
  font-family:var(--ff-ui);
  /* Was .86rem, which read as tiny next to everything around it.
     Steven asked for 3px more; written as the sum rather than resolved to
     1.05rem so the intent stays visible and it tracks the root size.

     The pill grows with it: the padding is in px, so the height follows
     the text. And this is EVERY back button in the app — `.backlink` is
     the same class on all 36 screens — which is deliberate. One control
     that changed size depending on where she was would be worse than a
     small one. */
  font-weight:700;font-size:calc(.86rem + 3px);line-height:1;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  transition:background .12s ease, border-color .12s ease, color .12s ease;
}
/* Tied to the font-size above in `em`, so the 3px Steven asked for still
   applies and the size stays one number in one place. `block` kills the
   inline baseline gap that would push the arrow off-centre in the pill. */
.backlink svg{ width:1.1em; height:1.1em; display:block }

.backlink:hover{
  background:var(--flame);
  border-color:var(--flame);
  color:var(--on-flame);
}
.backlink:active{transform:translateY(1px)}
.backlink:focus-visible{
  outline:3px solid var(--flame);
  outline-offset:2px;
}
/* THE SPACE IT NO LONGER OCCUPIES.

   Fixed means out of the flow, so without this every screen's title slides
   up underneath the floating button.

   NOT `main:has(.backlink)`, which was the first attempt. `:has()` is
   missing from Safari before 15.4 and Firefox before 121, and the fallback
   there is the title tucked under the button on every screen in the app.
   A layout that depends on a recent selector is a layout that is broken on
   someone's phone.

   These two containers hold every back link in the codebase — checked, not
   assumed: `.practice-head` is created 39 times and gets one all 39 times,
   `.cm-bar` once. The comic INDEX appends its link straight to the view, so
   that one screen sets its own padding.

   No :has, no @supports, works in every browser that can render the site. */
/* 52px OF NOTHING, RESERVED FOR A BUTTON THAT IS NOT THERE.

   `.backlink` is `position:fixed` at `top:8px`, so it is pinned to the
   VIEWPORT — and at the top of the page the 110px topbar already occupies
   that band. Back floats over the topbar, not over this header, so the gap
   was clearing a collision that cannot happen. Word Lab is the only user of
   this class, so nothing else moves.

   6px rather than 0: the first thing under it is an h1, and its own margin
   does the rest. */
.practice-head{ padding-top:6px }
.cm-bar{ padding-top:52px }
.cm-intro{ margin-top:52px }

.empty{
  border:1.5px dashed rgb(var(--mauve-rgb) / .6);
  border-radius:var(--r-lg);
  padding:26px;
  color:var(--ink-on-light);
  font-weight:600;
  background:var(--paper);
}

@media (max-width:560px){
  body{font-size:16px}
  .card-tools{flex-wrap:wrap}
  .mode-toggle{margin-left:0;width:100%;justify-content:center}
}

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


/* --- theme picker ------------------------------------------- */

/* `position:relative` because `.purse-pop` is absolute inside this row
   with `top:calc(100% + 6px)`, and GH.nav.clampPanel writes a `left`
   measured from this element's own box. Without it the containing block
   is the viewport: the panel lands a full screen height below the header
   and off the bottom, so tapping the balance looks like it does nothing.
   `.pick` next to it declares the same thing for the same reason. */
/* ============================================================
   THE TOOLBAR ROW. ONE HEIGHT, SET HERE, FOR EVERYTHING IN IT.
   ============================================================

   Steven, 09 Sep: "I WANT ALL OF THEM TO ALIGN LIKE THEY WERE BORN TO BE
   EXACTLY IN ALIGNMENT — to the pixel, no up or down even by 1 pixel on
   1 of them. I never want to have to mention this again, ever."

   THIS BLOCK IS THE ONLY PLACE THE ROW'S HEIGHT IS DECIDED. Do not set a
   height, min-height, padding or border on `.purse`, `.themeswitch`,
   `.petstrip`, `.petstrip-btn` or `.bt-perch` that could change their
   outer box. If a new control joins the row it inherits 40px here and
   needs no rule of its own.

   WHY IT KEPT DRIFTING. Each control carried its own sizing, so the row
   was only ever as aligned as five separate rules happened to agree.
   `.themeswitch` was the last offender: `padding:3px` and a `1.5px`
   border around a 40px swatch, with no height of its own, computed to
   49px under `border-box` — and a `min-height:40px` added to it did
   nothing, because 49 already cleared it. Fixing them one at a time is
   how this took eight attempts.

   `height` AND `min-height` AND `max-height`, all 40: `min` alone lets a
   tall child push a control out, `max` alone lets a short one collapse,
   and `height` alone is overridable by a later `min-height`. All three
   together cannot be beaten by content.

   `> *` reaches the direct children (`.purse`, `.themeswitch`,
   `.petstrip`, `.bt-perch`), and the second selector reaches the button
   INSIDE the pet strip, which is a nav wrapping a button.

   `flex:none` stops the flex row stretching or shrinking any of them.
   `align-self:center` is belt and braces over the row's own
   `align-items`, so a control setting its own alignment cannot escape. */
.topbar-controls{display:flex;align-items:center;gap:10px;flex-wrap:wrap;justify-content:flex-end;position:relative}

/* NAMED CONTROLS, NOT `> *`. The first version blanketed every direct
   child — and the panels that open FROM this row are also children of it:
   the crystals popup (`.purse-pop`) and the Appearance panel. Both got
   clamped to 40px and collapsed on themselves. Steven, 09 Sep, twice,
   which is once more than it should have taken.

   Naming the four controls means a panel added later cannot be caught by
   this, and a new CONTROL is one line to add. */
.topbar-controls.topbar-controls > .purse,
.topbar-controls.topbar-controls > .themeswitch,
.topbar-controls.topbar-controls > .petstrip,
.topbar-controls.topbar-controls .petstrip-btn{
  height:40px;
  min-height:40px;
  max-height:40px;
  flex:none;
  align-self:center;
  box-sizing:border-box;
  margin-top:0;
  margin-bottom:0;
}
/* The pet strip is a nav around the button; it must not add a box of its
   own on top of the button's 40. */
.topbar-controls.topbar-controls .petstrip{ padding:0; border:0; background:none }

/* The listening counter above a song, and the one-off explanation of what
   tapping every line is worth. The tip is a note, not a dialog: it appears
   once ever and vanishing quietly is the right exit for it. */
.sg-credit{
  margin:10px 0 0;
  font:700 .86rem/1.3 var(--ff-ui);
  letter-spacing:.04em; text-transform:uppercase;
  color:var(--fg-accent);
}
.sg-credit.is-rest{ color:var(--fg-soft) }
/* The counter and the quiz on one line: the counter takes the room it
   needs, the button takes what it needs, and neither becomes a banner. */
.sg-credit-row{
  display:flex; align-items:center; justify-content:space-between;
  gap:10px; flex-wrap:wrap; margin-top:10px;
}
.sg-credit-row .sg-credit{ margin:0; flex:1 1 auto; min-width:0 }
.sg-fill{ flex:none; min-height:44px; padding:10px 16px; font-size:.9rem }
.sg-tip{
  margin:8px 0 0;
  padding:11px 14px;
  border-radius:var(--r-md);
  background:rgb(var(--sand-rgb) / .32);
  border:1.5px solid rgb(var(--mauve-rgb) / .4);
  font-size:.9rem; line-height:1.4; color:var(--fg);
}

/* ---------- THE VOICE PICKER IN SETTINGS ----------

   One row per German voice the BROWSER is offering. The name is a button
   that speaks a sample; the two chips assign it to the main voice or the
   second dialogue speaker. Tapping a chip that is already on clears it and
   hands the slot back to the automatic pick, so there is always a way out
   of a choice she regrets. */
.st-voices{ display:flex; flex-direction:column; gap:6px; margin:10px 0 0 }
.st-voice{ display:flex; align-items:center; gap:6px }
.st-voice-name{
  flex:1 1 auto; min-width:0;
  display:flex; align-items:center; gap:8px;
  min-height:44px; padding:8px 12px;
  border:1.5px solid rgb(var(--mauve-rgb) / .38);
  border-radius:var(--r-sm);
  background:transparent; color:var(--fg);
  font:600 .98rem/1.2 var(--ff-ui);
  text-align:left; cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.st-voice-label{ overflow:hidden; text-overflow:ellipsis; white-space:nowrap }
/* The quality marker, straight off the identifier — this is the line that
   tells him whether his premium voices reached the web API at all. */
.st-voice-q{
  flex:none;
  font:700 .7rem/1 var(--ff-ui);
  letter-spacing:.08em; text-transform:uppercase;
  color:var(--fg-soft);
  border:1px solid rgb(var(--mauve-rgb) / .45);
  border-radius:999px; padding:4px 7px;
}
.st-voice-pick{
  flex:none;
  min-height:44px; min-width:52px; padding:8px 10px;
  border:1.5px solid rgb(var(--mauve-rgb) / .38);
  border-radius:var(--r-sm);
  background:transparent; color:var(--fg-soft);
  font:700 .82rem/1 var(--ff-ui);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.st-voice-pick.is-on{
  background:var(--flame); border-color:var(--flame); color:var(--on-flame);
}

/* A THIRD SPEAKER DOES NOT FIT BESIDE THE NAME ON A SMALL PHONE.

   Three 52px chips plus their gaps leave 64px for the voice name at 320px,
   which is not a name, it is an ellipsis. The row wraps instead: the name
   takes the full width and the three chips sit under it. Shrinking the
   chips was the other option and it is the wrong one — they are already at
   the minimum tap size. */
@media (max-width:389px){
  .st-voice{ flex-wrap:wrap }
  .st-voice-name{ flex:1 0 100% }
  .st-voice-pick{ flex:1 1 0 }
}

/* ---------- COLLAPSED HEADER PICKERS ----------

   The colour picker and the type picker are both one control that opens
   into its own list. Twelve swatches wrapped to three rows and four `Aa`
   in a permanent row cost roughly 190px of the first screen between them,
   above content she actually came for, for two settings she touches once.

   OUT OF FLOW, and that is the point. A panel that expands in the document
   pushes everything below it down by its own height — which is exactly the
   problem this replaces, just moved one tap later.

   WHICH EDGE IT HANGS FROM DEPENDS ON WHERE THE CONTROLS SIT, and getting
   this wrong put the panel half off the phone twice.

   Attempt one anchored `right:0` to the trigger. The colour trigger is the
   LEFTMOST control, so the panel ran off the left edge.

   Attempt two anchored `right:0` to `.topbar-controls` instead. That fit
   only because the language control was still three wide buttons. Replacing
   them with the narrow `Eng → Ger` pair shrank the row, and the panel went
   off the left edge again — this time far enough to lose a whole column of
   swatches and an `Aa`.

   So it follows the controls instead of guessing. On a phone the brand and
   the controls wrap onto separate rows and the controls sit at the LEFT, so
   the panel opens rightward from the trigger. Above the wrap the controls
   are right-aligned, so it opens leftward. Either way it opens INTO the
   page and never off it, and it does not care how wide the controls are —
   which is what broke it the first two times. */
.pick{ position:relative; display:flex; align-items:center }

.pick-now{
  width:40px; height:40px;              /* 40px tap target, same as a swatch */
  flex:none; padding:0;
  display:grid; place-items:center;
  border-radius:999px;
  background:rgb(var(--paper-rgb) / .8);
  border:1.5px solid rgb(var(--mauve-rgb) / .45);
  color:var(--fg);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
/* The letters are the preview: they render in whichever face is applied,
   so the trigger shows the setting rather than merely naming it. */
.pick-now-aa{ font-family:var(--ff-display); font-weight:600; font-size:.95rem; line-height:1 }
.pick-now:hover{ background:rgb(var(--sand-rgb) / .55) }
.pick-now:focus-visible{ outline:2px solid var(--flame); outline-offset:2px }
.pick.is-open .pick-now{ border-color:var(--flame) }

/* ONE PANEL, TWO AXES. The palette and the type scale are both "how the
   app looks", so they share a card rather than each having their own
   control in the header.

   AN EXPLICIT WIDTH is required, not cosmetic: an absolutely positioned
   flex container with `flex-wrap` shrinks to its narrowest item instead of
   wrapping, and the twelve swatches came out as one 40px column twelve
   rows tall. 230px holds five per row — 5x40 + 4x2 gap = 208, plus 16
   padding and 3 border = 227. */
.pick .pick-panel{
  display:none;
  position:absolute;
  top:calc(100% + 6px);
  /* Phone: controls have wrapped to their own row and sit at the left, so
     the panel opens rightward from the trigger. Flipped above the wrap by
     the media query at the end of this block. */
  left:0;
  right:auto;
  z-index:60;
  flex-direction:column;
  gap:8px;
  width:244px;
  /* A BACKSTOP, in case place() has not run yet — the first paint of a
     panel happens before it is measured. `max-width` in viewport units
     cannot be exceeded whatever the offset works out to, so the worst
     case is a panel that is narrower than it wants rather than one that
     is off the screen. */
  max-width:calc(100vw - 16px);
  box-sizing:border-box;
  padding:8px;
  border-radius:var(--r-md);
  /* Opaque, unlike the old inline row: this floats over content now, and
     content reading through a settings panel is unreadable. */
  background:rgb(var(--paper-rgb));
  border:1.5px solid rgb(var(--mauve-rgb) / .45);
  box-shadow:0 10px 28px rgb(var(--ink-rgb) / .22);
}
.pick.is-open .pick-panel{ display:flex }

/* Inside the card the two rows are bare — the card already carries the
   border, the background and the padding, and a bordered box inside a
   bordered box reads as a mistake. */
/* THE PANEL'S OWN ROWS ARE NOT THE TOOLBAR ROW.

   `.themeswitch` is used twice: once as the collapsed 40px control in the
   header, and again INSIDE this panel as a wrapping grid of swatches. The
   40px height that makes the header row line up crushes the panel version,
   whose rows wrap — which is how the colour dots ended up sitting on top
   of the `Aa` type samples. Steven's screenshot, 09 Sep.

   So the panel explicitly undoes the height. `height:auto` and
   `max-height:none` together, because the header rule sets both and
   overriding only one leaves the other clamping. */
.pick-panel .themeswitch,
.pick-panel .typeswitch,
.pick-panel .backswitch{
  background:none; border:0; padding:0;
  max-width:none; width:auto;
  height:auto; min-height:0; max-height:none;
  flex-wrap:wrap;
  justify-content:flex-start;
}
/* A hairline between the colours and the faces, so the four `Aa` read as a
   second choice rather than a thirteenth swatch. */
.pick-panel .typeswitch,
.pick-panel .backswitch{
  border-top:1px solid rgb(var(--mauve-rgb) / .35);
  padding-top:8px;
}

/* ---------- BIGGER INSIDE THE PANEL THAN IN THE HEADER ----------

   Steven, on the first version: "could be a tiny bit larger so things are
   more easily clickable".

   The header is where space is scarce and the trigger stays 40px. The
   panel is a deliberate stop — she has already decided to change something
   — so it can spend the room, and a 40px swatch with a 22px dot inside it
   is a small target for a thumb on a phone.

   FOUR PER ROW, NOT FIVE, and the size was chosen to get there: twelve
   swatches divide into three clean rows of four, where five per row left an
   orphaned row of two. Bigger targets and a tidier grid from one change.

     4 x 52 + 3 x 4 gap = 220, inside 244 - 16 padding - 3 border = 225.

   The faces get a real 40px height for the same reason: `padding:7px 10px`
   on a .8rem line is a 27px target, the smallest thing in the panel and
   the one furthest from a fingertip.

   The width lives in the `.pick .pick-panel` rule above, NOT here — a
   `.pick-panel{width:...}` override at lower specificity silently lost and
   the grid stayed at three per row while claiming to be four.

   Horizontal padding is 12px and not 16px because four faces at 16px came
   to 240 against 225 of usable width and quietly compressed. */
.pick-panel .themeswitch{ gap:4px }
.pick-panel .swatch{ width:52px; height:52px }
.pick-panel .swatch-dot{ width:28px; height:28px }

/* THE COLLAPSED CONTROL IN THE TOOLBAR, and only that one. `> .swatch`
   is the button sitting in the row; everything in `.pick-panel` is the
   opened menu and keeps the 22px above. 38px inside a 40px button so it
   fills it the way the crystal pill and the pet pill fill theirs. */
.themeswitch > .swatch .swatch-dot{
  width:38px; height:38px;
}
.pick-panel .typeswitch button{
  padding:13px 12px;         /* 14.4px line + 26 = 40.4px, a real target */
  font-size:.9rem;
}
/* Same 40px target as the faces: a 20px glyph plus 20 of padding. */
.pick-panel .backswitch button{
  padding:10px 14px;
  font-size:1.25rem;
}

/* WHICH WAY IT OPENS IS MEASURED, NOT GUESSED.

   A media query was tried and was wrong at exactly 560px, because the
   breakpoint has to predict where the header flex-wraps — and that moves
   with the type variant (Large and Loud widen the title), with the
   translated title, and with anything added to the controls. A rule that
   has to guess a layout it cannot see is a rule that is wrong at some
   width nobody tested.

   NO FLIP CLASS EITHER. `.pick.opens-left{ left:auto; right:0 }` stood here and
   is deleted: theme.js's place() now MEASURES the panel against the
   viewport and sets `left` in pixels, clamped inside both edges. See the
   long note there — the flip failed in both directions because which side
   has room is not a property of the trigger.

   Nothing may reintroduce a fixed side here. `left` is written by
   place(); a rule setting `right` would fight it and win on the first
   paint. */

/* Wraps rather than shrinking: nine targets will not fit one phone
   row at a size a finger can hit, so they go to two rows instead. */
/* EXACTLY 40px TALL, LIKE EVERY OTHER CONTROL IN THIS ROW.

   This was the misalignment, and it survived several attempts to fix it
   because the number was never on this rule. It had `padding:3px` and a
   `1.5px` border wrapped around a 40px `.swatch`, and no height of its
   own — so with `box-sizing:border-box` its computed height was
   40 + 3 + 3 + 1.5 + 1.5 = 49px while the crystal pill, the pet pill and
   the perch were all 40. A `min-height:40px` added here earlier did
   nothing, because 49 already cleared it.

   The padding and border are gone rather than the swatch being shrunk to
   fit inside them: the coloured dot IS the control, and a frame around it
   only made it read smaller than its neighbours. `height`, not
   `min-height`, so nothing inside can push it out of line again. */
.themeswitch{
  display:flex;flex-wrap:wrap;justify-content:flex-end;align-items:center;
  gap:2px;
  height:40px;
  padding:0;
  border:0;
  border-radius:999px;
  background:transparent;
}
.swatch{
  width:40px;height:40px;               /* 40px tap target */
  padding:0;border:0;border-radius:var(--r-sm);background:transparent;
  cursor:pointer;display:grid;place-items:center;line-height:0;
  -webkit-tap-highlight-color:transparent;
}
/* THE DEFAULT IS 22px, AND THAT IS THE PANEL'S SIZE.

   The open Appearance panel lives INSIDE `#themeswitch`, so a bare
   `.swatch-dot` rule reaches its swatches too — sizing them to 38px made
   the dots overlap the "Aa" type samples beside them. Measured from
   Steven's screenshot, 09 Sep.

   The toolbar's own swatch is enlarged separately below, where it can be
   addressed without touching the panel.

   1px of half-transparent white on every dot: several themes are close
   enough to the page behind them that a plain circle dissolves into it,
   which is the other half of what Steven reported. It reads as a rim on
   a light theme and a highlight on a dark one, which is what a hairline
   at 50% does on both. */
.swatch-dot{
  display:block;width:22px;height:22px;border-radius:999px;
  border:1px solid rgb(255 255 255 / .5);
  box-shadow:inset 0 0 0 1.5px rgb(var(--ink-rgb) / .30);
  transition:transform .15s ease;
}
.swatch[aria-pressed="true"]{background:rgb(var(--sand-rgb) / .45)}
.swatch[aria-pressed="true"] .swatch-dot{
  box-shadow:inset 0 0 0 1.5px rgb(var(--ink-rgb) / .30), 0 0 0 2.5px var(--flame);
}
.swatch:hover .swatch-dot{transform:scale(1.12)}
.swatch:active .swatch-dot{transform:scale(.94)}

/* each dot shows its own theme's accent and card colour */
.swatch-sand   {background:linear-gradient(135deg,#FCD0A1 50%,#5E4B56 50%)}
.swatch-mint   {background:linear-gradient(135deg,#BAFFDF 50%,#4A5952 50%)}
.swatch-plum   {background:linear-gradient(135deg,#DCC9D8 50%,#54404E 50%)}
.swatch-orchid {background:linear-gradient(135deg,#FF9FE5 50%,#3C2F3B 50%)}
.swatch-moss   {background:linear-gradient(135deg,#FF9FE5 50%,#364823 50%)}
.swatch-lagoon {background:linear-gradient(135deg,#E1D89F 50%,#234548 50%)}
.swatch-tobacco{background:linear-gradient(135deg,#E1D89F 50%,#483E23 50%)}
.swatch-citrus {background:linear-gradient(135deg,#D4CB92 50%,#483323 50%)}
.swatch-grove  {background:linear-gradient(135deg,#D4CB92 50%,#2E4823 50%)}
/* the dark themes, previewed the same way: accent over ground */
.swatch-midnight{background:linear-gradient(135deg,#FF7A59 50%,#0B0E16 50%)}
.swatch-ember   {background:linear-gradient(135deg,#FF8A4C 50%,#120C09 50%)}
.swatch-pine    {background:linear-gradient(135deg,#FFA24C 50%,#080F0A 50%)}

@media (max-width:560px){
  /* five per row, two rows, still 40px each */
  .themeswitch{max-width:222px}
}

/* --- vocab: word tiles and picture options ------------------- */

.vword{
  font-family:var(--ff-display);
  font-size:clamp(1.5rem,5vw,2.1rem);
  color:var(--fg);
  text-align:center;
  margin:6px 0 22px;
}

.vpics{
  display:grid;
  grid-template-columns:repeat(2, minmax(0,1fr));
  gap:10px;
  margin-top:6px;
  max-width:min(100%, 460px);
  margin-inline:auto;
}
@media (min-width:561px){
  .vpics{grid-template-columns:repeat(4, minmax(0,1fr));max-width:min(100%, 800px)}
}

.vopt-pic{
  padding:6px;border:1.5px solid rgb(var(--sand-rgb) / .38);
  background:rgb(var(--paper-rgb) / .94);
  border-radius:var(--r-md);cursor:pointer;
  transition:transform .12s ease, border-color .12s ease;
  -webkit-tap-highlight-color:transparent;
}
.vopt-pic .sp{border-radius:var(--r-sm)}
.vopt-pic:hover{transform:translateY(-2px);border-color:var(--sand)}
.vopt-pic:active{transform:scale(.97)}

/* --- auto-play toggle --------------------------------------- */

.autotoggle{
  border:1.5px solid rgb(var(--sand-rgb) / .4);
  background:transparent;
  color:var(--fg-soft);
  font:600 0.92rem/1 var(--ff-ui);
  letter-spacing:.04em;text-transform:uppercase;
  padding:9px 13px;border-radius:999px;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.autotoggle[aria-pressed="true"]{
  background:rgb(var(--sand-rgb) / .22);
  color:var(--ink-on-dark);
  border-color:var(--soft-on-dark);
}
.autotoggle[aria-pressed="false"]{
  opacity:.55;
  text-decoration:line-through;
}

/* --- listen and pick ---------------------------------------- */

/* The cap is what sets the picture size: columns divide it, and each
   picture fills its column. Wide enough that the images are worth looking
   at, narrow enough that nine of them still fit on a screen. */
.lp-grid{
  display:grid;grid-template-columns:repeat(2, minmax(0,1fr));gap:10px;margin-top:14px;
  max-width:min(100%, 460px);margin-inline:auto;
}
.lp-grid.lp-nine{
  grid-template-columns:repeat(3, minmax(0,1fr));
  max-width:min(100%, 620px);
}
@media (min-width:561px){
  .lp-grid{grid-template-columns:repeat(4, minmax(0,1fr));max-width:min(100%, 800px)}
  .lp-grid.lp-nine{grid-template-columns:repeat(3, minmax(0,1fr));max-width:min(100%, 780px)}
}

.lp-pic{
  padding:5px;border:1.5px solid rgb(var(--sand-rgb) / .32);
  background:rgb(var(--paper-rgb) / .92);
  border-radius:var(--r-md);cursor:pointer;line-height:0;
  transition:transform .12s ease, border-color .12s ease, opacity .2s ease;
  -webkit-tap-highlight-color:transparent;
}
.lp-pic .sp{border-radius:var(--r-sm)}
.lp-pic:hover:not(:disabled){transform:translateY(-2px);border-color:var(--sand)}
.lp-pic:active:not(:disabled){transform:scale(.97)}
.lp-pic:disabled{opacity:.45;cursor:default}
.lp-pic.is-right{border-color:var(--yes);border-width:3px;padding:3px}
.lp-pic.is-dim{opacity:.32}

.lp-clock{height:5px;border-radius:999px;overflow:hidden;
  background:rgb(var(--shade-rgb) / .22);margin:12px 0 2px}
.lp-clock-bar{height:100%;background:var(--flame);border-radius:999px;
  transition:width .1s linear}

.lp-hint{color:var(--fg-soft);font-size:.9rem;text-align:center;margin:14px 0 0}
.lp-plays{color:var(--fg-soft);font-size:0.92rem;
  text-transform:uppercase;letter-spacing:.05em;align-self:center}
.lp-reveal{margin-top:12px}
.lp-word{color:var(--mint);font-family:var(--ff-display);
  font-size:clamp(1.2rem, 4.2vw, 1.55rem);text-align:center;margin:6px 0 0}
.lp-play:disabled{opacity:.45;cursor:default}

/* --- word list ---------------------------------------------- */

.ref-views{display:flex;flex-wrap:wrap;gap:6px;margin:14px 0 10px}
.ref-view{
  border:1.5px solid rgb(var(--mauve-rgb) / .45);
  background:rgb(var(--paper-rgb) / .8);
  color:var(--ink-soft);
  font:600 0.94rem/1 var(--ff-ui);
  padding:10px 13px;border-radius:999px;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.ref-view[aria-pressed="true"]{
  background:var(--liver);color:var(--on-dark);border-color:var(--liver);
}
.ref-hint{color:var(--fg-soft);font-size:0.97rem;margin:0 0 12px}

.ref-list{display:flex;flex-direction:column;gap:5px}

.ref-group{
  display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;
  margin:20px 0 6px;padding-bottom:6px;
  border-bottom:1.5px solid rgb(var(--mauve-rgb) / .35);
}
.ref-group-name{font:600 1.05rem/1.2 var(--ff-display);color:var(--fg)}
.ref-group-count{
  font:700 0.85rem/1 var(--ff-ui);color:var(--on-dark);
  background:var(--liver);padding:4px 8px;border-radius:999px;
}
.ref-group-note{font-size:0.92rem;color:var(--fg-soft);font-style:italic}

.ref-row{
  /* WRAPS, and it has to. `.ref-ex` — the example sentences revealed on
     tap — is appended as a THIRD child of this row by reference.js, so
     without wrapping it laid out BESIDE the word as a flex item: the body
     was squeezed to a couple of characters wide and the sentence printed
     over the top of it. Steven, with two screenshots: "words getting
     murdered when you clicked on them."

     Wrapping plus `flex:0 0 100%` on the example block below puts it on
     its own full-width line under the word, which is where it was always
     meant to be. Fixed here rather than by restructuring the DOM: the row
     is built in one pass and an inner wrapper would mean touching every
     caller of row().

     ALSO NOW OPAQUE. `rgb(var(--paper-rgb) / .9)` was a text box at 90%,
     so the page showed through the word list. Steven's rule: a box with
     text in it is never even partially transparent. */
  display:flex;flex-wrap:wrap;align-items:stretch;gap:9px;width:100%;
  padding:6px 8px;border:1.5px solid rgb(var(--mauve-rgb) / .28);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
  color:var(--fg);
  border-radius:var(--r-md);
}
.ref-row:hover{border-color:var(--sand-deep)}

.ref-thumb{
  flex:0 0 auto;line-height:0;padding:0;border:0;background:none;
  border-radius:6px;cursor:zoom-in;position:relative;
  -webkit-tap-highlight-color:transparent;
}
/* `.sp`, NOT `.sp`. sprite.js has always emitted `sp` — nothing in
   the app has ever carried a class called `sprite`, so this rule and 18
   others like it elsewhere in this file have never matched anything.

   Here it was not merely cosmetic. Without the 52px the thumbnail fell
   back to the base `.sp` rule at `width:100%`, and `.ref-thumb` is
   `flex:0 0 auto` with no width of its own — so a 100%-wide child of a
   zero-width parent collapsed and the picture was simply not there.
   Steven: "black doesn't show the picture for black, which does exist."
   It does exist: n:284 is sheet 32 position 5, the black paint strokes.
   The crop was right and the box was zero-wide.

   THE OTHER 18 ARE STILL DEAD. They are listed in HANDOFF; most only set
   a border-radius and cost nothing, but eight of them set a width that
   has never applied. Fixed here only, because flipping all nineteen at
   once re-sizes pictures on fifteen screens and that wants looking at
   rather than reasoning about. */
.ref-thumb .sp{width:52px;border-radius:6px;transition:opacity .15s ease}
.ref-thumb:hover .sp{opacity:.82}
/* --shade-rgb is white in the dark themes, so this badge cannot use it
   and then print white on top. Fixed black plate, fixed white glyph. */
.ref-thumb::after{
  content:'⤢';position:absolute;right:3px;bottom:3px;
  font-size:0.85rem;line-height:1;padding:2px 3px;border-radius:3px;
  background:rgba(0,0,0,.72);color:#fff;
  opacity:0;transition:opacity .15s ease;pointer-events:none;
}
.ref-thumb:hover::after,.ref-thumb:focus-visible::after{opacity:1}

.ref-body{
  display:flex;flex-direction:column;gap:1px;min-width:0;flex:1;
  text-align:left;background:none;border:0;padding:2px 0;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.ref-de{font:500 1.02rem/1.25 var(--ff-display);color:var(--fg)}
.ref-art{font-weight:700}
.art-der{color:var(--gen-m)}
.art-die{color:var(--gen-f)}
.art-das{color:var(--gen-n)}
.ref-gloss{font-size:.86rem;color:var(--fg-soft)}
.ref-gloss2{font-size:0.9rem;color:var(--fg-soft);opacity:.75;font-style:italic}
.ref-num{font:600 0.85rem/1 var(--ff-ui);color:var(--fg-soft);opacity:.6;margin-top:2px}

/* russian gender tag in the word list */
.ref-rg{
  display:inline-block;margin-left:7px;padding:1px 5px;border-radius:4px;
  font:700 0.85rem/1.5 var(--ff-ui);letter-spacing:.06em;vertical-align:1px;
}
.rg-m{background:rgba(47,111,168,.18);color:var(--gen-m)}
.rg-f{background:rgba(180,67,108,.18);color:var(--gen-f)}
.rg-n{background:rgba(92,122,58,.20);color:var(--gen-n)}
.rg-pl{background:rgb(var(--mauve-rgb) / .22);color:var(--ink-on-light)}

/* --- jump bar ----------------------------------------------- */

/* Wraps at every width. A sideways-scrolling row looks broken rather
   than scrollable when the last pill is sliced mid-word, and six long
   Russian labels will not fit one phone row at any font size worth
   reading. Two or three short rows beats one clipped one. */
/* Back to the top. Narrower than the labelled pills — it is a glyph,
   not a word, and at label width it would read as a section jump. */
.jump.is-up{
  padding:11px 13px;
  font-size:1.05rem;
  line-height:1;
}

/* The Table of Contents pill. Same shape as the eight jumps beside it,
   marked by colour only — it is the one button in the row that leaves the
   page rather than scrolling it, so it should read as related but not
   identical. */
.jump.is-toc{
  border-color:var(--flame);
  color:var(--flame);
  font-weight:700;
}

.jumpbar{
  position:sticky;top:0;z-index:20;
  display:flex;flex-wrap:wrap;gap:6px;
  /* 4px, not 10+6+4. See the note by `.filterwrap`. */
  padding:4px 0;margin:4px 0 2px;
  background:linear-gradient(var(--page) 74%, rgb(var(--paper-rgb) / 0));
}

.jump{
  border:1.5px solid rgb(var(--mauve-rgb) / .45);
  background:rgb(var(--paper-rgb) / .92);
  color:var(--ink);
  font:600 0.92rem/1 var(--ff-ui);
  padding:11px 14px;border-radius:999px;cursor:pointer;
  white-space:nowrap;
  -webkit-tap-highlight-color:transparent;
  transition:background .12s ease, border-color .12s ease;
}
.jump:hover{border-color:var(--sand-deep)}
.jump:active{background:var(--liver);color:var(--on-dark);border-color:var(--liver)}

/* ONE-TAP HOME / TOP ON EVERY SUBPAGE.

   Same ↑ vocabulary as the hub jump bar, but fixed in the navigation rail.
   It sits ABOVE Back: ↑ means "main site, top"; Back still means one level.
   app.js adds body.has-home-top only while a non-hub screen is open. */
.home-top{
  position:fixed;
  top:calc(8px + env(safe-area-inset-top, 0px));
  left:clamp(16px, 4vw, 40px);
  z-index:41;
  width:52px;
  height:52px;
  padding:0 !important;
  display:inline-flex;
  align-items:center;
  justify-content:center;
  border-radius:999px;
  font-size:1.18rem !important;
  line-height:1;
  box-shadow:0 3px 14px rgb(var(--scrim-rgb) / .3);
}

/* Make literal vertical room for the new control. Only subpages receive the
   class, so the hub's own jump bar is completely unchanged. */
body.has-home-top .backlink{
  top:calc(68px + env(safe-area-inset-top, 0px));
}

/* On a phone the labels are what make the row too wide, so drop the
   descriptive half and keep the number — 'Раздел 1' instead of
   'Раздел 1 · Предложения'. Set by app.js as a second span. */
@media (max-width:560px){
  .jump{font-size:0.87rem;padding:10px 12px}
  .jump-long{display:none}
}
@media (min-width:561px){
  .jump-short{display:none}
}

.hub-section{scroll-margin-top:76px}

/* --- topic filter ------------------------------------------- */

/* TIGHTER. Steven, 09 Sep: the filter row, the Table of Contents button
   and the jumpbar between the pet card and the sections were "a huge
   waste of space." Together they were carrying about 46px of vertical
   margin and padding for three rows of controls. */
.filterwrap{margin:6px 0 0}
.filter-row{display:flex;flex-wrap:wrap;gap:8px}

.filter-toggle{
  display:inline-flex;align-items:center;gap:8px;
  border:1.5px solid rgb(var(--mauve-rgb) / .45);
  background:rgb(var(--paper-rgb) / .9);
  color:var(--ink);
  font:600 0.94rem/1 var(--ff-ui);
  padding:12px 16px;border-radius:999px;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.filter-toggle:hover{border-color:var(--sand-deep)}
.filter-toggle.has{background:var(--liver);color:var(--on-dark);border-color:var(--liver)}

/* OPEN SAYS SO ON THE BUTTON ITSELF. Steven: "When you click filter by
   topic that button should glow to make it obvious that you clicked on
   it, not just pop out the filter filters."

   The only feedback was the caret flipping from ▾ to ▴ — a 0.85rem glyph
   at 80% opacity, which is not feedback on a phone.

   `[aria-expanded="true"]` rather than a new class: the button already
   sets that attribute for the state, so nothing in js/app.js needs to
   change and the visual state cannot drift out of step with the real one.

   A ring rather than a background, so it layers over `.has` (which turns
   the button liver when topics are selected) instead of fighting it.
   Declared AFTER `.has` because both selectors weigh the same and order
   decides. */
.filter-toggle[aria-expanded="true"]{
  background:color-mix(in srgb, var(--flame) 16%, var(--paper));
  border-color:var(--flame);
  color:var(--ink);
}
/* Topics selected AND open. `.has` paints the button liver with light
   text, so a pale tint would be unreadable here — darken the liver
   instead, which reads as the same "pressed" idea in the other
   direction. Two classes plus an attribute, so it outweighs both the
   rule above and `.has` on its own. */
.filter-toggle.has[aria-expanded="true"]{
  background:color-mix(in srgb, #000 18%, var(--liver));
  border-color:var(--flame);
  color:var(--on-dark);
}
.filter-caret{font-size:0.85rem;opacity:.8}
.filter-badge{
  background:var(--flame);color:var(--on-flame);
  font:700 0.85rem/1 var(--ff-ui);
  padding:4px 7px;border-radius:999px;
}

.chips{
  display:flex;flex-wrap:wrap;gap:6px;
  margin:12px 0 0;
}
.chip{
  border:1.5px solid rgb(var(--mauve-rgb) / .4);
  background:transparent;
  color:var(--fg);
  font:600 0.9rem/1 var(--ff-ui);
  padding:10px 13px;border-radius:999px;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  transition:background .12s ease, color .12s ease, border-color .12s ease;
}
.chip-glyph{font-size:.9rem}
.chip:hover{border-color:var(--sand-deep)}
.chip.on{background:var(--flame);color:var(--on-flame);border-color:var(--flame)}

.filter-hint{
  font:italic 400 0.92rem/1.4 var(--ff-ui);
  color:var(--fg);opacity:.75;
  margin:10px 0 0;
}

/* --- word list: collapsible groups --------------------------- */

.ref-tools{display:flex;justify-content:flex-end;margin:0 0 8px}
.ref-tool{
  border:1.5px solid rgb(var(--mauve-rgb) / .4);
  background:transparent;color:var(--fg-soft);
  font:600 0.87rem/1 var(--ff-ui);
  padding:9px 13px;border-radius:999px;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.ref-tool:hover{border-color:var(--sand-deep)}

button.ref-group{
  width:100%;text-align:left;cursor:pointer;
  background:rgb(var(--paper-rgb) / .55);
  border:1.5px solid rgb(var(--mauve-rgb) / .3);
  border-radius:var(--r-md);
  padding:12px 14px;margin:10px 0 4px;
  -webkit-tap-highlight-color:transparent;
}
button.ref-group:hover{border-color:var(--sand-deep)}
button.ref-group.open{
  background:var(--liver);border-color:var(--liver);
}
button.ref-group.open .ref-group-name,
button.ref-group.open .ref-group-caret{color:var(--fg)}
button.ref-group.open .ref-group-note{color:var(--fg-soft)}
/* sand at 30% over --liver lands mid-tone, where --on-dark measures 3.46
   in mint. Deepen the pill instead of lightening the text. */
button.ref-group.open .ref-group-count{
  background:rgb(var(--shade-rgb) / .35);color:var(--ink-on-dark);
}
/* One glyph that turns rather than two that swap — the open state is
   the rotation, so it has to be declared here now. */
.ref-group-caret{
  font-size:1rem;color:var(--fg-soft);margin-right:2px;
  display:inline-block;
  transition:transform .18s ease;
}
button.ref-group.open .ref-group-caret{ transform:rotate(90deg) }
@media (prefers-reduced-motion: reduce){
  .ref-group-caret{ transition:none }
}

/* --- word list: group jump row ------------------------------- */

.ref-jump{display:flex;flex-wrap:wrap;gap:6px;margin:4px 0 12px}
.ref-jump-btn{
  display:inline-flex;align-items:center;gap:6px;
  border:1.5px solid rgb(var(--mauve-rgb) / .38);
  background:rgb(var(--paper-rgb) / .85);
  color:var(--ink);
  font:600 0.87rem/1 var(--ff-ui);
  padding:9px 12px;border-radius:999px;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.ref-jump-btn:hover{border-color:var(--sand-deep)}
.ref-jump-btn.on{background:var(--liver);color:var(--on-dark);border-color:var(--liver)}
.ref-jump-n{
  font-size:0.85rem;font-weight:700;opacity:.7;
  background:rgb(var(--ink-rgb) / .1);padding:2px 6px;border-radius:999px;
}
.ref-jump-btn.on .ref-jump-n{background:rgb(var(--sand-rgb) / .3);opacity:1}


/* --- lightbox ------------------------------------------------ */



/* ======================================================================
   A LONG WORD MAY NOT PUSH A COLUMN OFF THE SCREEN.

   Steven, with a screenshot of the conveyor game: the third bin —
   ZUKUNFT / будущее — was cut off at the right edge and unreachable.

   TWO CAUSES, AND BOTH ARE DEFAULTS RATHER THAN MISTAKES.

   `1fr` IS `minmax(auto, 1fr)`. The `auto` minimum means a column will
   not shrink below its widest unbreakable word, so a three-column row
   holding VERGANGENHEIT stops being three equal columns and becomes
   three columns wider than the phone. `minmax(0, 1fr)` is what a fixed
   column count almost always means, and every `repeat(N, 1fr)` in this
   file is now that. It cannot change anything that already fitted — it
   only permits shrinking that was previously refused.

   AND A WORD STILL HAS TO BREAK once the column is narrow enough.
   `overflow-wrap:break-word` breaks one only when there is no other
   option, so ordinary text is unaffected; German compounds are exactly
   the case it exists for. Set here rather than per component because the
   next long compound will land somewhere nobody has thought about.

   `hyphens:auto` is deliberately NOT used: it needs a `lang` attribute
   matching the text, and the German here sits inside a page whose lang
   is Russian, so it would hyphenate by the wrong rules or not at all.
   ====================================================================== */
body{
  overflow-wrap:break-word;
}

/* ======================================================================
   NO OVERLAY MAY EVER CLIP ITS OWN TOP.  Steven: "make this never show
   up at all and be impossible in the site."

   THE BUG THIS FORBIDS. A full-screen overlay centres its panel with
   `align-items:center`. When the panel is taller than the viewport,
   centring pushes its top edge to a NEGATIVE offset — and negative
   offset cannot be scrolled to. `overflow-y:auto` does not save it: the
   scrollbar only reaches content below the start of the box. So the
   first rule of a five-rule How-to-play panel was permanently
   unreachable on a phone, and adding `overflow` looked like it had been
   handled. Centring is the bug.

   THE RULE, and it is one line of real CSS rather than a convention
   somebody has to remember: `align-items: safe center`. `safe` means
   "centre it, unless centring would push it out of the scrollable area,
   in which case start-align it." Exactly the behaviour wanted, decided
   by the browser per panel rather than by us guessing which panels get
   tall.

   TWO DECLARATIONS ON PURPOSE. A browser too old for `safe` throws the
   second one away and keeps `flex-start`, which is the safe direction —
   scrollable and slightly high rather than centred and unreachable. The
   `margin:auto` on each panel then re-centres it when it fits, so
   nothing that was working changes on any browser.

   THE SELECTOR IS THE POINT. `[class*="-overlay"]` catches every overlay
   in the app — howto, lightbox, pet grid, welcome, grammar, butler — and
   every one added later, without anyone knowing this rule exists. A new
   `.foo-overlay` is covered the moment it is named.

   Do not add `align-items:center` to an overlay. If a panel needs to be
   centred and can never be tall, it is already centred by the margin.
   ====================================================================== */
[class*="-overlay"]{
  align-items:flex-start;
  overflow-y:auto;
  /* iOS: keep momentum scrolling inside the overlay */
  -webkit-overflow-scrolling:touch;
}
[class*="-overlay"]{
  align-items:safe center;
}

/* ---- A CENTRED OVERLAY MUST NOT CLIP ITS OWN TOP ----------------------

   Steven, with a screenshot of the How-to-play panel: "This cut off the
   top and there is no way to scroll up. Make sure this cannot happen on
   any panel."

   THE TRAP, because it looks like it is already handled. `.howto-overlay`
   HAS `overflow-y:auto`. It still clipped, because `align-items:center`
   on a flex container whose child is taller than the viewport pushes the
   child's top edge to a NEGATIVE offset — and negative offset is not
   scrollable. The scrollbar only ever reaches content below the start of
   the box, so the first rule of a five-rule panel was permanently
   unreachable. Centring is the bug, not the missing overflow.

   THE FIX, and it is the standard one: `align-items:flex-start` plus
   `margin:auto` on the box. When the box FITS, `margin:auto` still
   centres it, so nothing that was working changes. When it does not fit,
   the box starts at the top of the scrollable area and every line can be
   reached.

   Applied to all four centred overlays, not just the one he hit — How to
   play, the lightbox, the pet grid, and the welcome screens. The welcome
   one matters most on a small phone: it is the first thing she ever sees
   and there is no way past it.

   `.gr-overlay` is already `display:block` with `overflow-y:auto`, which
   has never had this problem. Left alone.
   ------------------------------------------------------------------ */
.lb-overlay{
  position:fixed;inset:0;z-index:780;
  /* 780 — above every panel the user can open (language picker 770, how-to
     765, pet detail 758, grammar 755) and BELOW the tour bubble (900), so
     the Full Tour's Word List step can open the lightbox and then light
     `.lb-close` over it (the highlight ring is 810). It must stay below the
     bubble: step 61 points at `.lb-close`, and above the bubble that
     instruction would hide behind the very thing it describes.

     ONE EXCEPTION — `.lb-atop` (840): opened from the pet/achievement
     window (z 820), the lightbox must sit ABOVE that window, so those opens
     add this class. That window never coexists with a tour — you cannot buy
     a pet or earn an achievement mid-tour — so 840 there can never collide
     with the tour highlight. Steven, 11 Sep. */
  display:none;align-items:flex-start;justify-content:center;
  background:rgb(var(--scrim-rgb) / .88);
  padding:24px;
  overflow-y:auto;
}
.lb-overlay.lb-atop{ z-index:840; }
.lb-overlay.open{display:flex}
.lb-overlay.fade{animation:lb-in .18s ease}
@keyframes lb-in{from{opacity:0}to{opacity:1}}

.lb-close{
  position:absolute;top:14px;right:14px;
  width:44px;height:44px;border-radius:999px;
  border:1.5px solid rgba(255,255,255,.35);
  background:rgba(255,255,255,.12);color:#fff;
  font-size:1.1rem;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.lb-close:hover{background:rgba(255,255,255,.22)}

.lb-figure{display:flex;flex-direction:column;align-items:center;gap:16px;max-width:100%  /* centres it when it fits, top-aligns it when it does not */
  margin:auto;
}
.lb-pic{
  /* Bigger, not full-screen. Steven, 11 Sep — the source art is 1254px so
     it can be larger; the old 46vh cap kept it small on tall phones. This
     is a clear step up (was min(72vw, 46vh)) while leaving comfortable
     margins and room below for the caption and close button. */
  width:min(82vw, 58vh);
  border-radius:var(--r-lg);
  background-repeat:no-repeat;
  box-shadow:0 24px 60px -20px rgba(0,0,0,.7);
}
.lb-caption{text-align:center;color:#fff}
.lb-de{font-family:var(--ff-display);font-size:clamp(1.3rem,5vw,2rem);line-height:1.2}
.lb-gloss{font-size:.95rem;opacity:.8;margin-top:4px}
.lb-num{font-size:0.85rem;opacity:.45;margin-top:6px;letter-spacing:.08em}

/* --- catch the word ----------------------------------------- */

.cw-target{
  display:flex;align-items:center;gap:10px;flex-wrap:wrap;
  padding-bottom:12px;margin-bottom:2px;
  border-bottom:1.5px solid rgb(var(--sand-rgb) / .22);
}
.cw-target-label{
  font:600 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.08em;
  color:var(--fg-soft);
}
.cw-target-topic{
  display:inline-flex;align-items:center;
  font:700 1rem/1 var(--ff-ui);
  color:var(--on-dark);
  background:rgb(var(--sand-rgb) / .2);
  border:1.5px solid rgb(var(--sand-rgb) / .5);
  padding:8px 13px;border-radius:999px;
}
.cw-target-glyph{font-size:1.05rem}

.cw-status{
  display:flex;align-items:center;gap:14px;flex-wrap:wrap;
  padding:12px 0 14px;
}
.cw-hearts{display:inline-flex;gap:3px}
.cw-heart{width:19px;height:19px;fill:var(--flame)}
.cw-heart.spent{fill:rgb(var(--sand-rgb) / .22)}
.cw-score{font:700 .9rem/1 var(--ff-ui);color:var(--fg)}
.cw-left{font:600 0.9rem/1 var(--ff-ui);color:var(--fg-soft);
  text-transform:uppercase;letter-spacing:.06em}

.cw-grid{
  display:grid;grid-template-columns:repeat(3, minmax(0,1fr));gap:8px;
}
.cw-cell{
  aspect-ratio:1 / 1;
  display:flex;align-items:center;justify-content:center;
  padding:6px;border-radius:var(--r-md);
  border:1.5px dashed rgb(var(--sand-rgb) / .18);
  background:rgb(var(--shade-rgb) / .12);
  cursor:default;
  -webkit-tap-highlight-color:transparent;
  transition:background .12s ease, border-color .12s ease, transform .1s ease;
}
.cw-cell.cw-up{
  border-style:solid;
  border-color:var(--soft-on-dark);
  /* the theme's tinted light surface, not flat paper — every theme defines
     its own, so the game shifts with the palette */
  background:var(--sand-wash);
  cursor:pointer;
}
.cw-cell.cw-up:active{transform:scale(.95)}
.cw-cell.cw-hit{border-style:solid;background:var(--yes);border-color:var(--yes)}
.cw-cell.cw-wrong{border-style:solid;background:var(--flame);border-color:var(--flame)}

.cw-word{
  font-family:var(--ff-display);
  font-size:clamp(.9rem,3.1vw,1.05rem);
  line-height:1.15;text-align:center;
  color:var(--fg);
  word-break:break-word;
}
.cw-hit .cw-word{color:var(--fg)}
.cw-wrong .cw-word{color:var(--fg)}

/* This one class lands on two different backgrounds: catch the word puts it
   straight on the page, which is light, while spot-the-wrong-form and
   der-die-das put it inside the dark card. Dark ink on the dark card made
   it invisible in every theme. */
.cw-rules{
  color:var(--fg-soft);font-size:.85rem;line-height:1.6;
  max-width:52ch;margin:18px 0 0;
}
.card .cw-rules{
  color:var(--fg-soft);
}
.cw-final{
  font:700 1.3rem/1 var(--ff-ui);color:var(--fg-accent);margin:6px 0 0;
}

/* --- catch the word: the picture preview ---------------------- */

.cw-count{
  margin-left:auto;
  font:700 1.1rem/1 var(--ff-ui);
  color:var(--accent-on-light);
  background:rgb(var(--paper-rgb) / .9);
  min-width:34px;height:34px;
  display:inline-flex;align-items:center;justify-content:center;
  border-radius:999px;
}

/* preview tiles hold a picture and its word; play tiles are bare */
.cw-cell.cw-shot{
  aspect-ratio:auto;
  border-style:solid;
  border-color:var(--soft-on-dark);
  background:var(--sand-wash);
  flex-direction:column;gap:5px;padding:5px 5px 7px;
}
.cw-shot .sp{
  /* fills the tile; the 2:3 aspect ratio supplies the height, so the tile
     ends up the shape of the picture instead of a wide box with a stamp in
     the middle */
  width:100%;height:auto;aspect-ratio:2 / 3;
  border-radius:var(--r-sm);
}
.cw-shot-word{
  font-family:var(--ff-display);
  font-size:clamp(.85rem,2.4vw,.95rem);
  line-height:1.1;text-align:center;color:var(--fg);
  word-break:break-word;
}

/* empty play tiles rotate through the palette so the grid reads as
   waiting rather than broken */
/* Only the empty waiting tiles get tinted. Without the :not() guards these
   rules also repainted the tiles holding a word or a picture, which put dark
   text on a dark tint and made the words invisible. */
.cw-cell.is-empty:nth-child(3n+1){background:rgb(var(--sand-rgb) / .13)}
.cw-cell.is-empty:nth-child(3n+2){background:rgb(var(--mauve-rgb) / .15)}
.cw-cell.is-empty:nth-child(3n+3){background:rgb(var(--liver-rgb) / .16)}

/* a word of hers that got away — red flash on the way out */
.cw-cell.cw-missed{
  border-style:solid;
  border-color:var(--no);
  background:rgb(var(--no-rgb) / .34);
  animation:cw-miss .38s ease-out;
}
.cw-missed .cw-word{color:var(--fg);text-decoration:line-through}
@keyframes cw-miss{
  0%{transform:scale(1);opacity:1}
  40%{transform:scale(1.04)}
  100%{transform:scale(.97);opacity:.55}
}
@media (prefers-reduced-motion:reduce){
  .cw-cell.cw-missed{animation:none}
}

/* topic shown as a word instead of pictures */
.cw-bigtopic{
  display:flex;flex-direction:column;align-items:center;gap:8px;
  padding:36px 16px 30px;text-align:center;
}
.cw-bigtopic-glyph{font-size:3rem;line-height:1}
.cw-bigtopic-name{
  font-family:var(--ff-display);
  font-size:clamp(1.7rem,7vw,2.6rem);
  line-height:1.1;color:var(--fg);
}
.cw-bigtopic-sub{
  font:600 0.99rem/1 var(--ff-ui);
  text-transform:uppercase;letter-spacing:.08em;
  color:var(--fg-soft);
}

/* --- catch the word: the end screen --------------------------- */

.cw-done{text-align:center}
.cw-done-glyph{font-size:2.8rem;line-height:1;margin-bottom:6px}
.cw-done.is-perfect .cw-done-glyph{animation:cw-pop .5s ease-out}
@keyframes cw-pop{
  0%{transform:scale(.4);opacity:0}
  60%{transform:scale(1.18)}
  100%{transform:scale(1);opacity:1}
}
@media (prefers-reduced-motion:reduce){ .cw-done.is-perfect .cw-done-glyph{animation:none} }

.cw-done-topic{margin:2px 0 18px}
.cw-done-topic span{
  display:inline-flex;align-items:center;
  font:700 0.97rem/1 var(--ff-ui);
  text-transform:uppercase;letter-spacing:.06em;
  color:var(--fg-soft);
  border:1.5px solid rgb(var(--mauve-rgb) / .45);
  padding:8px 14px;border-radius:999px;
}

.cw-stats{
  display:flex;justify-content:center;gap:10px;flex-wrap:wrap;
  margin:0 0 14px;
}
.cw-stat{
  min-width:88px;padding:12px 10px;border-radius:var(--r-md);
  display:flex;flex-direction:column;gap:2px;
  border:1.5px solid rgb(var(--mauve-rgb) / .35);
}
.cw-stat-n{font:700 1.7rem/1 var(--ff-ui);color:var(--fg)}
.cw-stat-l{
  font:600 0.85rem/1 var(--ff-ui);text-transform:uppercase;
  letter-spacing:.07em;color:var(--fg-soft);
}
.cw-stat-good{background:rgb(var(--yes-rgb) / .22);border-color:rgb(var(--yes-rgb) / .55)}
.cw-stat-bad{background:rgb(var(--no-rgb) / .16);border-color:rgb(var(--no-rgb) / .5)}
.cw-stat-bad .cw-stat-n{color:var(--fg-soft)}

.cw-accuracy{
  font:700 1rem/1 var(--ff-ui);color:var(--fg);margin:0 0 22px;
}

.cw-review{margin:0 0 20px;text-align:left}
.cw-review-head{
  font:700 0.85rem/1.3 var(--ff-ui);text-transform:uppercase;letter-spacing:.07em;
  margin:0 0 9px;
}
.cw-review-missed{color:var(--fg-accent)}
.cw-review-wrong{color:var(--fg-soft)}
.cw-review-row{
  display:grid;grid-template-columns:repeat(auto-fill,minmax(88px,1fr));gap:8px;
}
.cw-review-item{
  display:flex;flex-direction:column;gap:4px;align-items:stretch;
  padding:5px 5px 8px;border-radius:var(--r-md);cursor:pointer;
  border:1.5px solid rgb(var(--mauve-rgb) / .35);
  background:var(--sand-wash);
  -webkit-tap-highlight-color:transparent;
  transition:transform .12s ease, border-color .12s ease;
}
.cw-review-item:hover{transform:translateY(-2px);border-color:var(--sand-deep)}
.cw-review-item:active{transform:scale(.97)}
.cw-review-item .sp{width:100%;height:auto;aspect-ratio:2 / 3;border-radius:var(--r-sm)}
.cw-review-de{
  font-family:var(--ff-display);font-size:0.97rem;line-height:1.15;
  color:var(--fg);text-align:center;word-break:break-word;
}
.cw-review-tr{
  font:400 0.85rem/1.2 var(--ff-ui);color:var(--fg-soft);text-align:center;
}

/* --- which form? ---------------------------------------------- */

.cg-prompt{
  display:flex;align-items:baseline;justify-content:center;gap:10px;flex-wrap:wrap;
  padding:26px 10px 22px;text-align:center;
}
.cg-person{
  font-family:var(--ff-display);
  font-size:clamp(1.5rem,6vw,2.2rem);line-height:1;
  color:var(--fg-accent);
}
.cg-person-gloss{
  font:600 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.07em;
  color:var(--fg-soft);
}
.cg-plus{font:400 1.2rem/1 var(--ff-ui);color:var(--fg-soft)}
.cg-verb{
  font-family:var(--ff-display);
  font-size:clamp(1.5rem,6vw,2.2rem);line-height:1;
  color:var(--fg);
}
.cg-answer{text-align:center}

.cg-table{
  background:var(--sand-wash);
  border:1.5px solid rgb(var(--sand-rgb) / .45);
  border-radius:var(--r-md);
  padding:12px 14px;margin:16px 0 4px;
  text-align:left;
}
.cg-table-head{
  display:flex;align-items:baseline;gap:9px;flex-wrap:wrap;
  padding-bottom:9px;margin-bottom:7px;
  border-bottom:1.5px solid rgb(var(--mauve-rgb) / .3);
}
.cg-table-verb{font-family:var(--ff-display);font-size:1.05rem;color:var(--fg)}
.cg-table-kind{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.07em;
  color:var(--on-flame);background:var(--flame);
  padding:4px 7px;border-radius:999px;
}
.cg-row{
  display:grid;grid-template-columns:5.2rem 1fr auto;gap:8px;align-items:baseline;
  padding:5px 7px;border-radius:var(--r-sm);
}
.cg-row.is-here{background:rgb(var(--flame-rgb) / .18);font-weight:700}
.cg-pron{font:600 0.97rem/1.3 var(--ff-ui);color:var(--fg-soft)}
.cg-form{font-family:var(--ff-display);font-size:1rem;color:var(--fg)}
.cg-row.is-here .cg-form{color:var(--fg-accent)}
.cg-gloss{font:400 0.85rem/1.3 var(--ff-ui);color:var(--fg-soft);opacity:.8}

/* conjugation game: the sentence prompt */
.cg-sentence{
  font-family:var(--ff-display);
  font-size:clamp(1.15rem,4.6vw,1.6rem);
  line-height:1.5;text-align:center;
  color:var(--fg);
  padding:22px 8px 4px;margin:0;
}
.cg-gap{
  display:inline-block;min-width:3.6em;
  border-bottom:2.5px solid var(--flame);
  color:var(--fg-accent);font-weight:700;
  margin:0 3px;
}
.cg-which{
  text-align:center;margin:6px 0 16px;
  font:600 0.87rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.09em;
  color:var(--fg-soft);
}
.cg-both{
  background:var(--sand-wash);
  border:1.5px solid rgb(var(--sand-rgb) / .45);
  border-radius:var(--r-md);padding:11px 13px;margin:12px 0 4px;text-align:left;
}
.cg-both-head{
  font:700 0.85rem/1.3 var(--ff-ui);text-transform:uppercase;letter-spacing:.07em;
  color:var(--fg-accent);margin:0 0 7px;
}
.cg-both-row{
  display:flex;justify-content:space-between;gap:10px;align-items:baseline;
  margin:0 0 4px;
}

/* --- progress ------------------------------------------------- */

.pg-head{text-align:center}
.pg-big{display:flex;flex-direction:column;align-items:center;gap:2px;padding:8px 0 4px}
.pg-big-n{font:700 3rem/1 var(--ff-ui);color:var(--fg-accent)}
.pg-big-l{
  font:600 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.08em;
  color:var(--fg-soft);
}
.pg-total{
  font:400 0.99rem/1.4 var(--ff-ui);color:var(--fg-soft);
  margin:8px 0 0;
}
.pg-h{
  font-family:var(--ff-display);font-size:1.05rem;
  color:var(--fg);margin:0;
}
.pg-area-head{
  display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  padding-bottom:10px;margin-bottom:6px;
  border-bottom:1.5px solid rgb(var(--sand-rgb) / .22);
}
.pg-area-pct{font:700 1.05rem/1 var(--ff-ui);color:var(--fg-accent)}

.pg-row{padding:9px 0;border-bottom:1px solid rgb(var(--sand-rgb) / .12)}
.pg-row:last-child{border-bottom:none}
.pg-row-top{
  display:flex;align-items:baseline;justify-content:space-between;gap:10px;
  margin-bottom:5px;
}
.pg-name{font-family:var(--ff-display);font-size:.96rem;color:var(--fg)}
.pg-pct{font:700 .88rem/1 var(--ff-ui);color:var(--fg)}
.pg-pct.is-low{color:var(--fg-accent)}

.pg-track{
  height:7px;border-radius:999px;overflow:hidden;
  background:rgb(var(--shade-rgb) / .35);
}
.pg-fill{height:100%;border-radius:999px;transition:width .3s ease}
.pg-recent{background:var(--flame)}

.pg-slip{
  display:flex;gap:7px;align-items:baseline;margin-top:5px;
  font:600 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.06em;
  color:var(--fg-soft);
}
.pg-was{opacity:.75;text-transform:none;letter-spacing:0;font-weight:400}
.pg-seen{
  font:400 0.85rem/1 var(--ff-ui);color:var(--fg-soft);margin-top:4px;
}
.pg-more{
  font:italic 400 0.94rem/1.4 var(--ff-ui);
  color:var(--fg-soft);margin:10px 0 0;
}

/* --- spot the wrong form -------------------------------------- */

.wf-banner{
  display:flex;align-items:center;gap:9px;
  padding:11px 13px;margin:0 0 4px;border-radius:var(--r-md);
  background:rgb(var(--flame-rgb) / .18);
  border:1.5px solid rgb(var(--flame-rgb) / .45);
}
.wf-banner.is-right{
  background:rgb(var(--sand-rgb) / .22);
  border-color:var(--soft-on-dark);
}
.wf-banner-l{
  font:700 0.9rem/1.3 var(--ff-ui);text-transform:uppercase;letter-spacing:.06em;
  color:var(--fg);
}

.wf-grid{display:flex;flex-direction:column;gap:8px}
.wf-cell{
  min-height:62px;padding:12px 14px;border-radius:var(--r-md);
  display:flex;align-items:center;text-align:left;
  border:1.5px dashed rgb(var(--sand-rgb) / .18);
  background:rgb(var(--shade-rgb) / .12);
  cursor:default;-webkit-tap-highlight-color:transparent;
  transition:background .12s ease, border-color .12s ease, transform .1s ease;
}
.wf-cell.wf-up{
  border-style:solid;border-color:var(--soft-on-dark);
  background:var(--sand-wash);cursor:pointer;
}
.wf-cell.wf-up:active{transform:scale(.98)}
.wf-cell.wf-hit{border-style:solid;background:var(--yes);border-color:var(--yes)}
.wf-cell.wf-wrongtap,.wf-cell.wf-missed{
  border-style:solid;background:var(--no);border-color:var(--no);
}
.wf-text{
  font-family:var(--ff-display);
  font-size:clamp(.94rem,3.6vw,1.12rem);line-height:1.35;
  color:var(--fg);
}
.wf-wrongtap .wf-text,.wf-missed .wf-text{color:var(--fg)}

.wf-fix{
  padding:9px 0;border-bottom:1px solid rgb(var(--mauve-rgb) / .2);
}
.wf-fix:last-child{border-bottom:none}
.wf-bad{
  font-family:var(--ff-display);font-size:.94rem;margin:0;
  color:var(--fg-accent);text-decoration:line-through;
}
.wf-good{
  font-family:var(--ff-display);font-size:.98rem;margin:2px 0 0;
  color:var(--fg);
}
.wf-rule{
  font:600 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.06em;
  color:var(--fg-soft);margin:5px 0 0;
}


/* --- how to play --------------------------------------------- */

.howto-btn{
  display:inline-flex;align-items:center;gap:7px;
  border:1.5px solid rgb(var(--sand-rgb) / .5);
  background:transparent;color:var(--fg);
  font:600 0.92rem/1 var(--ff-ui);
  padding:10px 14px;border-radius:999px;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.howto-btn:hover{border-color:var(--sand-deep);background:rgb(var(--sand-rgb) / .12)}
.howto-q{
  display:inline-flex;align-items:center;justify-content:center;
  width:17px;height:17px;border-radius:999px;
  background:var(--flame);color:var(--on-flame);
  font:700 0.85rem/1 var(--ff-ui);
}

.howto-overlay{
  position:fixed;inset:0;z-index:765
  /* 765, not 200 — above the tour scrim (700), below the
     bubble (900). the how-to / game-guide panel; a tour step ends inside it. A panel below the scrim gets
     dimmed during a tour and its contents cannot be lit, because a
     child cannot escape its ancestor’s stacking context. */;
  display:none;align-items:flex-start;justify-content:center;
  padding:20px;background:rgb(var(--scrim-rgb) / .72);
  overflow-y:auto;
}
.howto-overlay.is-open{display:flex}
.howto-box{
  background:var(--paper);
  border-radius:var(--r-lg);
  padding:20px 22px 18px;
  max-width:34rem;width:100%;
  box-shadow:0 18px 44px rgb(var(--scrim-rgb) / .4);
  /* centres it when it fits, top-aligns it when it does not */
  margin:auto;
}
.howto-head{
  display:flex;align-items:flex-start;justify-content:space-between;gap:12px;
  margin-bottom:12px;
}
.howto-head h2{
  font-family:var(--ff-display);font-size:1.3rem;line-height:1.2;
  color:var(--fg);margin:0;
}
.howto-x{
  border:none;background:transparent;cursor:pointer;
  font-size:1.05rem;line-height:1;color:var(--fg-soft);
  padding:4px 6px;border-radius:var(--r-sm);
}
.howto-x:hover{background:rgb(var(--mauve-rgb) / .22)}
.howto-list{
  margin:0 0 16px;padding-left:1.3em;
  color:var(--fg);font-size:.95rem;line-height:1.65;
}
.howto-list li{margin-bottom:8px}
.howto-list li:last-child{margin-bottom:0}

/* --- vocabulary: the review between the two phases -------------- */

.vr-score{
  display:flex;flex-direction:column;align-items:center;gap:2px;
  padding:6px 0 16px;
}
.vr-score-n{font:700 2.6rem/1 var(--ff-ui);color:var(--fg-accent)}
.vr-score-l{
  font:600 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.08em;
  color:var(--fg-soft);
}
.vr-clean{
  text-align:center;font-family:var(--ff-display);font-size:1.05rem;
  color:var(--fg);margin:0 0 4px;
}
.vr-head{
  font-family:var(--ff-display);font-size:1rem;color:var(--fg);
  margin:0 0 12px;padding-bottom:8px;
  border-bottom:1.5px solid rgb(var(--sand-rgb) / .22);
}
.vr-grid{
  display:grid;grid-template-columns:repeat(2, minmax(0,1fr));gap:12px;
  max-width:min(100%, 460px);margin:0 auto 16px;
}
@media (min-width:561px){
  .vr-grid{grid-template-columns:repeat(4, minmax(0,1fr));max-width:min(100%, 800px)}
}
.vr-item{display:flex;flex-direction:column;gap:4px}
.vr-pic{
  padding:5px;border:1.5px solid rgb(var(--sand-rgb) / .38);
  background:rgb(var(--paper-rgb) / .92);
  border-radius:var(--r-md);cursor:zoom-in;line-height:0;
  -webkit-tap-highlight-color:transparent;
}
.vr-pic .sp{border-radius:var(--r-sm)}
.vr-de{font-family:var(--ff-display);font-size:.95rem;color:var(--fg);line-height:1.25}
.vr-gloss{font:400 0.92rem/1.3 var(--ff-ui);color:var(--fg-soft)}
.vr-times{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.06em;
  color:var(--fg-accent);
}

/* --- vocabulary: the miss overlay ----------------------------- */

.vm-note{
  position:absolute;left:12px;right:12px;
  top:50%;transform:translateY(-50%);
  background:rgb(var(--scrim-rgb) / .96);
  border:2px solid var(--flame);
  border-radius:var(--r-lg);
  padding:20px 20px 18px;
  text-align:center;
  box-shadow:0 18px 44px rgb(var(--scrim-rgb) / .55);
  max-width:30rem;margin-inline:auto;
}
.vm-head{
  font-family:var(--ff-display);
  font-size:clamp(1.3rem,5vw,1.7rem);line-height:1.15;
  color:var(--fg-accent);margin:0 0 10px;
}
.vm-right{
  display:flex;flex-direction:column;gap:2px;
  margin:0 0 12px;
}
.vm-right-l{
  font:600 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.08em;
  color:var(--fg-soft);
}
.vm-right-w{
  font-family:var(--ff-display);
  font-size:clamp(1.15rem,4.4vw,1.45rem);line-height:1.2;
  color:var(--fg);
}
.vm-body{
  font:400 .98rem/1.6 var(--ff-ui);
  color:var(--fg-soft);
  margin:0 0 16px;
}
.vm-note .btn{width:100%}

/* the card becomes the positioning context for the overlay */
.card{position:relative}

/* --- der · die · das ------------------------------------------ */

.gn-figure{display:flex;justify-content:center;padding:14px 0 10px}
.gn-figure .sp{width:clamp(150px, 42vw, 210px);border-radius:var(--r-md)}

.gn-word{
  font-family:var(--ff-display);
  font-size:clamp(1.6rem,7vw,2.4rem);line-height:1.1;
  text-align:center;color:var(--fg);
  margin:0 0 18px;
}

.gn-row{display:grid;grid-template-columns:repeat(3, minmax(0,1fr));gap:9px}
.gn-btn{
  padding:16px 8px;border-radius:var(--r-md);
  border:1.5px solid rgb(var(--sand-rgb) / .5);
  background:var(--sand-wash);color:var(--ink-on-light);
  font-family:var(--ff-display);font-size:clamp(1.05rem,4.4vw,1.35rem);
  cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:transform .1s ease, background .12s ease, border-color .12s ease;
}
.gn-btn:hover:not(:disabled){border-color:var(--sand-deep);transform:translateY(-2px)}
.gn-btn:active:not(:disabled){transform:scale(.97)}
.gn-btn.is-right{background:var(--yes);border-color:var(--yes);color:var(--on-yes)}
.gn-btn.is-wrong{background:var(--no);border-color:var(--no);color:var(--on-no)}
.gn-btn:disabled{cursor:default}

.gn-verdict{
  margin-top:16px;padding:15px 16px 14px;
  border-radius:var(--r-md);
  background:rgb(var(--no-rgb) / .16);
  border:1.5px solid rgb(var(--no-rgb) / .45);
}
.gn-verdict.is-right{
  background:rgb(var(--yes-rgb) / .16);
  border-color:rgb(var(--yes-rgb) / .5);
}
.gn-verdict-head{
  font:700 0.97rem/1.35 var(--ff-ui);
  color:var(--fg);margin:0 0 10px;
}
.gn-pair{display:grid;grid-template-columns:1fr 1fr;gap:12px;margin-bottom:10px}
.gn-side{display:flex;flex-direction:column;gap:2px}
.gn-side-l{
  font:600 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.08em;
  color:var(--fg-soft);
}
.gn-side-w{font-family:var(--ff-display);font-size:1.05rem;color:var(--fg);line-height:1.2}
.gn-side-g{font:400 0.87rem/1.3 var(--ff-ui);color:var(--fg-soft)}
.gn-note{
  font:400 0.97rem/1.5 var(--ff-ui);
  color:var(--fg-soft);margin:0 0 12px;
}
.gn-verdict .btn{width:100%}

/* speed run */
.gn-score{font:700 1.5rem/1 var(--ff-ui);color:var(--fg-accent);margin-left:auto}
.gn-streak{
  font:700 .95rem/1 var(--ff-ui);color:var(--ink-on-dark);
  padding:5px 9px;border-radius:999px;background:rgb(var(--shade-rgb) / .3);
}
.gn-streak.is-hot{color:var(--ink-on-accent);background:var(--flame)}
.gn-clock{display:flex;align-items:center;gap:9px;margin:10px 0 4px}
.gn-clock-track{
  flex:1;height:6px;border-radius:999px;overflow:hidden;
  background:rgb(var(--shade-rgb) / .35);
}
.gn-clock-bar{height:100%;background:var(--sand-deep);transition:width .2s linear}
.gn-clock-n{
  font:700 .95rem/1 var(--ff-ui);color:var(--fg);min-width:2ch;text-align:right;
}
.gn-gained{
  text-align:center;margin:10px 0 0;
  font:700 1.6rem/1 var(--ff-ui);color:var(--yes);
}
.gn-final{font:700 3rem/1 var(--ff-ui);color:var(--fg-accent);margin:6px 0 4px}
.gn-final-sub,.gn-prev{
  font:400 .86rem/1.4 var(--ff-ui);color:var(--fg-soft);margin:0;
}

/* the locked / unlocked speed tile */
.gn-speed-tile{
  display:flex;align-items:center;gap:14px;width:100%;
  margin-top:14px;padding:16px 18px;
  border-radius:var(--r-lg);text-align:left;cursor:pointer;
  border:2px solid rgb(var(--sand-rgb) / .3);
  background:rgb(var(--shade-rgb) / .18);
  -webkit-tap-highlight-color:transparent;
  transition:transform .12s ease, border-color .12s ease;
}
.gn-speed-tile.is-locked{opacity:.55;cursor:default}
.gn-speed-tile.is-open{
  border-color:var(--flame);
  background:rgb(var(--flame-rgb) / .16);
}
.gn-speed-tile.is-open:hover{transform:translateY(-2px)}
.gn-speed-glyph{font-size:1.9rem;line-height:1}
.gn-speed-body{display:flex;flex-direction:column;gap:3px}
.gn-speed-name{font-family:var(--ff-display);font-size:1.15rem;color:var(--fg)}
.gn-speed-sub{font:400 0.99rem/1.35 var(--ff-ui);color:var(--fg-soft)}
.gn-speed-best{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.07em;
  color:var(--fg-accent);margin-top:2px;
}

.gn-unlock{
  margin:18px 0 4px;padding:18px;
  border-radius:var(--r-lg);text-align:center;
  border:2px solid var(--flame);
  background:rgb(var(--flame-rgb) / .18);
  display:flex;flex-direction:column;align-items:center;gap:5px;
}
.gn-unlock-glyph{font-size:2.4rem;line-height:1}
.gn-unlock-head{font-family:var(--ff-display);font-size:1.3rem;color:var(--fg-accent)}
.gn-unlock-sub{font:400 .86rem/1.45 var(--ff-ui);color:var(--fg-soft);margin-bottom:6px}

.gn-miss{
  display:flex;justify-content:space-between;gap:10px;align-items:baseline;
  padding:7px 0;border-bottom:1px solid rgb(var(--mauve-rgb) / .2);
}
.gn-miss:last-child{border-bottom:none}
.gn-miss-de{font-family:var(--ff-display);font-size:1rem;color:var(--fg)}
.gn-miss-ru{font:400 0.94rem/1.3 var(--ff-ui);color:var(--fg-soft)}

/* --- the record splash ---------------------------------------- */

.gn-splash{
  display:flex;flex-direction:column;align-items:center;text-align:center;
  padding:34px 20px 26px;
  border-radius:var(--r-lg);
  background:
    radial-gradient(120% 90% at 50% 0%, rgb(var(--flame-rgb) / .34), transparent 68%),
    rgb(var(--scrim-rgb) / .55);
  border:2px solid var(--flame);
  animation:gn-pop .34s cubic-bezier(.2,1.3,.5,1);
}
@keyframes gn-pop{
  from{ transform:scale(.9); opacity:0 }
  to  { transform:scale(1);  opacity:1 }
}

.gn-burst{display:flex;gap:6px;margin-bottom:6px}
.gn-spark{
  font-size:1.7rem;line-height:1;
  animation:gn-rise .7s ease both;
}
@keyframes gn-rise{
  0%  { transform:translateY(14px) scale(.5); opacity:0 }
  60% { transform:translateY(-4px) scale(1.15); opacity:1 }
  100%{ transform:translateY(0) scale(1); opacity:1 }
}

.gn-splash-kicker{
  font:700 0.94rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.14em;
  color:var(--fg-accent);margin:4px 0 6px;
}
.gn-splash-score{
  font:700 clamp(3.4rem,17vw,5.5rem)/1 var(--ff-ui);
  color:var(--fg);margin:0;
  text-shadow:0 6px 26px rgb(var(--flame-rgb) / .5);
  animation:gn-count .5s .1s ease both;
}
@keyframes gn-count{
  from{ transform:scale(.7); opacity:0 }
  to  { transform:scale(1);  opacity:1 }
}
.gn-splash-beat{
  font:600 .92rem/1.45 var(--ff-ui);
  color:var(--fg-soft);
  margin:8px 0 20px;max-width:26rem;
}
.gn-splash-stats{display:flex;gap:26px;margin-bottom:22px}
.gn-splash-stat{display:flex;flex-direction:column;gap:2px}
.gn-splash-stat-n{font:700 1.5rem/1 var(--ff-ui);color:var(--fg)}
.gn-splash-stat-l{
  font:600 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.08em;
  color:var(--fg-soft);
}
.gn-splash .done-actions{width:100%;max-width:22rem}
.gn-splash .btn{width:100%}

@media (prefers-reduced-motion: reduce){
  .gn-splash,.gn-spark,.gn-splash-score{animation:none}
}

/* --- songs ----------------------------------------------------- */

.sg-player{margin-bottom:14px}
.sg-audio{width:100%;height:38px;display:block}

.sg-body{margin-top:4px}
.sg-block{margin-bottom:22px}
.sg-block:last-child{margin-bottom:0}
.sg-block-head{
  display:flex;align-items:baseline;gap:8px;
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.11em;
  color:var(--fg-soft);
  margin:0 0 9px;padding-bottom:7px;
  border-bottom:1.5px solid rgb(var(--sand-rgb) / .2);
}
.sg-times{
  font:700 0.85rem/1 var(--ff-ui);letter-spacing:.04em;
  color:var(--on-flame);background:var(--flame);
  padding:3px 6px;border-radius:999px;
}
.sg-line{
  display:flex;gap:9px;align-items:baseline;width:100%;
  padding:7px 8px;margin:0 0 2px;
  background:none;border:0;border-radius:var(--r-sm);
  text-align:left;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  transition:background .12s ease;
}
.sg-line:hover{background:rgb(var(--sand-rgb) / .14)}
.sg-line:active{background:rgb(var(--sand-rgb) / .22)}
.sg-num{
  flex:0 0 1.9rem;
  font:600 0.85rem/1.5 var(--ff-ui);
  color:var(--fg-soft);
}
.sg-line-body{display:flex;flex-direction:column;gap:2px}
.sg-de{
  font-family:var(--ff-display);
  font-size:clamp(.98rem,3.7vw,1.14rem);line-height:1.4;
  color:var(--fg);
}
.sg-tr{
  font:400 clamp(.9rem, 2.7vw, 1.02rem)/1.5 var(--ff-ui);
  color:var(--fg-soft);
}
.sg-note{
  font:400 .85rem/1.55 var(--ff-ui);
  color:var(--fg-soft);
  margin:0 0 14px;
}

/* --- listen and pick: what she missed -------------------------- */

.lp-review{
  margin:20px 0 4px;padding:15px 15px 13px;
  border-radius:var(--r-md);
  background:rgb(var(--sand-rgb) / .16);
  border:1.5px solid rgb(var(--sand-rgb) / .4);
  text-align:left;
}
.lp-review-head{
  font:700 0.87rem/1.4 var(--ff-ui);
  color:var(--fg);margin:0 0 12px;
}
.lp-review-grid{
  display:grid;grid-template-columns:repeat(2, minmax(0,1fr));gap:10px;
  margin-bottom:12px;
}
@media (min-width:561px){
  .lp-review-grid{grid-template-columns:repeat(4, minmax(0,1fr))}
}
.lp-review-item{
  display:flex;flex-direction:column;gap:5px;
  padding:5px;border-radius:var(--r-md);
  border:1.5px solid rgb(var(--sand-rgb) / .34);
  background:rgb(var(--paper-rgb) / .9);
  cursor:pointer;text-align:left;
  -webkit-tap-highlight-color:transparent;
  transition:transform .12s ease, border-color .12s ease;
}
.lp-review-item:hover{transform:translateY(-2px);border-color:var(--sand-deep)}
.lp-review-item:active{transform:scale(.97)}
.lp-review-item .sp{border-radius:var(--r-sm)}
.lp-review-body{display:flex;flex-direction:column;gap:1px;padding:0 3px 3px}
.lp-review-de{
  font-family:var(--ff-display);font-size:.92rem;line-height:1.25;
  color:var(--fg);
}
.lp-review-gloss{font:400 0.87rem/1.3 var(--ff-ui);color:var(--fg-soft)}
.lp-review-flag{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.06em;
  color:var(--fg);margin-top:2px;
}
.lp-review .btn{width:100%}

/* a chorus coming round again: a link back to where it was written out */
.sg-recur{
  display:flex;align-items:center;gap:9px;width:100%;
  margin:0 0 22px;padding:10px 13px;
  border-radius:var(--r-md);
  border:1.5px dashed rgb(var(--sand-rgb) / .4);
  background:rgb(var(--shade-rgb) / .16);
  cursor:pointer;text-align:left;
  -webkit-tap-highlight-color:transparent;
  transition:border-color .12s ease, background .12s ease;
}
.sg-recur:hover{
  border-style:solid;border-color:var(--sand-deep);
  background:rgb(var(--sand-rgb) / .16);
}
.sg-recur-arrow{
  font:700 .95rem/1 var(--ff-ui);
  color:var(--fg-accent);
}
.sg-recur-label{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.11em;
  color:var(--fg);
}
.sg-recur-hint{
  margin-left:auto;
  font:400 0.87rem/1 var(--ff-ui);
  color:var(--fg-soft);
}

/* a song's title in her language — a title, not a caption */
/* on the page, which is light in nine themes and dark in three */
.practice-title p.sg-subtitle{
  margin:5px 0 0;
  font-family:var(--ff-display);
  font-size:clamp(1.05rem,4vw,1.4rem);
  line-height:1.25;
  font-weight:400;
  color:var(--fg);
}

/* --- the shared end-of-round screen ---------------------------- */

.es-done{text-align:center}
.es-glyph{font-size:2.6rem;line-height:1;margin-bottom:6px}
.es-done.is-perfect .es-glyph{animation:es-pop .4s cubic-bezier(.2,1.3,.5,1)}
@keyframes es-pop{ from{transform:scale(.6);opacity:0} to{transform:scale(1);opacity:1} }

.es-badge{margin:8px 0 0}
.es-badge span{
  display:inline-block;padding:5px 12px;border-radius:999px;
  border:1.5px solid rgb(var(--mauve-rgb) / .5);
  font:600 0.9rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.07em;
  color:var(--fg-soft);
}

.es-stats{
  display:flex;justify-content:center;gap:10px;flex-wrap:wrap;
  margin:16px 0 0;
}
.es-stat{
  min-width:5.2rem;padding:11px 14px;border-radius:var(--r-md);
  display:flex;flex-direction:column;gap:1px;
  border:1.5px solid transparent;
}
.es-stat-n{font:700 1.55rem/1 var(--ff-ui)}
.es-stat-l{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.08em;
  opacity:.78;
}
.es-stat-good{
  background:rgb(var(--yes-rgb) / .2);border-color:rgb(var(--yes-rgb) / .55);
}
.es-stat-good .es-stat-n{color:var(--fg-soft)}
.es-stat-bad{
  background:rgb(var(--no-rgb) / .16);border-color:rgb(var(--no-rgb) / .5);
}
.es-stat-bad .es-stat-n{color:var(--fg-soft)}

.es-note{
  font:600 .95rem/1.4 var(--ff-ui);color:var(--fg-soft);
  margin:12px 0 0;
}

/* --paper at 50% lands mid-tone in some themes and carries neither ink.
   The surface is --paper, so say so and pair it properly. */
.es-review{
  margin:22px 0 0;padding:14px 14px 12px;
  border-radius:var(--r-md);text-align:left;
  border:1.5px solid rgb(var(--mauve-rgb) / .34);
  background:var(--paper);
}
.es-review-missed{
  border-color:rgb(var(--no-rgb) / .45);
  background:rgb(var(--no-rgb) / .1);
}
.es-review-wrong{
  border-color:rgb(var(--mauve-rgb) / .45);
  background:rgb(var(--mauve-rgb) / .12);
}
.es-review-head{
  font:700 0.85rem/1.3 var(--ff-ui);text-transform:uppercase;letter-spacing:.09em;
  color:var(--fg-soft);margin:0 0 11px;
}

.es-grid{display:grid;grid-template-columns:repeat(2, minmax(0,1fr));gap:10px}
@media (min-width:561px){ .es-grid{grid-template-columns:repeat(4, minmax(0,1fr))} }
.es-list{display:flex;flex-direction:column;gap:2px}

.es-item{
  display:flex;flex-direction:column;gap:4px;
  padding:5px;border-radius:var(--r-md);text-align:left;
  border:1.5px solid rgb(var(--mauve-rgb) / .3);
  background:var(--paper);
  cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:transform .12s ease, border-color .12s ease;
}
.es-item:hover:not(:disabled){transform:translateY(-2px);border-color:var(--sand-deep)}
.es-item:active:not(:disabled){transform:scale(.97)}
.es-item:disabled{cursor:default}
.es-item .sp{border-radius:var(--r-sm)}
.es-list .es-item{
  flex-direction:row;align-items:baseline;gap:10px;
  border:0;background:none;padding:7px 6px;
  border-bottom:1px solid rgb(var(--mauve-rgb) / .2);border-radius:0;
}
.es-list .es-item:last-child{border-bottom:none}
.es-list .es-item:hover:not(:disabled){transform:none;background:rgb(var(--mauve-rgb) / .12)}

.es-item-body{display:flex;flex-direction:column;gap:1px;padding:0 3px 3px}
.es-list .es-item-body{flex-direction:row;align-items:baseline;gap:10px;padding:0;flex:1}
.es-item-strike{
  font-family:var(--ff-display);font-size:.9rem;
  color:var(--fg-soft);text-decoration:line-through;
}
.es-item-de{
  font-family:var(--ff-display);font-size:.95rem;line-height:1.25;
  color:var(--fg);
}
.es-item-gloss{font:400 0.9rem/1.3 var(--ff-ui);color:var(--fg-soft)}
.es-item-flag{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.06em;
  color:var(--fg-soft);margin-top:2px;
}
.es-hear{width:100%;margin-top:11px}

/* --- where or where to ----------------------------------------- */

.ww-figure{display:flex;justify-content:center;padding:10px 0 14px}
.ww-figure .sp{width:clamp(130px, 34vw, 180px);border-radius:var(--r-md)}

.ww-sentence{
  font-family:var(--ff-display);
  font-size:clamp(1.25rem,4.8vw,1.7rem);line-height:1.5;
  text-align:center;color:var(--fg);
  margin:0 0 6px;
}
.ww-gap{
  display:inline-block;min-width:3.4em;
  border-bottom:2.5px solid var(--flame);
  margin:0 3px;
}

.ww-row{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:20px}
.ww-btn{
  padding:18px 12px;border-radius:var(--r-md);
  border:1.5px solid rgb(var(--sand-rgb) / .5);
  background:var(--sand-wash);color:var(--ink-on-light);
  font-family:var(--ff-display);
  font-size:clamp(.98rem,3.6vw,1.16rem);line-height:1.3;
  cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:transform .1s ease, background .12s ease, border-color .12s ease;
}
.ww-btn:hover{border-color:var(--sand-deep);transform:translateY(-2px)}
.ww-btn:active{transform:scale(.97)}

/* the matched pair, which is the actual lesson */
.ww-pair{
  display:grid;gap:9px;margin:16px 0 12px;
  padding:13px 14px;border-radius:var(--r-md);
  background:var(--sand-wash);
  border:1.5px solid rgb(var(--sand-rgb) / .45);
}
@media (min-width:561px){ .ww-pair{grid-template-columns:1fr 1fr} }
.ww-side{
  display:flex;flex-direction:column;gap:2px;
  padding:9px 10px;border-radius:var(--r-sm);
  border:1.5px solid transparent;
}
.ww-side.is-this{
  background:rgb(var(--flame-rgb) / .16);
  border-color:rgb(var(--flame-rgb) / .45);
}
.ww-side-l{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.09em;
  color:var(--fg-soft);margin-bottom:2px;
}
.ww-side-de{font-family:var(--ff-display);font-size:1rem;line-height:1.35;color:var(--fg)}
.ww-side-tr{font:400 0.92rem/1.35 var(--ff-ui);color:var(--fg-soft)}

.ww-note{
  font:400 .92rem/1.55 var(--ff-ui);
  color:var(--fg-soft);
  margin:0 0 14px;text-align:left;
}

/* conjugation table rows are buttons now — each one speaks */
.cg-row{
  width:100%;border:0;background:none;text-align:left;
  cursor:pointer;font:inherit;
  -webkit-tap-highlight-color:transparent;
  transition:background .12s ease;
}
.cg-row:hover{background:rgb(var(--mauve-rgb) / .2)}
.cg-row:active{background:rgb(var(--mauve-rgb) / .3)}
.cg-row.is-here:hover{background:rgb(var(--flame-rgb) / .26)}
.cg-table-hint{
  margin-left:auto;
  font:400 0.85rem/1 var(--ff-ui);
  color:var(--fg-soft);opacity:.7;
}

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

.gr-lede{
  font:400 clamp(.98rem,3.2vw,1.12rem)/1.6 var(--ff-ui);
  color:var(--fg-soft);
  margin:0 0 20px;max-width:44rem;
}
.gr-group{
  font-family:var(--ff-display);
  font-size:clamp(1.05rem,3.6vw,1.25rem);
  color:var(--fg);
  margin:26px 0 10px;padding-bottom:7px;
  border-bottom:1.5px solid rgb(var(--sand-rgb) / .24);
}
.gr-note{
  font:400 .9rem/1.55 var(--ff-ui);
  color:var(--fg-soft);
  margin:0 0 14px;max-width:44rem;
}
.gr-preps{
  font-family:var(--ff-display);
  font-size:clamp(1rem,3.6vw,1.22rem);line-height:1.7;
  color:var(--fg);margin:0 0 18px;
}

.gr-stats{display:flex;gap:10px;margin:0 0 8px}
.gr-stat{
  flex:1;padding:13px 10px;border-radius:var(--r-md);
  background:var(--sand-wash);border:1.5px solid rgb(var(--sand-rgb) / .45);
  display:flex;flex-direction:column;gap:2px;align-items:center;
}
.gr-stat-a{font-family:var(--ff-display);font-size:1.2rem;color:var(--fg)}
.gr-stat-n{font:700 1.4rem/1 var(--ff-ui);color:var(--fg-accent)}

.gr-pairs{display:flex;flex-direction:column;gap:2px;margin-bottom:8px}
.gr-pair{
  display:flex;justify-content:space-between;gap:12px;align-items:baseline;
  width:100%;padding:9px 11px;border-radius:var(--r-sm);
  border:0;background:var(--sand-wash);text-align:left;
  cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:background .12s ease;
}
.gr-pair:hover{background:var(--sand)}
.gr-pair-de{font-family:var(--ff-display);font-size:1rem;color:var(--fg)}
.gr-pair-ru{font:400 0.97rem/1.3 var(--ff-ui);color:var(--fg-soft);text-align:right}

.gr-caserow{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-bottom:6px}
.gr-casecell{
  padding:14px 12px;border-radius:var(--r-md);
  display:flex;flex-direction:column;gap:4px;align-items:center;text-align:center;
  border:1.5px solid transparent;
}
.gr-dat{background:rgb(var(--sand-rgb) / .22);border-color:var(--soft-on-dark)}
.gr-acc{background:rgb(var(--flame-rgb) / .16);border-color:rgb(var(--flame-rgb) / .45)}
.gr-case-l{
  font:700 0.85rem/1.2 var(--ff-ui);text-transform:uppercase;letter-spacing:.07em;
  color:var(--fg-soft);
}
.gr-case-f{font-family:var(--ff-display);font-size:1.1rem;color:var(--fg)}

.gr-practise{
  margin-top:28px;padding-top:20px;
  border-top:1.5px solid rgb(var(--sand-rgb) / .24);
  text-align:center;
}
.gr-practise-l{
  font:600 .92rem/1.4 var(--ff-ui);
  color:var(--fg-soft);margin:0 0 12px;
}
.gr-practise .btn{width:100%;max-width:22rem}

/* in the grammar section the pair halves are tappable */
.gr-practise + *,
.card .ww-side{width:100%;text-align:left}
.card .ww-side:hover{background:rgb(var(--flame-rgb) / .1)}

/* --- build the sentence ----------------------------------------- */

.sc-model{
  font-family:var(--ff-display);
  font-size:clamp(1.2rem,4.4vw,1.6rem);line-height:1.5;
  text-align:center;color:var(--fg);
  margin:6px 0 14px;
}
.sc-figure{display:flex;justify-content:center;padding:4px 0 12px}
.sc-figure .sp{width:clamp(110px, 28vw, 150px);border-radius:var(--r-md)}

.sc-line{
  display:flex;flex-wrap:wrap;gap:7px;align-items:center;
  min-height:4.2rem;padding:13px;margin:8px 0 16px;
  border-radius:var(--r-md);
  border:1.5px dashed rgb(var(--sand-rgb) / .45);
  background:rgb(var(--shade-rgb) / .14);
}
.sc-line.is-right{border-style:solid;border-color:var(--yes);background:rgb(var(--yes-rgb) / .14)}
.sc-line.is-wrong{border-style:solid;border-color:var(--no);background:rgb(var(--no-rgb) / .12)}
.sc-empty{
  font:400 .9rem/1.4 var(--ff-ui);
  color:var(--fg-soft);
}

.sc-slot,.sc-word{
  font-family:var(--ff-display);
  font-size:clamp(1rem,3.8vw,1.2rem);line-height:1.2;
  padding:11px 14px;border-radius:999px;
  cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:transform .1s ease, background .12s ease, border-color .12s ease;
}
.sc-slot{
  background:var(--sand);border:1.5px solid var(--sand-deep);
  color:var(--ink-on-sand);
}
.sc-slot:hover{transform:translateY(-2px)}
.sc-slot:active{transform:scale(.96)}
/* Layered over --sand rather than replacing it, so the slot stays a slot
   instead of compositing onto the panel behind the tray. */
.sc-slot.is-off{
  background:
    linear-gradient(rgb(var(--no-rgb) / .3), rgb(var(--no-rgb) / .3)),
    var(--sand);
  border-color:var(--no);color:var(--ink-on-sand);
}

.sc-tray{display:flex;flex-wrap:wrap;gap:8px;justify-content:center}
.sc-word{
  background:var(--paper);border:1.5px solid rgb(var(--mauve-rgb) / .5);
  color:var(--ink-on-light);
}
.sc-word:hover{transform:translateY(-2px);border-color:var(--sand-deep)}
.sc-word:active{transform:scale(.96)}

.sc-answer{
  display:flex;flex-direction:column;gap:3px;
  padding:12px 14px;margin:0 0 10px;border-radius:var(--r-md);
  background:var(--sand-wash);border:1.5px solid rgb(var(--sand-rgb) / .45);
  text-align:left;
}
.sc-answer-l{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.08em;
  color:var(--fg-soft);
}
.sc-answer-de{font-family:var(--ff-display);font-size:1.1rem;line-height:1.4;color:var(--fg)}
.sc-note{
  font:400 .85rem/1.5 var(--ff-ui);
  color:var(--fg-soft);margin:0 0 14px;text-align:left;
}

/* --- plurals ---------------------------------------------------- */

.mz-q{text-align:center;padding:6px 0 4px}
.mz-figure{display:flex;justify-content:center;padding:6px 0 12px}
.mz-figure .sp{width:clamp(120px, 30vw, 160px);border-radius:var(--r-md)}
.mz-sg{
  font-family:var(--ff-display);
  font-size:clamp(1.5rem,6vw,2.1rem);line-height:1.15;
  color:var(--fg);margin:0;
}
.mz-gloss{
  font:400 clamp(.9rem,3vw,1.05rem)/1.4 var(--ff-ui);
  color:var(--fg-soft);margin:5px 0 0;
}
.mz-ask{
  font:600 0.94rem/1.3 var(--ff-ui);text-transform:uppercase;letter-spacing:.07em;
  color:var(--fg-soft);margin:16px 0 14px;
}

.mz-fams{display:grid;grid-template-columns:repeat(2, minmax(0,1fr));gap:9px}
@media (min-width:561px){ .mz-fams{grid-template-columns:repeat(5, minmax(0,1fr))} }
.mz-fam{
  padding:16px 8px;border-radius:var(--r-md);
  border:1.5px solid rgb(var(--sand-rgb) / .5);
  background:var(--sand-wash);color:var(--ink-on-light);
  font-family:var(--ff-display);
  font-size:clamp(.95rem,3.4vw,1.1rem);line-height:1.25;
  cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:transform .1s ease, border-color .12s ease;
}
.mz-fam:hover{border-color:var(--sand-deep);transform:translateY(-2px)}
.mz-fam:active{transform:scale(.97)}

.mz-verdict{
  margin-top:16px;padding:15px 16px 13px;border-radius:var(--r-md);
  background:rgb(var(--no-rgb) / .14);
  border:1.5px solid rgb(var(--no-rgb) / .42);
  text-align:left;
}
.mz-verdict.is-right{
  background:rgb(var(--yes-rgb) / .14);
  border-color:rgb(var(--yes-rgb) / .45);
}
.mz-pair{
  display:flex;flex-wrap:wrap;gap:10px;align-items:baseline;
  margin-bottom:10px;
}
.mz-pair-sg,.mz-pair-pl{
  font-family:var(--ff-display);
  font-size:clamp(1.05rem,3.8vw,1.3rem);color:var(--fg);
}
.mz-pair-arrow{color:var(--fg-soft);font-size:1.1rem}
.mz-why{
  font:400 .92rem/1.55 var(--ff-ui);
  color:var(--fg-soft);margin:0 0 6px;
}
.mz-die{
  font:700 0.92rem/1.4 var(--ff-ui);
  color:var(--yes);margin:10px 0 0;
}

/* grammar: plural patterns and word order */
.gr-banner{
  padding:11px 14px;margin:0 0 18px;border-radius:var(--r-md);
  background:rgb(var(--yes-rgb) / .16);
  border:1.5px solid rgb(var(--yes-rgb) / .45);
  font:700 .9rem/1.4 var(--ff-ui);
  color:var(--on-dark);
}
.gr-count{
  margin-left:auto;
  font:600 0.85rem/1 var(--ff-ui);
  color:var(--fg-soft);
}
.gr-group{display:flex;align-items:baseline;gap:10px}

.gr-sent{
  display:block;width:100%;text-align:left;
  padding:11px 13px;margin-bottom:5px;border-radius:var(--r-sm);
  border:0;background:var(--sand-wash);
  font-family:var(--ff-display);
  font-size:clamp(1rem,3.5vw,1.15rem);line-height:1.45;
  color:var(--ink-on-light);cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  transition:background .12s ease;
}
.gr-sent:hover{background:var(--sand)}
.gr-verb{
  background:rgb(var(--flame-rgb) / .3);
  border-radius:5px;padding:1px 5px;
  box-shadow:0 1px 0 var(--flame-dark);
}
.gr-sent-tr{
  display:block;margin-top:3px;
  font:400 0.94rem/1.35 var(--ff-ui);color:var(--fg-soft);
}

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

.st-toggle{margin:0 0 16px}
.st-stats{justify-content:flex-start;margin:14px 0 0}

.st-players{display:flex;flex-direction:column;gap:6px;margin:4px 0 12px}
.st-player{
  display:flex;align-items:center;gap:8px;
  padding:4px 6px;border-radius:var(--r-md);
  border:1.5px solid transparent;
}
.st-player.is-current{
  background:rgb(var(--sand-rgb) / .18);
  border-color:var(--soft-on-dark);
}
.st-pick{
  flex:1;display:flex;align-items:center;gap:10px;
  padding:11px 10px;border:0;background:none;text-align:left;
  cursor:pointer;-webkit-tap-highlight-color:transparent;
}
.st-dot{color:var(--fg-accent);font-size:.9rem;line-height:1}
.st-name{
  font-family:var(--ff-display);font-size:1.05rem;
  color:var(--fg);
}
.st-edit,.st-del{
  border:1.5px solid rgb(var(--sand-rgb) / .4);
  background:none;color:var(--fg-soft);
  font:600 0.87rem/1 var(--ff-ui);
  padding:8px 11px;border-radius:999px;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.st-edit:hover,.st-del:hover{border-color:var(--sand);color:var(--fg)}
.st-del{padding:8px 12px;font-size:1rem;line-height:.9}

.st-rename{display:flex;gap:8px;padding:4px 6px 10px}
.st-gender{display:flex;flex-direction:column;gap:5px;padding:2px 6px 12px}

/* Name and gender, asked at the top of the hub while either is missing.
   Styled as a card rather than a banner: it is a thing to answer, not a
   notice to dismiss. */
.who-card{
  display:flex;flex-direction:column;gap:8px;
  padding:16px 14px;margin:0 0 14px;
  border-radius:var(--r-md);
  border:2px solid var(--flame);
}
.who-head{
  margin:0;font-family:var(--ff-display);font-size:1.25rem;color:var(--fg);
}
.who-note{
  margin:0;font:400 .95rem/1.5 var(--ff-ui);color:var(--fg-soft);
}
.who-label{
  font:600 .98rem/1.2 var(--ff-ui);color:var(--fg);margin-top:6px;
}
.who-row{display:flex;gap:8px}
.who-input{
  flex:1;padding:10px 12px;border-radius:var(--r-sm);
  font:500 1rem/1.2 var(--ff-ui);
  color:var(--ink);background:var(--paper);
  border:1.5px solid var(--sand-deep);
}
.who-input:focus{outline:2px solid var(--flame);outline-offset:1px}
.who-toggle{align-self:flex-start}
.who-acts{display:flex;gap:8px;margin-top:4px}
.who-acts .btn{flex:1}

/* Filled in and closed: one line saying who the app thinks you are.
   Quieter than the open form — a border rather than the flame outline,
   because at this point it is a fact on display, not a question. */
.who-card:not(.is-blank):not(.is-open){
  padding:0;border:1.5px solid var(--sand-deep);
}
.who-summary{
  display:flex;align-items:center;gap:10px;width:100%;
  padding:11px 14px;border:0;background:none;cursor:pointer;text-align:left;
  border-radius:var(--r-md);
  -webkit-tap-highlight-color:transparent;
}
.who-summary-name{
  font-family:var(--ff-display);font-size:1.15rem;color:var(--fg);
}
.who-summary-g{
  font:500 1rem/1.2 var(--ff-ui);color:var(--fg-soft);flex:1;
}
/* --flame-dark measures 2.95 on paper in orchid, so emphasis here is
   weight and a rule, not a hue. */
.who-summary-edit{
  font:700 .95rem/1 var(--ff-ui);color:var(--fg);
  text-transform:lowercase;text-decoration:underline;text-underline-offset:3px;
}
.who-summary:hover{background:rgb(var(--sand-rgb) / .18)}
.who-summary:focus-visible{outline:2px solid var(--flame);outline-offset:2px}
.st-gender-l{font:600 0.97rem/1.2 var(--ff-ui);color:var(--fg-soft)}
.st-gender .st-toggle{align-self:flex-start}
.st-gender-note{margin:0}
.st-input{
  flex:1;font:inherit;font-size:1rem;
  padding:11px 13px;border-radius:var(--r-md);
  border:1.5px solid rgb(var(--sand-rgb) / .5);
  background:var(--paper);color:var(--ink);
}
.st-input:focus{outline:2px solid var(--flame);outline-offset:1px}

.st-warn{
  padding:13px 14px;margin:4px 0 10px;border-radius:var(--r-md);
  background:rgb(var(--no-rgb) / .16);
  border:1.5px solid rgb(var(--no-rgb) / .45);
  display:flex;flex-wrap:wrap;gap:8px;align-items:center;
}
.st-warn p{
  flex:1 1 100%;margin:0 0 4px;
  font:400 .9rem/1.45 var(--ff-ui);color:var(--fg);
}
.st-add{width:100%;margin-bottom:4px}

/* --- content packs ------------------------------------------------ */

.pk-bar{
  height:8px;border-radius:999px;overflow:hidden;
  background:rgb(var(--shade-rgb) / .3);
  margin:2px 0 6px;
}
.pk-fill{
  height:100%;background:var(--yes);
  transition:width .3s ease;
}
.pk-read{
  font:600 .92rem/1.45 var(--ff-ui);
  color:var(--fg-soft);margin:0 0 14px;
}

/* How many achievements, at a size that says it is the point of the page.
   It was the .practice-title subtitle — 0.8rem, muted, under a 3rem
   heading — which read as a stray line of debug. */
.aw-score{
  display:flex;align-items:baseline;gap:10px;margin:0 0 8px;
}
.aw-score-n{
  display:inline-flex;align-items:baseline;
  font-family:var(--ff-display);line-height:1;
}
.aw-score-got{ font-size:2.6rem;color:var(--fg); }
.aw-score-of{
  font-size:1.5rem;color:var(--fg-soft);margin:0 3px;
}
.aw-score-all{ font-size:1.5rem;color:var(--fg-soft); }
.aw-score-l{
  font:700 .85rem/1.2 var(--ff-ui);text-transform:uppercase;
  letter-spacing:.07em;color:var(--fg-soft);
}

/* What the achievements have paid. Shown only once they have paid
   something, so the page never reports a zero as if it were news. */
.aw-paid{
  display:flex;align-items:baseline;gap:7px;margin:0 0 14px;
}
.aw-paid-n{
  font:700 1.15rem/1 var(--ff-ui);color:var(--fg-accent);
}
.aw-paid-l{
  font:600 .92rem/1.2 var(--ff-ui);color:var(--fg-soft);
}

.pk-row{
  display:flex;align-items:center;gap:8px;
  margin-bottom:6px;
}
.pk-toggle{
  flex:1;display:flex;align-items:center;gap:11px;
  padding:12px 13px;border-radius:var(--r-md);
  border:1.5px solid rgb(var(--sand-rgb) / .3);
  background:rgb(var(--shade-rgb) / .14);
  text-align:left;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  transition:border-color .12s ease, background .12s ease;
}
.pk-row.is-on .pk-toggle{
  border-color:rgb(var(--yes-rgb) / .5);
  background:rgb(var(--yes-rgb) / .12);
}
.pk-toggle:hover:not(:disabled){border-color:var(--sand)}
.pk-toggle:disabled{cursor:default;opacity:.9}
.pk-mark{
  flex:0 0 1.3rem;height:1.3rem;line-height:1.3rem;text-align:center;
  border-radius:5px;font-size:.85rem;
  border:1.5px solid rgb(var(--sand-rgb) / .45);
  color:var(--fg);
}
.pk-row.is-on .pk-mark{background:var(--yes);border-color:var(--yes)}
.pk-body{display:flex;flex-direction:column;gap:1px}
.pk-name{font-family:var(--ff-display);font-size:1.02rem;color:var(--fg)}
.pk-sub{font:400 0.9rem/1.3 var(--ff-ui);color:var(--fg-soft)}

.pk-tip,.pk-wait{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.07em;
  padding:6px 9px;border-radius:999px;white-space:nowrap;
}
.pk-tip{background:var(--flame);color:var(--on-flame)}
.pk-wait{color:var(--fg-soft)}

.pk-suggest{
  margin:12px 0 4px;padding:12px 14px;border-radius:var(--r-md);
  background:rgb(var(--flame-rgb) / .16);
  border:1.5px solid rgb(var(--flame-rgb) / .45);
  font:600 .92rem/1.5 var(--ff-ui);
  color:var(--on-dark);
}

/* --- progress ------------------------------------------------------ */

.pv-head{display:grid;grid-template-columns:repeat(2, minmax(0,1fr));gap:9px;margin-bottom:14px}
@media (min-width:561px){ .pv-head{grid-template-columns:repeat(4, minmax(0,1fr))} }
.pv-tile{
  padding:14px 12px;border-radius:var(--r-md);
  display:flex;flex-direction:column;gap:2px;
  border:1.5px solid transparent;
}
.pv-tile-n{font:700 1.7rem/1 var(--ff-ui)}
.pv-tile-l{
  font:700 0.92rem/1.2 var(--ff-ui);text-transform:uppercase;letter-spacing:.06em;
}

/* The due tile is the one that does something, so it has to look like it.
   The others stay plain — a screen where everything looks tappable is a
   screen where nothing does. */
.pv-tile.is-live{
  cursor:pointer;text-align:left;font:inherit;position:relative;
  -webkit-tap-highlight-color:transparent;
}
.pv-tile.is-live:hover{border-color:var(--flame)}
.pv-tile.is-live.is-on{border-color:var(--flame);background:rgb(var(--flame-rgb) / .3)}
.pv-tile-go{
  position:absolute;top:10px;right:10px;
  font-size:0.94rem;line-height:1;color:var(--fg-accent);
}

/* the list behind the number */
.pv-due{
  margin:12px 0 4px;padding:14px;border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .18);
  border:1.5px solid rgb(var(--flame-rgb) / .35);
}
.pv-due-head{
  margin:0 0 10px;font:700 1rem/1.3 var(--ff-ui);color:var(--fg);
}
.pv-due-group{
  padding:10px 0;border-top:1px solid rgb(var(--sand-rgb) / .18);
}
.pv-due-group:first-of-type{border-top:0}
.pv-due-top{display:flex;align-items:baseline;gap:8px;margin-bottom:7px}
.pv-due-area{
  font-family:var(--ff-display);font-size:1.08rem;color:var(--fg);
}
.pv-due-n{
  font:700 .85rem/1 var(--ff-ui);color:var(--fg);
}
.pv-due-chips{display:flex;flex-wrap:wrap;gap:5px;margin-bottom:9px}
.pv-due-chip{
  display:inline-flex;align-items:baseline;gap:5px;
  padding:5px 10px;border-radius:999px;
  background:rgb(var(--sand-rgb) / .14);
  font:500 .9rem/1.2 var(--ff-ui);color:var(--ink-on-dark);
}
.pv-due-chip.is-late{
  background:rgb(var(--flame-rgb) / .22);color:var(--flame-pale);
}
.pv-due-chip.is-settled{
  background:rgb(var(--yes-rgb) / .16);color:var(--yes);
}
/* the settled panel is a good-news panel, so it is not outlined in flame */
.pv-settled{ border-color:rgb(var(--yes-rgb) / .35); }
.pv-settled-what{
  margin:0 0 8px;font:400 .92rem/1.5 var(--ff-ui);
  color:var(--fg-soft);
}
.pv-due-chip-d{font-size:0.9rem;opacity:.85}
.pv-due-go{width:100%}
.pv-due-nogame{margin:0}
.pv-act{background:rgb(var(--flame-rgb) / .2);border-color:rgb(var(--flame-rgb) / .5)}
.pv-act .pv-tile-n{color:var(--fg-accent)}
.pv-good{background:rgb(var(--yes-rgb) / .18);border-color:rgb(var(--yes-rgb) / .5)}
.pv-good .pv-tile-n{color:var(--yes)}
.pv-bad{background:rgb(var(--no-rgb) / .16);border-color:rgb(var(--no-rgb) / .45)}
.pv-bad .pv-tile-n{color:var(--no)}
.pv-calm{background:rgb(var(--shade-rgb) / .18);border-color:var(--soft-on-dark)}
.pv-calm .pv-tile-n{color:var(--fg)}
.pv-tile-l{color:var(--fg-soft)}

.pv-reading{
  font:600 clamp(.95rem,3.1vw,1.08rem)/1.55 var(--ff-ui);
  color:var(--fg-soft);
  margin:0 0 8px;
}
.pv-off{
  padding:11px 13px;border-radius:var(--r-md);margin:0 0 8px;
  background:rgb(var(--shade-rgb) / .22);
  border:1.5px solid rgb(var(--sand-rgb) / .3);
  font:400 .88rem/1.5 var(--ff-ui);color:var(--ink-on-dark);
}

.pv-areas{display:flex;flex-direction:column;gap:6px;margin-bottom:6px}
.pv-area{
  display:grid;grid-template-columns:1fr auto 5rem 3rem;gap:10px;align-items:center;
  padding:11px 13px;border-radius:var(--r-md);text-align:left;
  border:1.5px solid rgb(var(--sand-rgb) / .28);
  background:rgb(var(--shade-rgb) / .14);
  cursor:pointer;-webkit-tap-highlight-color:transparent;
}
.pv-area.is-open{border-color:var(--sand);background:rgb(var(--sand-rgb) / .16)}
.pv-area-n{font-family:var(--ff-display);font-size:1rem;color:var(--fg)}
.pv-area-c{font:400 0.9rem/1 var(--ff-ui);color:var(--fg-soft)}
.pv-area-p{font:700 0.99rem/1 var(--ff-ui);color:var(--fg);text-align:right}

.pv-bar{
  height:7px;border-radius:999px;overflow:hidden;
  background:rgb(var(--shade-rgb) / .32);
}
.pv-fill{height:100%;border-radius:999px}
.pv-fill-life{background:rgb(var(--sand-rgb) / .55)}
.pv-fill-now{background:var(--sand-deep)}

.pv-row{
  display:block;width:100%;text-align:left;
  padding:11px 13px;margin-bottom:5px;border-radius:var(--r-md);
  border:1.5px solid rgb(var(--sand-rgb) / .24);
  background:rgb(var(--shade-rgb) / .12);
  cursor:pointer;-webkit-tap-highlight-color:transparent;
}
.pv-row:hover{border-color:var(--soft-on-dark)}
.pv-row-head{display:flex;align-items:baseline;gap:9px;margin-bottom:7px}
.pv-name{
  font-family:var(--ff-display);font-size:1.02rem;
  color:var(--fg);flex:1;
}
.pv-when{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.06em;
  padding:5px 8px;border-radius:999px;white-space:nowrap;
  background:rgb(var(--sand-rgb) / .2);color:var(--soft-on-dark);
}
.pv-when.is-due{background:var(--flame);color:var(--on-flame)}
.pv-bars{display:flex;flex-direction:column;gap:3px}
.pv-foot{
  display:flex;gap:10px;margin-top:6px;
  font:400 0.87rem/1.3 var(--ff-ui);color:var(--fg-soft);
}
.pv-slip{color:var(--no);font-weight:700}
.pv-up{color:var(--yes);font-weight:700}
/* the row goes somewhere, so it looks like it does */
.pv-row.is-live:hover{border-color:var(--accent-on-dark)}
.pv-row:disabled{cursor:default;opacity:.75}
.pv-row-go{
  margin-left:auto;white-space:nowrap;
  font:700 0.9rem/1.2 var(--ff-ui);
  color:var(--fg-accent);
}
/* the scheduler's internals, for anyone who wants them */
.pv-why{
  margin-left:auto;flex:none;
  width:1.5rem;height:1.5rem;border-radius:999px;
  display:inline-flex;align-items:center;justify-content:center;
  font:700 0.9rem/1 var(--ff-ui);
  color:var(--ink-on-dark);
  background:rgb(var(--shade-rgb) / .25);
  cursor:pointer;
}
.pv-why:hover{color:var(--fg)}

.pv-detail{
  margin-top:9px;padding-top:8px;
  border-top:1px solid rgb(var(--sand-rgb) / .2);
}
.pv-detail p{
  margin:0;font:400 0.94rem/1.45 var(--ff-ui);
  color:var(--fg-soft);
}

.pv-chips{display:flex;flex-wrap:wrap;gap:5px}
.pv-chip{
  padding:6px 10px;border-radius:999px;
  background:rgb(var(--yes-rgb) / .16);
  border:1.5px solid rgb(var(--yes-rgb) / .4);
  font-family:var(--ff-display);font-size:.86rem;
  color:var(--on-dark);
}

/* --- the rolling week -------------------------------------------------

   Kronen per day for the last 7, and which of them were full days —
   requested directly. One row per day, a horizontal bar scaled to the
   busiest of the seven rather than to a fixed ceiling, so a quiet week and
   a heavy one both use the space they have. */
.pv-week{ margin:14px 0 4px }
.pv-week-tally{
  margin:0 0 10px;font:700 0.95rem/1.4 var(--ff-ui);color:var(--fg);
}
.pv-week-rows{display:flex;flex-direction:column;gap:6px}
.pv-week-row{
  display:grid;grid-template-columns:2.6rem 1fr 3.2rem 1.2rem;
  align-items:center;gap:9px;
  padding:6px 2px;
}
.pv-week-row.is-today{
  background:rgb(var(--shade-rgb) / .16);border-radius:var(--r-md);
  padding:6px 8px;
}
.pv-week-day{
  font:700 0.82rem/1.2 var(--ff-ui);color:var(--fg-soft);
  text-transform:uppercase;letter-spacing:.04em;
}
.pv-week-row.is-today .pv-week-day{color:var(--fg)}
.pv-week-bar{
  height:8px;border-radius:999px;
  background:rgb(var(--shade-rgb) / .2);overflow:hidden;
}
.pv-week-fill{
  height:100%;border-radius:999px;
  background:var(--fg-accent);
}
.pv-week-row.is-full .pv-week-fill{background:var(--yes)}
.pv-week-n{
  font:700 0.88rem/1 var(--ff-ui);color:var(--fg-soft);text-align:right;
  white-space:nowrap;
}
.pv-week-mark{
  font:700 1rem/1 var(--ff-ui);color:var(--yes);text-align:center;
}

/* --- the coach ------------------------------------------------------ */

.co-hello{
  margin:0 0 18px;padding:13px 16px;
  border-radius:var(--r-md);
  background:var(--paper);
  border:1.5px solid rgb(var(--sand-rgb) / .55);
  font:600 clamp(.98rem,3.2vw,1.12rem)/1.5 var(--ff-ui);
  color:var(--ink-on-light);
}

.co-wrap{
  position:fixed;inset:0;z-index:60;
  display:flex;align-items:flex-end;justify-content:center;
  padding:16px;
  background:rgb(var(--scrim-rgb) / .5);
  animation:co-fade .18s ease;
}
@media (min-width:561px){ .co-wrap{align-items:center} }
@keyframes co-fade{ from{opacity:0} to{opacity:1} }

.co-panel{
  width:100%;max-width:30rem;max-height:86vh;overflow-y:auto;
  padding:22px 20px 18px;border-radius:22px;
  background:var(--paper);
  border:2px solid var(--sand-deep);
  box-shadow:0 24px 60px -20px rgb(var(--ink-rgb) / .6);
  text-align:center;
  animation:co-rise .24s cubic-bezier(.2,1.1,.5,1);
}
@keyframes co-rise{ from{transform:translateY(14px);opacity:0} to{transform:none;opacity:1} }
.co-panel.is-cheer{max-width:24rem}

.co-glyph{font-size:1.9rem;line-height:1;display:block;margin-bottom:8px}
.co-head{
  font-family:var(--ff-display);
  font-size:clamp(1.15rem,4.2vw,1.4rem);line-height:1.25;
  color:var(--fg);margin:0 0 8px;
}
.co-line{
  font:400 clamp(.95rem,3.2vw,1.06rem)/1.6 var(--ff-ui);
  color:var(--fg-soft);margin:0 0 16px;
}
.co-sub{
  font:700 0.87rem/1.3 var(--ff-ui);text-transform:uppercase;letter-spacing:.07em;
  color:var(--fg-soft);margin:0 0 10px;text-align:left;
}

.co-words{
  display:grid;grid-template-columns:repeat(2, minmax(0,1fr));gap:8px;
  margin-bottom:18px;
}
@media (min-width:420px){ .co-words{grid-template-columns:repeat(4, minmax(0,1fr))} }
.co-word{
  display:flex;flex-direction:column;gap:3px;
  padding:5px;border-radius:var(--r-md);
  border:1.5px solid rgb(var(--mauve-rgb) / .35);
  background:rgb(var(--sand-rgb) / .12);
  cursor:pointer;text-align:left;
  -webkit-tap-highlight-color:transparent;
  transition:transform .12s ease, border-color .12s ease;
}
.co-word:hover{transform:translateY(-2px);border-color:var(--sand-deep)}
.co-word .sp{border-radius:var(--r-sm)}
.co-word-de{
  font-family:var(--ff-display);font-size:.88rem;line-height:1.2;
  color:var(--fg);padding:0 3px;
}
.co-word-tr{font:400 0.85rem/1.25 var(--ff-ui);color:var(--fg-soft);padding:0 3px 3px}

.co-acts{display:flex;gap:8px;justify-content:center;flex-wrap:wrap}
.co-acts .btn{flex:1;min-width:9rem}
.co-stack{flex-direction:column}
.co-stack .btn{width:100%}

.co-off{
  display:block;width:100%;margin-top:14px;
  border:0;background:none;cursor:pointer;
  font:400 0.94rem/1.4 var(--ff-ui);
  color:var(--fg-soft);text-decoration:underline;
  -webkit-tap-highlight-color:transparent;
}
.co-off:hover{color:var(--fg)}

/* the past-tense tables */
.gr-past{display:flex;flex-direction:column;gap:0;margin-bottom:16px}
.gr-past-row{
  display:grid;grid-template-columns:1fr 1fr 1.4fr;gap:8px;align-items:baseline;
  width:100%;padding:10px 12px;border:0;border-radius:var(--r-sm) var(--r-sm) 0 0;
  background:var(--sand-wash);text-align:left;cursor:pointer;
  font-family:var(--ff-display);
  -webkit-tap-highlight-color:transparent;
}
.gr-past-row:hover{background:var(--sand)}
.gr-past-row.is-sein{box-shadow:inset 3px 0 0 var(--flame)}
.gr-past-inf{font-size:1rem;color:var(--fg)}
.gr-past-pt{font-size:.95rem;color:var(--fg-soft)}
.gr-past-pp{font-size:.95rem;color:var(--fg)}
.gr-past-ex{
  padding:8px 12px 12px;margin-bottom:6px;
  border-radius:0 0 var(--r-sm) var(--r-sm);
  background:rgb(var(--sand-rgb) / .14);
  display:flex;flex-direction:column;gap:2px;
}
.gr-past-de{font-family:var(--ff-display);font-size:.95rem;color:var(--fg)}
.gr-past-tr{font:400 0.94rem/1.35 var(--ff-ui);color:var(--fg-soft)}

/* the future: time markers and the two styles side by side */
.gr-marks{display:flex;flex-wrap:wrap;gap:6px;margin-bottom:16px}
.gr-mark{
  display:flex;flex-direction:column;gap:1px;
  padding:8px 12px;border-radius:999px;
  border:1.5px solid rgb(var(--sand-rgb) / .45);
  background:var(--sand-wash);cursor:pointer;text-align:left;
  -webkit-tap-highlight-color:transparent;
}
.gr-mark:hover{background:var(--sand)}
.gr-mark-de{font-family:var(--ff-display);font-size:.95rem;color:var(--fg)}
.gr-mark-tr{font:400 0.85rem/1.2 var(--ff-ui);color:var(--fg-soft)}

.gr-fut{
  display:grid;grid-template-columns:1fr;gap:2px;margin-bottom:10px;
}
@media (min-width:561px){
  .gr-fut{grid-template-columns:1fr 1fr;gap:8px}
  .gr-fut-tr{grid-column:1 / -1}
}
.gr-fut-side{
  display:flex;flex-direction:column;gap:3px;
  padding:11px 13px;border:0;text-align:left;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.gr-fut-now{
  background:var(--sand-wash);
  border-radius:var(--r-sm) var(--r-sm) 0 0;
}
.gr-fut-futur{
  background:rgb(var(--flame-rgb) / .14);
  border-radius:0 0 var(--r-sm) var(--r-sm);
}
@media (min-width:561px){
  .gr-fut-now{border-radius:var(--r-sm)}
  .gr-fut-futur{border-radius:var(--r-sm)}
}
.gr-fut-side:hover{filter:brightness(1.06)}
.gr-fut-l{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.08em;
  color:var(--fg-soft);
}
.gr-fut-de{font-family:var(--ff-display);font-size:1rem;line-height:1.35;color:var(--fg)}
.gr-fut-tr{
  padding:2px 13px 8px;
  font:400 0.94rem/1.35 var(--ff-ui);color:var(--fg-soft);
}

/* numbered steps in the grammar pages */
.gr-step{
  display:flex;align-items:center;gap:10px;
  margin:28px 0 8px;padding-bottom:8px;
  border-bottom:1.5px solid rgb(var(--sand-rgb) / .24);
}
.gr-step-n{
  flex:0 0 1.7rem;height:1.7rem;line-height:1.7rem;text-align:center;
  border-radius:50%;
  background:var(--flame);color:var(--on-flame);
  font:700 0.97rem/1.7rem var(--ff-ui);
}
.gr-step-t{
  font-family:var(--ff-display);
  font-size:clamp(1.05rem,3.6vw,1.25rem);
  color:var(--fg);
}
.gr-step .gr-count{margin-left:auto}

/* --- the tense conveyor -------------------------------------------- */

.cv-stage{position:relative;overflow:hidden}
.cv-score-row{display:flex;align-items:center;gap:12px;margin-bottom:10px}
.cv-score{font:700 1.7rem/1 var(--ff-ui);color:var(--fg-accent)}
.cv-streak{
  font:700 .95rem/1 var(--ff-ui);color:var(--ink-on-dark);
  padding:5px 10px;border-radius:999px;background:rgb(var(--shade-rgb) / .3);
  margin-left:auto;
}
.cv-streak.is-hot{color:var(--ink-on-accent);background:var(--flame)}

.cv-clock{display:flex;align-items:center;gap:9px;margin-bottom:16px}
.cv-clock-track{
  flex:1;height:6px;border-radius:999px;overflow:hidden;
  background:rgb(var(--shade-rgb) / .35);
}
.cv-clock-bar{height:100%;background:var(--sand-deep);transition:width .2s linear}
.cv-clock-n{font:700 .95rem/1 var(--ff-ui);color:var(--fg);min-width:2ch;text-align:right}

/* the card behind, so it reads as a belt */
.cv-behind{
  margin:0 14px -8px;padding:12px 16px 20px;
  border-radius:var(--r-md) var(--r-md) 0 0;
  background:rgb(var(--sand-rgb) / .12);
  border:1.5px solid rgb(var(--sand-rgb) / .2);
  border-bottom:0;
  font-family:var(--ff-display);
  font-size:.9rem;line-height:1.3;
  color:var(--soft-on-dark);
  white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
}

.cv-card{
  position:relative;
  /* Light surface — declare the pairing so children (.cv-de, .cv-tr,
     .cv-marker, the bins) use dark ink, not a light --fg inherited from the
     dark app shell. Steven, 11 Sep (readability pass). */
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
  padding:20px 18px;border-radius:var(--r-lg);
  background:var(--paper);
  border:2px solid var(--sand-deep);
  box-shadow:0 10px 24px -12px rgb(var(--ink-rgb) / .6);
  display:flex;flex-direction:column;gap:5px;
  animation:cv-slide .22s ease;
}
@keyframes cv-slide{ from{transform:translateY(-10px);opacity:0} to{transform:none;opacity:1} }
.cv-card.is-right{border-color:var(--yes);box-shadow:0 0 0 4px rgb(var(--yes-rgb) / .35)}
.cv-card.is-wrong{border-color:var(--no);box-shadow:0 0 0 4px rgb(var(--no-rgb) / .3)}
.cv-marker{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.1em;
  color:var(--fg-accent);
}
.cv-de{
  font-family:var(--ff-display);
  font-size:clamp(1.15rem,4.4vw,1.5rem);line-height:1.4;
  color:var(--fg);
}
.cv-tr{font:400 0.99rem/1.4 var(--ff-ui);color:var(--fg-soft)}

.cv-flash{
  text-align:center;margin:12px 0 0;
  font:700 1.3rem/1 var(--ff-ui);
}
.cv-flash.is-right{color:var(--yes)}
.cv-flash.is-wrong{color:var(--no);font-size:1rem}

.cv-bins{display:grid;grid-template-columns:repeat(3, minmax(0,1fr));gap:8px;margin-top:18px}
.cv-bin{
  /* LIGHT SURFACE. Steven, 11 Sep: the bin labels were washed out — the
     bins have a light background (--sand-wash + a pale tint) but sit inside
     the dark .cv-card, so they inherited the card's LIGHT --fg/--fg-soft
     (meant for dark surfaces) and rendered light-on-light. Restating the
     light-surface pairing here flips the text back to dark ink, same as
     .surf-paper does. */
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
  padding:16px 6px;border-radius:var(--r-md);
  display:flex;flex-direction:column;gap:3px;align-items:center;
  border:2px solid rgb(var(--sand-rgb) / .45);
  background:var(--sand-wash);
  cursor:pointer;-webkit-tap-highlight-color:transparent;
  transition:transform .1s ease, background .12s ease, border-color .12s ease;
}
.cv-bin:hover{transform:translateY(-3px);border-color:var(--sand-deep)}
.cv-bin:active{transform:scale(.96)}
.cv-bin-de{
  /* clamped: a third of a 380px screen is about 115px, and
     VERGANGENHEIT does not fit that at the old fixed size. */
  font:700 clamp(0.62rem, 2.6vw, 0.85rem)/1.15 var(--ff-ui);
  letter-spacing:.03em;
  color:var(--fg);text-align:center;
  /* the compound may break rather than widen its column */
  overflow-wrap:anywhere;
}
.cv-bin-l{font:400 0.85rem/1 var(--ff-ui);color:var(--fg-soft)}
.cv-bin-past{background:linear-gradient(rgb(var(--mauve-rgb) / .22), rgb(var(--mauve-rgb) / .22)), var(--sand-wash)}
.cv-bin-present{background:linear-gradient(rgb(var(--sand-rgb) / .28), rgb(var(--sand-rgb) / .28)), var(--sand-wash)}
.cv-bin-future{background:linear-gradient(rgb(var(--flame-rgb) / .16), rgb(var(--flame-rgb) / .16)), var(--sand-wash)}

/* fill-blank: the streak and score line */
.fb-run{
  display:flex;align-items:center;gap:9px;
  margin-top:5px;
}
.fb-pct{
  font:700 0.97rem/1 var(--ff-ui);
  color:var(--fg);
}
.fb-streak{
  font:700 0.97rem/1 var(--ff-ui);
  color:var(--soft-on-dark);
  padding:4px 9px;border-radius:999px;
  background:rgb(var(--mauve-rgb) / .22);
}
.fb-streak.is-hot{
  background:var(--flame);color:var(--on-flame);
}
.fb-of{
  margin-left:auto;
  font:400 0.87rem/1 var(--ff-ui);
  color:var(--fg-soft);
}

/* vocabulary: the moment she gets one right.
   The picture she chose stays on screen and lights up, with the word on it,
   because the whole point is hearing the word while looking at the thing. */
.vopt-pic{position:relative}
.vopt-pic.is-got{
  outline:4px solid var(--yes);
  outline-offset:-2px;
  box-shadow:0 0 0 6px rgb(var(--yes-rgb) / .3);
  animation:vg-glow .3s ease;
}
@keyframes vg-glow{ from{box-shadow:0 0 0 0 rgb(var(--yes-rgb) / .6)} to{box-shadow:0 0 0 6px rgb(var(--yes-rgb) / .3)} }
.vopt-said{
  position:absolute;left:0;right:0;bottom:0;
  padding:8px 6px;
  background:var(--yes);
  color:var(--on-yes);
  font-family:var(--ff-display);
  font-size:clamp(.9rem,3.2vw,1.1rem);line-height:1.2;
  text-align:center;
  border-radius:0 0 var(--r-md) var(--r-md);
}

/* A column now, not a row: the tick and the streak on one line, the word
   and its meaning under them. */
.vg-got{
  display:flex;flex-direction:column;align-items:center;gap:6px;
  margin-top:14px;padding:11px 14px;
  border-radius:var(--r-md);
  background:rgb(var(--yes-rgb) / .18);
  border:1.5px solid rgb(var(--yes-rgb) / .5);
  animation:vg-pop .16s ease;
}
.vg-got-top{ display:flex;align-items:center;justify-content:center;gap:10px }
@keyframes vg-pop{ from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:none} }
.vg-got-tick{font:700 1.2rem/1 var(--ff-ui);color:var(--yes)}
.vg-got-word{
  font:700 .98rem/1 var(--ff-ui);
  color:var(--fg);
}
.vg-got-streak{
  margin-left:auto;
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.08em;
  color:var(--on-flame);background:var(--flame);
  padding:5px 10px;border-radius:999px;
}

/* the answer she picked, in the shape where options are words */
.option.is-got{
  background:var(--yes);
  border-color:var(--yes);
  color:var(--on-yes);
  font-weight:600;
  animation:vg-glow .3s ease;
}

/* --- the shared run line and acknowledgement ----------------------- */

.run-line{
  display:flex;align-items:center;gap:9px;
  margin-top:5px;min-height:1.4rem;
}
.run-pct{
  font:700 0.99rem/1 var(--ff-ui);
  color:var(--fg-soft);
}
.run-pct.is-good{color:var(--fg-soft)}
.run-streak{
  font:700 0.97rem/1 var(--ff-ui);
  color:var(--soft-on-dark);
  padding:4px 10px;border-radius:999px;
  background:rgb(var(--mauve-rgb) / .2);
  transition:background .15s ease;
}
.run-streak.is-hot{
  background:var(--flame);color:var(--on-flame);
}
.run-best{
  margin-left:auto;
  font:400 0.85rem/1 var(--ff-ui);
  color:var(--fg-soft);opacity:.8;
}

.run-note{
  display:flex;align-items:center;gap:10px;
  margin-top:14px;padding:11px 14px;
  border-radius:var(--r-md);
  animation:run-in .16s ease;
}
@keyframes run-in{ from{opacity:0;transform:translateY(4px)} to{opacity:1;transform:none} }
.run-note.is-right{
  background:rgb(var(--yes-rgb) / .18);
  border:1.5px solid rgb(var(--yes-rgb) / .5);
}
.run-note.is-wrong{
  background:rgb(var(--no-rgb) / .16);
  border:1.5px solid rgb(var(--no-rgb) / .45);
}
.run-note.is-big{
  background:rgb(var(--flame-rgb) / .2);
  border-color:var(--flame);
}
.run-note-mark{font:700 1.2rem/1 var(--ff-ui)}
.run-note.is-right .run-note-mark{color:var(--yes)}
.run-note.is-wrong .run-note-mark{color:var(--no)}
.run-note.is-big .run-note-mark{color:var(--fg-accent)}
.run-note-text{
  font:700 .98rem/1.3 var(--ff-ui);
  color:var(--fg);
}
.run-note.is-big .run-note-text{font-size:1.06rem}
.run-note-streak{
  margin-left:auto;
  font:700 0.87rem/1 var(--ff-ui);
  color:var(--on-flame);background:var(--flame);
  padding:5px 10px;border-radius:999px;
}

/* --- what is waiting, on the hub ----------------------------------- */

.nx-card{
  display:flex;flex-wrap:wrap;align-items:center;gap:12px;
  margin:0 0 22px;padding:15px 17px;
  border-radius:var(--r-md);
  background:rgb(var(--flame-rgb) / .14);
  border:1.5px solid rgb(var(--flame-rgb) / .45);
}
.nx-line{
  flex:1 1 12rem;margin:0;
  font:600 clamp(.96rem,3.1vw,1.08rem)/1.45 var(--ff-ui);
  color:var(--fg);
}
.nx-go{white-space:nowrap}

/* --- coins ---------------------------------------------------------- */

.co-earn{
  margin:16px 0 4px;padding:13px 15px;
  border-radius:var(--r-md);
  background:rgb(var(--flame-rgb) / .16);
  border:1.5px solid rgb(var(--flame-rgb) / .45);
}
.co-earn-head{display:flex;align-items:baseline;gap:8px;margin-bottom:8px}
.co-earn-n{font:700 1.5rem/1 var(--ff-ui);color:var(--fg-accent)}
.co-earn-l{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.09em;
  color:var(--fg-soft);
}
.co-earn-why{display:flex;flex-direction:column;gap:3px}
.co-earn-row{
  display:flex;justify-content:space-between;gap:12px;
  font:400 0.97rem/1.3 var(--ff-ui);color:var(--fg-soft);
}
.co-earn-plus{font-weight:700;color:var(--fg-accent)}

/* --- achievements ---------------------------------------------------- */

/* A row is a button now — tap it, get a description of what it asks for
   — so the defaults a bare <button> brings (shrink-to-fit width, centred
   text) are reset the same way .pv-row resets them, on top of the same
   look the row always had. */
.aw-row{
  display:flex;align-items:center;gap:11px;width:100%;text-align:left;
  padding:11px 13px;margin-bottom:5px;border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .14);
  border:1.5px solid rgb(var(--sand-rgb) / .22);
  cursor:pointer;-webkit-tap-highlight-color:transparent;
}
.aw-row:hover{border-color:var(--soft-on-dark)}
.aw-row.is-open{border-color:var(--sand-deep)}
.aw-row.is-got{
  background:rgb(var(--flame-rgb) / .14);
  border-color:rgb(var(--flame-rgb) / .4);
}
.aw-row.is-got.is-open{border-color:var(--flame-pale)}
.aw-mark{font-size:1.1rem;color:var(--fg-soft)}
.aw-row.is-got .aw-mark{color:var(--flame-pale)}
.aw-body{display:flex;flex-direction:column;gap:1px;flex:1;text-align:left}
.aw-name{font-family:var(--ff-display);font-size:1rem;color:var(--fg)}
.aw-when{font:400 .85rem/1.25 var(--ff-ui);color:var(--fg-soft)}
/* the description, shown only while its row is open */
.aw-desc{
  margin-top:5px;
  font:400 0.9rem/1.4 var(--ff-ui);color:var(--fg-soft);
  white-space:normal;
}
/* The payout is the reason to want the thing, so it is not decoration.
   It was .82rem sand at 45% — measured 2.79:1 in moss, below the floor
   in every theme.

   --flame is not available here either: on this row it measures 3.33 in
   mint. --flame-pale is the accent that survives every theme on a dark
   surface (6.95 worst on the row, 5.46 in the rule box). */
.aw-pay{
  font:700 1.05rem/1 var(--ff-ui);color:var(--flame-pale);white-space:nowrap;
}
.aw-pay.is-dim{color:var(--fg-soft)}

/* What a full day is. Nine of the nineteen achievements are counted in
   full days and nothing on the page said what one was. */
.aw-rule{
  display:flex;gap:9px;align-items:flex-start;
  margin:0 0 14px;padding:11px 13px;border-radius:var(--r-md);
  background:rgb(var(--flame-rgb) / .14);
  border:1.5px solid rgb(var(--flame-rgb) / .35);
}
.aw-rule-glyph{font-size:1.1rem;line-height:1.3;color:var(--flame-pale)}
.aw-rule-text{
  margin:0;font:500 .95rem/1.5 var(--ff-ui);color:var(--fg);
}
.aw-rule-text b{color:var(--flame-pale);font-weight:700}

.aw-won{
  display:flex;align-items:center;gap:11px;
  margin-top:10px;padding:12px 14px;border-radius:var(--r-md);
  background:rgb(var(--flame-rgb) / .2);
  border:1.5px solid var(--flame);
  animation:run-in .2s ease;
}
.aw-won-glyph{font-size:1.3rem;color:var(--fg-accent)}
.aw-won-body{display:flex;flex-direction:column;gap:1px;flex:1}
.aw-won-name{font:700 .98rem/1.2 var(--ff-ui);color:var(--fg)}
.aw-won-sub{
  font:700 0.85rem/1 var(--ff-ui);text-transform:uppercase;letter-spacing:.09em;
  color:var(--fg);
}
.aw-won-pay{font:700 1rem/1 var(--ff-ui);color:var(--fg-accent)}

/* --- the store ------------------------------------------------------- */

.pt-grid{
  display:grid;grid-template-columns:1fr;gap:12px;margin-bottom:8px;
}
@media (min-width:430px){ .pt-grid{grid-template-columns:repeat(2, minmax(0,1fr))} }
@media (min-width:760px){ .pt-grid{grid-template-columns:repeat(3, minmax(0,1fr))} }

/* ---- THE TABLE OF CONTENTS, IN COLLAPSIBLE GROUPS ---------------

   Steven: "Also need a way to collapse a lot of this stuff and expand it
   so it's not a giant list. You have to scroll down through like the old
   pet store... collapsible groups would be a great idea, with an obvious
   expand button and a description for each section."

   The flat version was forty-eight rows under four headings, which put
   Reference three screens down — a table of contents doing the very
   thing it exists to prevent. Now the four section buttons and the four
   group headings fit one screen, and a group opens where it stands. */
/* THE HEADING AND ITS GUIDE BUTTON, side by side.

   `.toc-head` is a <button> and so is the guide button, and a button
   inside a button is invalid HTML whose clicks fight each other — the
   same trap reference.js hit with its thumbnail and its word. So they are
   siblings in a plain wrapper instead.

   The wrapper carries the heading's own top margin and the heading drops
   its own, so a group with a guide button sits exactly where a group
   without one does. */
.toc-head-row{
  display:flex; align-items:stretch; gap:8px;
  margin:14px 0 0;
}
.toc-head-row .toc-head{ margin:0; flex:1 1 auto; min-width:0 }

/* A question mark, sized to the 44px tap target the rest of the app uses
   rather than to the glyph. Same border and paper as the heading it sits
   beside, so the pair reads as one control with two halves. */
.toc-head-guide{
  flex:none; width:44px;
  display:grid; place-items:center;
  border-radius:var(--r-md);
  border:1.5px solid rgb(var(--mauve-rgb) / .4);
  background:rgb(var(--paper-rgb) / .9);
  color:var(--ink);
  font:600 1.1rem/1 var(--ff-ui);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
/* No :hover on the ? guide buttons — the border-color cue was negligible and
   an unguarded hover caused the iOS "two taps to open" bug (first tap = hover,
   second = click). Removed entirely; touch fires on the first tap. Steven, Sep. */

/* The Table of Contents button every section guide carries, at the foot
   of the screen. Full width and spaced off the list above it, matching
   `.gd-open`/`.rg-open` at the other end. */
.gd-toc{ width:100%; margin:20px 0 0 }

.toc-head{
  display:flex; align-items:center; gap:10px;
  width:100%; margin:14px 0 0; padding:12px 14px;
  text-align:left;
  border-radius:var(--r-md);
  border:1.5px solid rgb(var(--mauve-rgb) / .4);
  background:rgb(var(--paper-rgb) / .9);
  color:var(--ink);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.toc-head.is-open{ border-color:var(--flame) }
.toc-head-l{ flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:3px }
.toc-head-t{ font:var(--t-card); letter-spacing:var(--t-card-ls) }
/* The description. This is what makes a CLOSED group useful: without it
   a collapsed group is just a word. */
.toc-head-s{
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--ink-soft);
}
/* How many are inside — the other half of deciding whether to open it. */
/* FIXED WIDTH, NOT VARIABLE. The badge sits between the chevron and the
   title, so any change in its width moves the title — and a badge that
   sized itself to its content started "Lessons" 4px right of "Reference".
   Wide enough for three digits, so nothing here is ever variable again
   however long a group gets.

   `tabular-nums` on top of that, so the digits themselves are equal
   width and a 1 does not sit in less room than an 8. Manrope carries
   tabular figures; `.purse-n` already relies on the same thing. The
   fixed width is what guarantees the alignment, though — the numerals
   only stop the number looking off-centre inside it. */
.toc-head-n{
  flex:0 0 auto;
  width:40px; padding:3px 0;
  border-radius:999px;
  background:rgb(var(--mauve-rgb) / .22);
  font:700 .8rem/1.3 var(--ff-ui);
  font-variant-numeric:tabular-nums;
  text-align:center;
  color:var(--ink);
}
/* Turns rather than swapping character, same as the store's.

   No circle and no background fill. A filled glyph inside a filled
   circle is the play button, and this control does not play anything —
   the whole heading is the hit target and this only shows which way the
   group is folded. A bare chevron that turns is the pattern every
   settings list and FAQ already uses. */
.toc-caret{
  flex:0 0 auto;
  width:22px; height:22px;
  display:flex; align-items:center; justify-content:center;
  font:700 1rem/1 var(--ff-ui);
  color:var(--ink-soft);
  transition:transform .18s ease;
}
.toc-head.is-open .toc-caret{ transform:rotate(90deg) }
@media (prefers-reduced-motion: reduce){ .toc-caret{ transition:none } }

/* A row inside an open group, indented so the grouping reads without a
   second container. */
.toc-list .toc-row{ margin-left:12px }

/* ==== THE STORE DROPS THE THEME ENTIRELY =========================

   Steven, looking at it on the phone: "Absolutely lose with prejudice
   all colors and identity of the themes in the store... Look how much
   fucking wasted space that is lost to theme cruft that can bugger off
   and die. I want the beautiful background images edge to edge."

   He is right on both counts. `.card` gave the store a `--liver`
   gradient and clamp(22px,5vw,38px) of padding on every side, so the
   velvets were postage stamps in a wide olive frame, and the tier
   headings sat on a translucent theme wash that made their text nearly
   unreadable.

   TWO LOOKS, NOT TWELVE. Light for the nine light themes, dark for
   midnight, ember and pine. Every colour in here resolves from these
   four variables and none of them come from the theme.

   EDGE TO EDGE. The card keeps its vertical padding and loses its
   horizontal padding, so a tier section spans the full width of the
   card and the velvet reaches both edges. Anything that is text gets its
   own side padding back, individually, below. */
:root{
  --store-page:#EFEAE1;
  --store-plate:#FAF7F1;
  --store-ink:#221F1B;
  --store-ink-soft:#5C554C;
}
html[data-theme="midnight"],
html[data-theme="ember"],
html[data-theme="pine"]{
  --store-page:#141317;
  --store-plate:#1F1E22;
  --store-ink:#F4F1EA;
  --store-ink-soft:#A8A29A;
}

/* THE PAGE ITSELF, while the store is open. `st-mode` is put on <html>
   by store.js and taken off on every route out. This is what kills the
   last of the theme: the gradient behind the card, the ink in the header,
   and the strip above and below.

   `!important` on the body background because `body` declares a
   three-layer gradient and a single override would otherwise lose to it
   on specificity of the shorthand. Used here and nowhere else. */
html.st-mode body{
  background:var(--store-page) !important;
  color:var(--store-ink);
}
html.st-mode .st-head h1,
html.st-mode .st-head p{ color:var(--store-ink) }
html.st-mode .st-head p{ color:var(--store-ink-soft) }
html.st-mode .st-head .backlink{
  background:var(--store-plate);
  color:var(--store-ink);
  border-color:rgb(0 0 0 / .14);
}
html[data-theme="midnight"].st-mode .st-head .backlink,
html[data-theme="ember"].st-mode .st-head .backlink,
html[data-theme="pine"].st-mode .st-head .backlink{
  border-color:rgb(255 255 255 / .16);
}

.st-card{
  background:var(--store-page);
  color:var(--store-ink);
  padding:16px 0;
  box-shadow:none;
  /* FULL BLEED. `main` puts clamp(16px,4vw,40px) of padding down each
     side, so even with the card's own padding gone there were 16 to 40px
     of theme page showing beside every velvet. Pulling the card out by
     exactly that amount takes it to the screen edges, and the square
     corners stop it reading as a card sitting on a page. */
  margin:0 calc(-1 * clamp(16px, 4vw, 40px));
  border-radius:0;
}
/* Text needs its margins back — only the velvet wants the full width. */
.st-card > h2,
.st-card > h3,
.st-card > p,
.st-card > .gr-note,
.st-card > .pt-tokens,
.st-card > button{ margin-left:16px; margin-right:16px }
.st-card > p, .st-card > .gr-note{ color:var(--store-ink-soft) }

/* THE HEADING IS AN OPAQUE TILE. Steven: "The text of each section has
   to be on a tile that is either light or dark depending on the theme."
   It was a translucent tint over the card, which on a mid-green page
   left the label fighting the background. Solid plate, tier-coloured
   text, tier-coloured border — nothing translucent anywhere. */
/* THE BAND TAKES THE TIER COLOUR. Steven: "I do like how Epic sits on a
   border row, but make that purple for epic, Blue for Rare, Green for
   common, and amber for Legendary, not this grey color."

   18% of the tier colour mixed over the plate — enough that the band
   reads as green/blue/purple/amber at a glance, while staying a solid
   opaque ground for the label rather than a wash over the velvet. The
   border and the tier name were already tier-coloured; the band makes it
   three, which is the point: the row should say which tier it is before
   the word is read. */
/* THE STRIPE IS THE TIER'S COLOUR; THE PILL IS READABLE.

   Steven: "Make that the color of that tier. The pill behind the text is
   totally fine. That has to be set so the text is readable. But the rest
   of that stripe should be the color for their tier."

   So two elements doing two jobs. `.gr-band` is a full-bleed stripe of
   the tier colour at full strength, butting straight against the velvet
   above and below it — no store-page background anywhere on the screen.
   `.gr-pill` is the readable ground the text sits on, and it keeps the
   plate colour, which is what the tier-coloured text was contrast-checked
   against when the tier palette was chosen.

   The band wraps the heading AND the run note, so an open tier is one
   solid stripe rather than a heading and then a second strip. */
.st-card .gr-band{ background:var(--store-plate) }
.st-card .gr-band.gr-common   { background:var(--tier-common) }
.st-card .gr-band.gr-rare     { background:var(--tier-rare) }
.st-card .gr-band.gr-epic     { background:var(--tier-epic) }
.st-card .gr-band.gr-legendary{ background:var(--tier-legendary) }

/* The heading is now just a transparent hit area filling the band. */
.st-card .gr-group{
  margin:0;
  width:100%;
  border:0;
  border-radius:0;
  padding:12px 16px;
  background:transparent;
}
.st-card .gr-pill{
  display:inline-flex; align-items:center; gap:10px;
  padding:8px 14px;
  border-radius:999px;
  background:var(--store-plate);
  color:var(--store-ink);
}
/* The note gets the same treatment for the same reason — its text would
   otherwise sit straight on a saturated stripe. */
.st-card .gr-note{
  display:inline-block;
  margin:0 16px 12px;
  padding:8px 14px;
  border-radius:var(--r-md);
  background:var(--store-plate);
  color:var(--store-ink);
}
/* The tier tint that used to sit on the heading itself is GONE — the
   colour is the band's job now, and `.st-card .gr-group.gr-<tier>` at
   three classes outranked `background:transparent` at two, so leaving it
   would have kept a second tinted layer over the band. */
.st-card .gr-group .gr-count{ color:var(--store-ink-soft) }
/* Colour only. The background overrides that used to sit here painted
   the circle in the store's own palette; the circle is gone, and a
   background with no border-radius would paint a square instead. */
.st-card .gr-caret{ color:var(--store-ink-soft) }

/* The velvet, full width and square-edged at the sides so it truly
   reaches the card's edges. */
/* A collapsed tier is one big tap target — see the note in store.js.
   `is-tap` is only on the collapsed one, so the cursor tells the truth:
   the open section is not clickable as a whole. */
.st-card .gr-sect.is-tap{ cursor:pointer }

/* No margins: the velvet meets the coloured band directly. Its own
   margins were the other half of the pale stripe. */
.st-card .gr-sect{
  margin:0;
  border-radius:0;
  background:var(--store-page);
}
.st-card .gr-sect.has-bg{ padding:12px }

/* NO PLATE. Steven: "I would lose that grey background and have the pet
   cards just sit on the colorful background.. that grey background
   obscures much of the nice background image and doesn't add value."

   The plate existed so nothing readable sat over a photograph. That
   reason does not apply here — every pet card is itself opaque
   (`--card-paper`), so all the text is already on a solid ground and the
   plate was covering the velvet for nothing. Padding stays, so the cards
   do not touch the card's edges. */
.st-card .gr-plate{
  background:transparent;
}

/* The pet card uses the same two-look palette, so `--card-*` simply
   follows `--store-*` rather than being a second set to keep in step. */
.st-card{
  --card-paper:var(--store-plate);
  --card-ink:var(--store-ink);
  --card-ink-soft:var(--store-ink-soft);
  /* The edge too, or the four tier borders would color-mix against a
     theme colour and the cards would still be following the page. */
  --card-edge:rgb(0 0 0 / .14);
}
html[data-theme="midnight"] .st-card,
html[data-theme="ember"] .st-card,
html[data-theme="pine"] .st-card{ --card-edge:rgb(255 255 255 / .16) }

/* ---- THE STORE HAS ITS OWN IDENTITY ------------------------------

   Steven: "The store, the background for the pets which has an image,
   and the pet cards are allowed to have their own visual identity. It
   doesn't have to borrow from the theme of the entire site... the store
   is a special place where you buy pets. You don't spend all day there
   so it should have its own visual identity, unique to itself."

   So a pet card has TWO looks, not twelve. `--card-paper` and
   `--card-ink` are declared here and NOT taken from the theme, and the
   three dark themes swap them. Every colour on the card resolves from
   those two.

   WHY THIS IS THE RIGHT CALL AND NOT JUST A SHORTCUT. The card now sits
   on a velvet photograph rather than on the page, so the theme's paper is
   no longer the surface it has to sit against. And the rarity colours
   have to be readable on the card — with twelve papers that was a
   twelve-way compromise which put legendary amber at 4.33, under the 4.5
   a small label needs. Against two known backgrounds the colours can be
   chosen properly instead.

   The card is the ONLY thing that opts out. The hub, the reader, the
   games and everything else still follow the theme exactly. */
:root{
  --card-paper:#FAF7F1;
  --card-ink:#221F1B;
  --card-ink-soft:#5C554C;
  --card-edge:#E2DACE;
}
html[data-theme="midnight"],
html[data-theme="ember"],
html[data-theme="pine"]{
  --card-paper:#1F1E22;
  --card-ink:#F4F1EA;
  --card-ink-soft:#A8A29A;
  --card-edge:#3A3740;
}

.pt-card{
  display:flex;flex-direction:column;gap:9px;align-items:center;
  padding:16px 14px;border-radius:var(--r-md);
  background:var(--card-paper);
  border:2px solid var(--card-edge);
  color:var(--card-ink);
  text-align:center;
}
/* Every line on the card, in the card's own ink rather than the theme's.
   Listed together so nothing on a card can quietly go back to following
   the page. */
.pt-card .pt-name,
.pt-card .pt-word,
.pt-card .pt-tr{ color:var(--card-ink) }
.pt-card .pt-full,
.pt-card .pt-about,
.pt-card .pt-locked-cost,
.pt-card .pt-form{ color:var(--card-ink-soft) }
.pt-card.is-own{border-color:rgb(var(--yes-rgb) / .55)}
.pt-card.is-picked{
  border-color:var(--flame);
  box-shadow:0 0 0 3px rgb(var(--flame-rgb) / .25);
}
/* The tier tints must sit ON the card's light ground, not replace it.

   These were `background: rgb(var(--mauve-rgb) / .18)` and so on. A
   translucent background does not composite over the declaration it
   overrides — it composites over whatever is behind the element, which
   here is the dark panel the grid sits in. So common stayed light and
   readable while rare, epic and legendary went dark under text that is
   still --ink. Layering the tint over --sand-wash in the same
   declaration is what the original intent actually requires. */
/* Each tier tints its card, and the tint is the tier's own colour rather
   than the theme's mauve and flame.

   THESE COME AFTER `.pt-card` AND ARE EQUALLY SPECIFIC, so they win the
   background outright — which is why they had to move onto the card's
   palette too. Left alone they would have kept painting `--sand-wash`
   over `--card-paper` and the card would have gone on following the
   theme no matter what `.pt-card` said.

   `color-mix` over the card's own paper, so one rule works for both the
   light and the dark card: the tint is a percentage of the tier colour
   laid over whatever `--card-paper` currently is. A browser without
   color-mix falls back to the flat paper, which is the previous look
   rather than a broken one. */
.pt-common{ background:var(--card-paper) }
.pt-rare{
  background:color-mix(in srgb, var(--tier-rare) 8%, var(--card-paper));
}
.pt-epic{
  background:color-mix(in srgb, var(--tier-epic) 10%, var(--card-paper));
}
.pt-legendary{
  background:color-mix(in srgb, var(--tier-legendary) 12%, var(--card-paper));
  border-color:color-mix(in srgb, var(--tier-legendary) 45%, var(--card-edge));
}
/* The other three take a matching edge, so the four tiers differ by more
   than a word and a wash. */
.pt-rare{ border-color:color-mix(in srgb, var(--tier-rare) 35%, var(--card-edge)) }
.pt-epic{ border-color:color-mix(in srgb, var(--tier-epic) 35%, var(--card-edge)) }
.pt-common{ border-color:color-mix(in srgb, var(--tier-common) 25%, var(--card-edge)) }
.pt-card .sp{width:70px;border-radius:var(--r-sm)}
.pt-blank{
  font-size:2rem;line-height:1;color:var(--fg-soft);opacity:.55;
}
.pt-body{display:flex;flex-direction:column;gap:1px}
.pt-name{font-family:var(--ff-display);font-size:1.45rem;line-height:1.15;color:var(--card-ink)}
.pt-tr{font:500 1.05rem/1.3 var(--ff-ui);color:var(--fg)}

/* ---- THE STORE'S TIERS, COLLAPSED --------------------------------

   Sixteen full cards was a very long page, and its shape hid what she
   was shopping for: a legendary pet could not be seen without scrolling
   past eleven others. So each tier is a switch with a row of thumbnails
   under it, and tapping one opens that tier at full size. One open at a
   time — see the note in js/activities/store.js.

   The heading is a real button, so it needs its own reset: a <button>
   inherits none of the h2 styling it replaced. */
.gr-group{
  display:flex; align-items:center; gap:10px;
  width:100%;
  margin:18px 0 0;
  padding:10px 12px;
  text-align:left;
  border-radius:var(--r-md);
  border:1.5px solid rgb(var(--mauve-rgb) / .35);
  background:rgb(var(--sand-rgb) / .35);
  color:var(--ink);
  font:var(--t-card); letter-spacing:var(--t-card-ls);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
/* Open. The tier's own border stays; the fill is the accent, so "this
   one is expanded" reads the same way on all four and does not compete
   with the tier colour for meaning. */
.gr-group.is-open{
  background:rgb(var(--flame-rgb) / .12);
}
.gr-group-t{ flex:1 1 auto; min-width:0 }
/* Not `.gr-count` from the old heading — it sat inside an h2 and took
   its colour from the page. Inside a button it needs its own. */
.gr-group .gr-count{
  flex:0 0 auto;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--ink-soft);
}
/* One triangle that turns. Pointing right when closed, down when open —
   the rotation IS the state, so there is no second character to keep in
   step with the panel. */
/* Bare chevron, no circle — same reason as `.toc-caret`. These tier
   headings really are pressable, unlike the TOC's, so the circle was
   less of a lie here, but a filled triangle in a filled circle still
   says "play this" rather than "unfold this". */
.gr-caret{
  flex:0 0 auto;
  width:22px; height:22px;
  display:flex; align-items:center; justify-content:center;
  font:700 1rem/1 var(--ff-ui);
  color:var(--ink-soft);
  transition:transform .18s ease;
}
.gr-group.is-open .gr-caret{ transform:rotate(90deg) }
@media (prefers-reduced-motion: reduce){
  .gr-caret{ transition:none }
}


/* ---- EACH TIER ITS OWN SECTION ------------------------------------

   Steven's velvets, one per tier, behind the cards. His framing: "The
   point isn't to stare at the background, the point is to have some sort
   of visual identity for the section."

   THE CARDS STAY OPAQUE. The texture shows in the gaps between them and
   in the padding around them, so no text ever sits on gold embroidery —
   the rule about opaque text boxes holds with no exception carved for
   this.

   `cover` and a fixed position, so one wide velvet fills a section of any
   height without tiling into a visible seam. A section with no `bg` gets
   `.gr-sect` alone and reads as a plain block. */
/* ONE ROUNDED RECTANGLE, EITHER WAY.

   Collapsed it holds the pets in miniature; open it holds them full size.
   Same panel, same background, same corners, so opening a tier reads as
   the panel growing rather than one thing swapped for another.

   NOTHING IS WRITTEN ON IT. Steven: "The background colors for each tier
   don't need to have any text visible on top of it. It's simply a
   background color." The run note moved out to the page above, so the
   panel carries pictures and nothing else — and there is no plate to
   design, no contrast to check, no rule to bend.

   `padding` is what makes the background visible at all: the cards are
   opaque, so the texture shows in the 12px margin around them and the
   6px gaps between them, and nowhere else. */
/* TWO LAYERS.

   Steven: "The collapsed strip is just a rounded rectangle with all the
   pets sitting on it. The background is behind the rounded rectangle
   containing all of the pets. The rounded rectangle doesn't fill the
   entire page/screen. There's a little bit of the background visible
   behind it."

   So `.gr-sect` is the velvet and nothing else — no padding tricks, no
   text, no corners doing double duty. `.gr-plate` is the rounded
   rectangle that holds the pets, inset far enough that the velvet shows
   as a margin around it.

   THE VELVET STAYS PUT WHILE THE SECTION SCROLLS. His words: "as you
   scroll, the background stays in place until you get to the next section
   in which case the new background is visible behind all the stuff."
   `background-attachment:fixed` does exactly that — the picture is
   painted relative to the viewport, so it holds still under a scrolling
   section and the next tier's own background takes over as it arrives.

   IOS IS THE RISK, AND IT IS A REAL ONE. Safari on iOS has never
   supported fixed attachment properly — it paints the image at document
   size and often stutters or ignores it. So `scroll` is the declared
   value and `fixed` follows it: a browser that handles fixed uses it, and
   iOS falls back to a background that simply moves with the section,
   which looks ordinary rather than broken. Worth checking on her phone
   before deciding whether the effect is worth keeping at all. */
.gr-sect{
  margin:0 0 10px;
  border-radius:var(--r-md);
  background:var(--card-edge);
}
.gr-sect.has-bg{
  padding:14px;
  background-size:cover;
  background-position:center;
  background-repeat:no-repeat;
  background-attachment:scroll;
  background-attachment:fixed;
}
.gr-sect:not(.has-bg){ padding:14px }

/* The rounded rectangle the pets sit on. Slightly translucent so the
   velvet reads THROUGH it as a tint rather than being hidden — the
   pictures are already opaque, so nothing here affects text. */
.gr-plate{
  border-radius:var(--r-md);
  padding:10px;
  background:rgb(var(--paper-rgb) / .82);
}

/* NOT a second `.gr-note` rule — one already exists further up and styles
   every note on the grammar screens too. Only the margin differs here,
   because this one sits between a heading and a panel rather than under a
   page title. A whole duplicate rule would have been two places to
   change the note's colour. */
.gr-sect + .gr-note, .gr-group + .gr-note{ margin:8px 2px 6px }

/* ---- THE FOUR TIER COLOURS ----

   Steven: common green, rare blue, epic purple, legendary amber — "but
   make sure it's still readable."

   READABLE IS THE WHOLE PROBLEM, and it needs two sets. Nine themes have
   near-white paper (#E8FFF4 through #FFF5E8) and three are dark
   (midnight #161C2A, ember #241A14, pine #142117). A mid-green that
   reads well on cream is nearly invisible on midnight, and a green pale
   enough for midnight washes out on cream.

   So: variables, with the light-paper values here and lightened
   overrides for the three dark themes below. That is exactly the pattern
   Word Lab already uses for its coloured tiles — same problem, same
   shape, one convention rather than two.

   Variables and not four hardcoded rules per element, because the colour
   now appears in two places — the section heading and the rarity word on
   every card — and two copies of a colour drift. */
/* Chosen against the CARD's two backgrounds — #FAF7F1 and #1F1E22 — not
   against twelve theme papers. That is what lets legendary be a true
   amber: the earlier twelve-way value had to be darkened to #A34608 to
   clear 4.5 on the palest paper, and against one known off-white it does
   not. */
:root{
  --tier-common:#1F7A47;
  --tier-rare:#1D5BB8;
  --tier-epic:#7B31B5;
  --tier-legendary:#B4530A;
}
html[data-theme="midnight"],
html[data-theme="ember"],
html[data-theme="pine"]{
  --tier-common:#63D695;
  --tier-rare:#7FB8FF;
  --tier-epic:#C79BF0;
  --tier-legendary:#F0AF62;
}

.gr-group.gr-common    { border-color:var(--tier-common) }
.gr-group.gr-rare      { border-color:var(--tier-rare) }
.gr-group.gr-epic      { border-color:var(--tier-epic) }
.gr-group.gr-legendary { border-color:var(--tier-legendary) }

/* The heading's own name takes the tier colour too, so the section says
   which tier it is before the word is read. */
.gr-common .gr-group-t    { color:var(--tier-common) }
.gr-rare .gr-group-t      { color:var(--tier-rare) }
.gr-epic .gr-group-t      { color:var(--tier-epic) }
.gr-legendary .gr-group-t { color:var(--tier-legendary) }

/* ---- THE RARITY WORD ON EACH CARD ----

   `.pt-card` already carries `pt-<tier>`, so the chip needs no new class.
   Bolder than the surrounding text as well as coloured: colour alone is
   not a distinction for anyone who cannot see it, and this is the one
   piece of the card that sorts the sixteen pets. */
.pt-common .pt-tier    { color:var(--tier-common);    font-weight:700 }
.pt-rare .pt-tier      { color:var(--tier-rare);      font-weight:700 }
.pt-epic .pt-tier      { color:var(--tier-epic);      font-weight:700 }
.pt-legendary .pt-tier { color:var(--tier-legendary); font-weight:700 }

/* The squished row. `auto-fill` rather than a fixed count: five common
   pets and three epic ones should not be forced into the same columns,
   and a tier gaining a pet must not need a rule change here. */
.pt-strip-row{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(56px, 1fr));
  gap:6px;
  margin:8px 0 0;
}
.pt-thumb{
  padding:4px;
  border-radius:var(--r-sm);
  border:1.5px solid rgb(var(--mauve-rgb) / .3);
  background:var(--paper);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.pt-thumb .sp, .pt-thumb img{
  width:100%; height:auto; display:block; border-radius:var(--r-sm);
}
/* Owned reads as collected, so the row is a collection and not a menu. */
.pt-thumb.is-own{
  border-color:var(--yes);
  background:rgb(var(--flame-rgb) / .06);
}
/* No art yet: the initial, so a missing picture is a placeholder rather
   than an empty box of unknown purpose. */
.pt-thumb-t{
  display:flex; align-items:center; justify-content:center;
  aspect-ratio:1/1;
  font:700 1.1rem/1 var(--ff-display);
  color:var(--ink-soft);
}

/* WHO THE PET IS — two or three sentences between the rarity chip and
   the price. Body text, not a heading: it is there to be read, and a
   card that shouts its personality line competes with its own name.
   `pt-about` is absent on a pet with no `about` written yet, so nothing
   here needs an empty state. */
.pt-about{
  margin:8px 0 0;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  line-height:1.45;
  color:var(--ink-soft);
}

.pt-tier{
  font:700 .92rem/1.15 var(--ff-ui);text-transform:uppercase;letter-spacing:.07em;
  color:var(--fg);
}
/* THE PRICE HAS TO BE READABLE IN EVERY THEME.

   Steven, 10 Sep: "pet costs are invisible — I tested the top 2 rows of
   colour schemes, only 1 was even slightly legible."

   Two faults stacked:

   `is-dear` multiplied the whole button by `opacity:.45`. Every one of
   the sixteen pets is currently unaffordable — they are gated by activity
   days, not crystals — so EVERY price on the page was at 45%.

   The colour pairing is fine and was not the fault: `--ink` is redefined
   in all twelve theme blocks and goes light on the dark ones. I changed
   it first and put it back; the opacity was the whole problem.

   Now an unaffordable price keeps full contrast and says so by other
   means: a dashed border and the shortfall row underneath. Being unable
   to afford something is information, not a reason to hide it. */
.pt-buy,.pt-pick{
  width:100%;padding:13px 10px;border-radius:999px;cursor:pointer;
  font:700 1.12rem/1.1 var(--ff-ui);
  border:1.5px solid var(--sand-deep);
  background:var(--paper);color:var(--ink);
  -webkit-tap-highlight-color:transparent;
}
.pt-buy:hover:not(:disabled),.pt-pick:hover{background:var(--sand)}
.pt-buy.is-dear{
  cursor:default;
  opacity:1;
  border-style:dashed;
  border-color:rgb(var(--mauve-rgb) / .7);
}
.pt-buy.is-claim{background:var(--flame);color:var(--on-flame);border-color:var(--flame)}
.pt-pick.is-on{background:var(--flame);color:var(--on-flame);border-color:var(--flame)}
.pt-need{
  font:500 1rem/1.45 var(--ff-ui);color:var(--fg);
  padding:0 2px;
}
.pt-slot-buy{width:100%;margin-bottom:6px}
/* ---------- THE PET SHE JUST BOUGHT ----------

   Steven, 09 Sep: a window that opens on purchase, the pet greeting her,
   a glow behind the animal, the message below it, and a cheerful button.

   A WINDOW, NOT A STRIP. This was one line under the shelf. A pet costs
   real crystals and is the one thing in the app she picks out and keeps.

   VERTICAL, and the order is the point: the animal, then who it is, then
   what it says. */
.pt-got-wrap{
  position:fixed; inset:0; z-index:820;
  display:flex; align-items:center; justify-content:center;
  padding:18px;
  background:rgb(var(--scrim-rgb) / .68);
}
/* The box sits in this so the sparkles can reach outside it — the box
   itself has `overflow:auto` and would clip them. See the note in
   store.js. */
.pt-got-frame{
  position:relative;
  width:100%; max-width:360px;
}

.pt-got{
  position:relative;
  width:100%; max-width:360px;
  max-height:88vh; overflow:auto;
  padding:57px 20px 20px;
  border-radius:var(--r-lg);
  /* SOLID, THEN DARKENED. Steven, 10 Sep: on a dark theme the window
     rendered see-through — the achievements list read straight through it.
     A layered `background` with a wash on top can composite to less than
     opaque if anything under it is not solid, so this pins an opaque base
     colour FIRST (`--paper` is opaque in every theme, light or dark), and
     the 10% black wash sits over it as a separate solid-alpha layer. The
     window can no longer be transparent on any theme.

     `--scrim-rgb` (always black) for the wash, NOT `--shade-rgb` (which
     flips to white on the three dark themes and would lighten it). */
  background-color:var(--paper) !important;
  background-image:
    linear-gradient(rgb(var(--scrim-rgb) / .10), rgb(var(--scrim-rgb) / .10)) !important;
  border:1.5px solid var(--flame);
  box-shadow:0 18px 48px rgb(var(--scrim-rgb) / .5);
  text-align:center;
}

/* THE GLOW SITS UNDER THE PICTURE, in its own layer, so a missing file
   changes nothing but the sparkle. Square and centred: the artwork is
   centred in the stage, so a square glow needs no knowledge of the
   window's width, which changes with the screen. */
.pt-got-stage{
  position:relative;
  width:150px; height:150px;
  margin:-35px auto 16px;
  display:grid; place-items:center;
}
.pt-got-glow{
  /* Sized so the glow image's DARK CENTRE lands just inside the 148px pet
     (pet hides it) while its BLUE HALO extends past the pet's edges (halo
     shows). The pet-glow.webp centre is ~72% of the image, so a ~194px
     glow puts the dark square near 140 and the halo beyond 148. */
  position:absolute; inset:-33px;
  width:calc(100% + 66px); height:calc(100% + 66px);
  object-fit:contain;
  pointer-events:none;
  z-index:0;
  /* The colour cast. `saturate` first so the rotation lands on a strong
     hue rather than washing out — a desaturated source rotates to a
     desaturated result, which reads as grey rather than as the tier. */
  filter:saturate(1.15) hue-rotate(var(--got-spin));
}
/* THE ARTWORK IS AN OPAQUE SQUARE, rounded here rather than in the file.

   Steven, 09 Sep: "they are square, no need for being transparent, you
   can round off the corners in CSS."

   `cover`, not `contain`: the source is square and the frame is square,
   so cover fills it exactly and crops nothing — but it also means a
   slightly off-square file fills the frame instead of letterboxing.

   The glow behind therefore reads as a HALO around the tile rather than
   light coming through the animal. That is the better look for an opaque
   square, and it is why the burst is sized to reach past the picture. */
.pt-got-stage img.pet-img.pt-got-pic{
  position:relative; z-index:1;
  /* THE PET COVERS THE GLOW'S DARK CENTRE. Steven, 10 Sep, with the source
     files: the "black border" is the DARK NAVY CENTRE baked into
     pet-glow.webp, showing around a pet that sat too small inside it.

     WHY EVERY SIZE CHANGE DID NOTHING UNTIL NOW: the pet is an
     `img.pet-img`, and the global `.pet-img` rule forces `width:70px` and
     comes LATER in the file — so a bare `.pt-got-pic` (equal specificity,
     earlier) lost, and the pet rendered at 70px no matter what number I
     set. Scoping to `.pt-got-stage .pt-got-pic` outranks it, so the size
     below actually applies. */
  width:148px !important;
  height:148px !important;
  object-fit:cover;
  border-radius:18px;
  box-shadow:0 4px 14px rgb(var(--scrim-rgb) / .45);
}

.pt-got-sub{
  margin:0;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  text-transform:uppercase;
  color:var(--fg-soft);
}
.pt-got-name{
  margin:2px 0 0;
  font-family:var(--ff-display);
  font-size:clamp(1.3rem, 5vw, 1.6rem); line-height:1.15;
  color:var(--fg);
}
.pt-got-de{
  margin:2px 0 0;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-accent);
}

/* What it says. Tappable to hear again; German over her language. */
.pt-got-say{
  display:block; width:100%;
  margin:14px 0 0; padding:12px 12px;
  border-radius:var(--r-md);
  background:rgb(var(--sand-rgb) / .16);
  border:0; text-align:center; cursor:pointer;
}
.pt-got-de-line{
  display:block;
  font:var(--t-de); letter-spacing:var(--t-de-ls);
  color:var(--fg);
}
.pt-got-tr{
  display:block; margin-top:4px;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
}
.pt-got-ok{
  display:block; width:100%;
  margin:16px 0 0;
}

/* The achievement window's second line — the "You got the achievement X"
   announcement under the pet's own congratulation. Quieter than the pet
   line above it so the pet's voice leads and this reads as the caption.
   Added 10 Sep with awardWindow(). */
.pt-got-told{
  margin:10px 0 0;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
  text-align:center;
}

/* The crystal bonus, above Yes! — brighter and heavier than the
   announcement line, since it is the payout. Added 10 Sep. */
.pt-got-crystals{
  margin:8px 0 0;
  font:var(--t-de); letter-spacing:var(--t-de-ls);
  color:var(--fg);
  text-align:center;
}

/* ---------- RARITY IS FELT, NOT JUST SEEN ----------

   Steven, 09 Sep: green common, blue rare, purple epic, amber legendary —
   and "I really want to elevate the experience of buying pets."

   So the tier drives four things, not one colour: the border, the burst
   behind the animal, the sparkle colour, and HOW BIG the burst is. A
   legendary opens wider and holds longer than a common. A legendary that
   lands exactly like a common teaches her the tiers do not matter.

   `--tier-*` are the shop's own tokens, already tuned for light and dark
   themes. Reusing them means this window can never disagree with the
   badge on the card she bought from. */
/* ONE GLOW FILE, FOUR COLOURS.

   Steven, 09 Sep: "you can take the same thing and just run a colour cast
   on it." Right, and much better than four files — one animation to make,
   one to load, one to change if the timing is wrong.

   `--got-spin` is a hue rotation applied to the glow image. The numbers
   are MEASURED from the tier tokens rather than guessed: the artwork is
   blue, blue is 216 degrees, and each tier's own colour sits at

       common     146     -70deg
       rare       216      +0deg   (the file as drawn)
       epic       274     +58deg
       legendary   26    +170deg

   So a tier is a colour token, a burst size and a rotation, and adding a
   fifth tier is three numbers in one rule. */
/* Declared on the FRAME, because the box and the sparkle layer are
   siblings inside it and both read these. A default on `.pt-got` itself
   would shadow the inherited value and every window would be green. */
/* Declared on the FRAME: the box and the sparkle layer are siblings
   inside it and both read these.

   `--spark` scales the sparkles — bigger dots, thrown further — so a
   legendary does not arrive with the same modest twinkle as a frog.
   Steven, 10 Sep: "have the sparkles bigger for higher tier."

   One number does both jobs because the layer is scaled as a whole: the
   dots grow with it and they spread with it. */
.pt-got-frame{
  --got:var(--tier-common); --got-reach:1; --got-spin:-70deg; --spark:1;
}
/* THE STEPS HAVE TO BE VISIBLE, NOT JUST PRESENT.

   Steven, 10 Sep: "the next tier up does nothing." He was right — rare
   was a 25% size bump and nothing else, which nobody can see against a
   common they watched thirty seconds earlier.

   Each tier now changes something you can NAME, not just a percentage:

     common     8 dots,  one ring
     rare      16 dots,  two rings          <- the ring is the tell
     epic      16 dots,  two rings, wider, and the burst behind grows
     legendary 24 dots, three rings, widest, longest

   Count and rings do the work; scale only amplifies it. A number going
   up by a quarter is invisible. A second ring of light appearing is not. */
.pt-got-common   { --got:var(--tier-common);    --got-reach:1;    --got-spin:-70deg; --spark:1 }
.pt-got-rare     { --got:var(--tier-rare);      --got-reach:1.15; --got-spin:0deg;   --spark:1.35 }
.pt-got-epic     { --got:var(--tier-epic);      --got-reach:1.35; --got-spin:58deg;  --spark:1.75 }
.pt-got-legendary{ --got:var(--tier-legendary); --got-reach:1.6;  --got-spin:170deg; --spark:2.2 }

.pt-got{ border-color:var(--got) }

/* The coloured burst behind the animal. CSS, so the tier reads before any
   glow file exists and keeps reading if one never does. Sits under both
   the glow image and the picture. */
.pt-got-burst{
  /* Reaches well past the 118px tile so the colour shows AROUND it — an
     opaque square hides anything directly behind it. */
  position:absolute; inset:-30px;
  border-radius:50%;
  pointer-events:none;
  z-index:0;
  background:radial-gradient(circle at 50% 50%,
    color-mix(in srgb, var(--got) 55%, transparent) 0%,
    color-mix(in srgb, var(--got) 22%, transparent) 42%,
    transparent 70%);
  transform:scale(var(--got-reach));
  animation:ptGotBurst 1.6s ease-out 1 both;
}
@keyframes ptGotBurst{
  0%  { opacity:0; transform:scale(calc(var(--got-reach) * .55)) }
  35% { opacity:1 }
  100%{ opacity:.55; transform:scale(var(--got-reach)) }
}







@media (prefers-reduced-motion: reduce){
  .pt-got-pop{ animation:none }
  .pt-got-burst{ animation:none }
}

/* the pets cheering at the end of a round */
/* The pets at the end of a round. Sized by how many there are: the store
   shows them at 140px and one pet should look the same here, but three at
   140 plus gaps is 448px on a 390px phone. */
.pt-cheer{display:flex;justify-content:center;gap:14px;margin:4px 0 10px;flex-wrap:wrap}
.pt-cheer-1 .pet-img{width:140px}
.pt-cheer-2 .pet-img{width:120px}
.pt-cheer-3 .pet-img{width:104px}
.pt-cheer-one{display:flex;flex-direction:column;align-items:center;gap:3px}
.pt-cheer-one .sp{width:52px;border-radius:var(--r-sm)}
.pt-cheer-blank{font-size:1.6rem;line-height:1;color:var(--fg-accent)}
.pt-cheer-name{
  font-family:var(--ff-display);font-size:0.9rem;color:var(--fg-soft);
}

.co-earn-shop{
  display:block;width:100%;margin-top:10px;
  padding:9px;border-radius:999px;cursor:pointer;
  border:1.5px solid var(--flame);background:none;
  font:700 0.97rem/1 var(--ff-ui);color:var(--fg-accent);
  -webkit-tap-highlight-color:transparent;
}
.co-earn-shop:hover{background:rgb(var(--flame-rgb) / .12)}

/* a pet's form, and growing it */
.pt-form{
  font:700 .9rem/1.1 var(--ff-ui);text-transform:uppercase;letter-spacing:.08em;
  color:var(--fg-accent);
}
.pt-grow{
  width:100%;margin-top:4px;padding:11px 8px;border-radius:999px;cursor:pointer;
  font:700 1.05rem/1.1 var(--ff-ui);
  border:1.5px dashed var(--flame);
  background:none;color:var(--fg-accent);
  -webkit-tap-highlight-color:transparent;
}
.pt-grow:hover:not(:disabled){background:rgb(var(--flame-rgb) / .14)}
.pt-grow:disabled{opacity:.4;cursor:default}

/* a pet's name, and the German word it is */
.pt-word{
  font-family:var(--ff-display);font-size:1.28rem;line-height:1.2;
  color:var(--fg);
}
.pt-cheer-word{
  font-family:var(--ff-display);font-size:0.85rem;
  color:var(--fg-soft);
}

/* the full title, under the short name */
.pt-full{
  font:400 .96rem/1.35 var(--ff-ui);
  color:var(--fg-soft);
}

/* the testing row, plainly not a game feature */
.st-test{
  background:rgb(var(--shade-rgb) / .2);
  border-color:var(--soft-on-dark);
  gap:8px;
}
.st-test .btn{flex:1;min-width:5rem}

/* the line about the word she just answered, ahead of the family rule */
.mz-why.is-this{
  font-weight:700;
  color:var(--fg);
}

/* a legendary's requirements, one per line with her progress on each */
/* LOCKED, said plainly. See the note in store.js — the requirement line
   alone read as a fault rather than a condition.

   IMPORTANT: this UI lives ON a pet card. It must therefore use the pet
   card's own two-look palette, never the page/theme --fg tokens. Otherwise
   a light theme can feed pale text into a light store card (the Quack Quack
   failure Steven caught on iPhone). Keeping the row + all requirement text
   on --card-* makes it readable in every site theme. */
.pt-lockrow{
  display:flex; align-items:center; gap:6px;
  width:100%;
  margin:8px 0 0;
  padding:5px 8px;
  border-radius:var(--r-sm);
  background:color-mix(in srgb, var(--card-ink) 6%, var(--card-paper));
  border:1px solid color-mix(in srgb, var(--card-ink) 24%, var(--card-paper));
  color:var(--card-ink);
}
/* The shortfall row keeps the flame cue in its EDGE only. Its ground and
   text stay card-local, so a theme accent can never destroy readability. */
.pt-lockrow.is-dear{
  background:color-mix(in srgb, var(--card-ink) 6%, var(--card-paper));
  border-color:var(--flame);
}
.pt-lock-ico{ font-size:.95rem; line-height:1 }
.pt-lock-word{
  font:700 .8rem/1 var(--ff-ui);
  text-transform:uppercase; letter-spacing:.09em;
  color:var(--card-ink);
}

.pt-need{
  white-space:pre-line;
  text-align:left;
  width:100%;
  padding:7px 4px 2px;
  font:600 .98rem/1.55 var(--ff-ui);
  color:var(--card-ink);
}

/* a gated pet's price, shown but not yet payable */
.pt-locked-cost{
  font:700 1.05rem/1.1 var(--ff-ui);
  color:var(--card-ink-soft);
  padding:5px 0 2px;
}
/* The way out of a locked card that wants achievements — a link, not a
   buy button, so it must not look like one. */
.pt-gate-link{
  display:block; width:100%;
  margin-top:6px; padding:8px 10px;
  border:0; border-radius:999px;
  background:transparent;
  color:var(--card-ink-soft);
  font:600 .9rem/1.3 var(--ff-ui);
  text-decoration:underline;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}

.pt-need.is-open{
  color:var(--card-ink-soft); font-weight:700;
  text-align:center; padding-top:4px;
}

/* how close today's bonus is */
.co-earn-goal{
  margin-left:auto;
  font:700 0.87rem/1 var(--ff-ui);
  color:var(--fg-soft);
}

/* --- lessons --------------------------------------------------------- */

.ls-head{
  margin:0 0 8px;
  font-family:var(--ff-display);
  font-size:clamp(1.15rem,4.2vw,1.45rem);line-height:1.25;
  color:var(--fg);
}
.ls-body{
  margin:0 0 16px;
  font:400 clamp(.95rem,3.2vw,1.05rem)/1.55 var(--ff-ui);
  color:var(--fg-soft);
}
.ls-note{
  margin:16px 0 0;padding:11px 13px;
  border-radius:var(--r-md);
  background:rgb(var(--mauve-rgb) / .2);
  border-left:3px solid var(--mauve);
  font:400 .88rem/1.5 var(--ff-ui);
  color:var(--soft-on-dark);
}

.ls-eg{
  padding:11px 13px;margin-bottom:7px;
  border-radius:var(--r-md);
  background:var(--paper);
  display:flex;flex-direction:column;gap:3px;position:relative;
}
.ls-eg-de{
  margin:0;padding-right:34px;
  font-family:var(--ff-display);
  font-size:clamp(1rem,3.6vw,1.2rem);line-height:1.4;
  color:var(--fg);
}
.ls-eg-de b{color:var(--fg-accent);font-weight:700}
.ls-eg-gloss{margin:0;font:400 0.97rem/1.35 var(--ff-ui);color:var(--fg-soft)}
.ls-eg .speak{position:absolute;top:9px;right:9px}

.ls-table{display:flex;flex-direction:column;gap:3px;margin-top:4px}
.ls-row{
  display:grid;grid-template-columns:1fr 1.3fr auto;gap:9px;align-items:baseline;
  padding:9px 11px;border-radius:var(--r-sm);
  background:rgb(var(--shade-rgb) / .16);
  border:0;cursor:pointer;text-align:left;
  -webkit-tap-highlight-color:transparent;
}
.ls-row:hover{background:rgb(var(--shade-rgb) / .3)}
.ls-row-a{font-family:var(--ff-display);font-size:.98rem;color:var(--fg)}
.ls-row-b{font-family:var(--ff-display);font-size:.98rem;color:var(--fg-accent)}
.ls-row-c{font:400 0.87rem/1.2 var(--ff-ui);color:var(--fg-soft)}

.ls-ask{
  margin:0 0 14px;
  font:700 .96rem/1.4 var(--ff-ui);
  color:var(--fg-soft);
}

/* DIFFICULTY, NAMED RATHER THAN HIDDEN.

   Rendered by js/activities/lessons.js when a step carries
   `level:'harder'` or `level:'hardest'`. Steven, 09 Sep: nothing gets
   withheld, the easy material is ordered first, and the harder rungs are
   simply labelled so she can decide.

   Deliberately quiet — an eyebrow, not a warning. A loud badge on a
   harder step reads as "you will fail this", which is the opposite of
   letting her choose. `--mauve` for harder, `--flame` for hardest, both
   theme tokens so all twelve themes are covered without new colours. */
.ls-level{
  margin:0 0 10px;
  font:700 .68rem/1 var(--ff-ui);
  text-transform:uppercase; letter-spacing:.12em;
  color:var(--mauve);
}
.ls-level.is-hardest{ color:var(--flame) }

/* ---------- MARK: THE TAPPABLE SENTENCE ----------

   Built by `paintMark()` in js/activities/lessons.js. She taps the words
   where they stand rather than picking from a list underneath, which is
   the whole point — three buttons, one of them obviously a verb, is a
   much easier exercise than finding the role in place.

   PERMANENT COLOUR SCHEME, Steven's: blue subject, orange verb, green
   object. Once introduced these do not get reused for anything else.

   `--role-verb` is a deep amber, NOT `--flame`. Flame is the app accent
   in 132 rules — primary buttons, the tour highlight, the correct-answer
   mark — so a flame-coloured verb would read as something to press. The
   amber is unmistakably orange and unmistakably not a button.

   Blue and green likewise avoid `--gen-m` and `--gen-n`, which already
   mean masculine and neuter in the gender lessons. Three colours meaning
   two different things is the trap this set exists to dodge. */
/* THE STRIP IGNORES THE COLOUR SCHEME ON PURPOSE.

   Steven, 09 Sep: off-white behind the sentence on the light schemes, a
   dark background on the dark ones, and otherwise the strip does not
   follow the theme — the rest of the interface still does.

   The reason it is the right call: the three role colours have to stay
   legible and stay *recognisable as the same three colours* on all
   twelve themes. Against twelve different paper colours they would need
   twelve tunings. Against exactly two known backgrounds they need two,
   which is what `--role-*` already has. The scheme becomes a constant
   rather than a variable.

   Only midnight, ember and pine are dark, and they are the same three
   blocks that already override the gender colours, so this cost nothing
   extra. */
/* ---------- A SPARKLE, ONCE, BEHIND WHAT HE IS POINTING AT ----------

   Steven, 09 Sep. The ring alone marks the target but does not draw the
   eye TO it; on a busy screen she can look straight past the one thing
   the step is about.

   Six dots drawn as radial-gradients in a single background, so it is one
   pseudo-element and one paint rather than six positioned children. They
   bloom out and fade over 1.5s — Steven, 09 Sep: "about 1.5 sec, not
   longer, just highlighting it is enough." Long enough to be seen, short
   enough that it is over before she has finished reading the step.

   ONCE, NOT LOOPING. `animation-iteration-count:1` with `forwards`, so it
   fires when the class lands and then leaves nothing behind. Each step
   applies `.bt-lit` fresh, so every step gets its own single sparkle
   without anything having to reset it.

   BEHIND, NOT OVER: `z-index:-1` puts it under the element's own content,
   and `pointer-events:none` keeps it out of the way of the tap the step
   is waiting for.

   ONE TARGET CANNOT SHOW IT. `.sg-audio` is a replaced element and
   browsers do not reliably render pseudo-elements on those — checked
   across all 68 tour targets, and it is the only one. It keeps the ring,
   which is what actually marks it. */
.bt-lit::after{
  content:'';
  position:absolute;
  left:-14px; right:-14px; top:-14px; bottom:-14px;
  z-index:-1;
  pointer-events:none;
  background:
    radial-gradient(circle 3px at 12% 22%, var(--flame) 96%, transparent 100%),
    radial-gradient(circle 2px at 86% 16%, var(--flame) 96%, transparent 100%),
    radial-gradient(circle 3px at 92% 74%, var(--flame) 96%, transparent 100%),
    radial-gradient(circle 2px at  8% 80%, var(--flame) 96%, transparent 100%),
    radial-gradient(circle 2px at 48%  4%, var(--flame) 96%, transparent 100%),
    radial-gradient(circle 3px at 56% 98%, var(--flame) 96%, transparent 100%);
  background-repeat:no-repeat;
  opacity:0;
  /* LOOPS AND STAYS VISIBLE FOR THE WHOLE STEP. Steven, 10 Sep: the burst
     kept reading as "glowed then stopped." It is `infinite`, so it already
     runs until the step ends — but the cycle is slow and never drops to
     invisible, so it reads as continuous sparkling rather than a quick
     pulse that looks finished. It clears itself: each step lands `.bt-lit`
     fresh and clearHighlight() strips it. */
  animation:btSparkle 3.4s ease-in-out infinite;
}
@keyframes btSparkle{
  0%,100%{ opacity:.55; transform:scale(.96) }
  25%    { opacity:1;   transform:scale(1.08) }
  50%    { opacity:.7;  transform:scale(1.04) }
  75%    { opacity:1;   transform:scale(1.10) }
}

/* The sparkle needs a positioned parent to sit against. `.bt-lit` itself
   deliberately does NOT set `position` — that clobbered `.backlink`'s
   `fixed` and dropped the back button out of its corner — so the ones
   that are already static get it from `bt-lit-static`, and the rest are
   positioned already. */
@media (prefers-reduced-motion: reduce){
  .bt-lit::after{ animation:none; opacity:0 }
}

.ls-mark{
  display:flex; flex-wrap:wrap; align-items:baseline;
  gap:6px 10px;
  margin:0 0 14px;
  /* Top padding leaves room for the speaker in the corner — see
     `.is-mark-say` below. Without it the first line runs under the icon. */
  padding:46px 14px 16px;
  position:relative;
  background:var(--strip-bg);
  color:var(--strip-ink);
  border:1.5px solid var(--strip-edge);
  border-radius:var(--r-lg);
  box-shadow:var(--shadow-card);
  font:var(--t-de); letter-spacing:var(--t-de-ls);
  line-height:1.9;
}
.ls-part{
  /* `inherit`, not `--fg`: inside the strip the text colour comes from
     the strip, so an unroled word is legible on both backgrounds without
     a second rule. */
  font:inherit; color:inherit;
  background:none; border:0; padding:0; margin:0;
}
/* Scenery — a chunk with no role. Rendered, never tappable. */
.ls-part.is-plain{ opacity:.72 }

/* A tappable part. Underlined rather than boxed: a box on every word
   turns the sentence into a row of buttons and stops reading as a
   sentence, which is what she is supposed to be reading. */
/* A BOX AROUND THE WORDS, NOT COLOURED TEXT.

   Steven, 09 Sep: "keep the sentence words the text color, just put a
   round corner rectangle box around the word/words and with a slight
   neon glow."

   Which is the better call, and not only aesthetically: the text stays
   at the strip's full ink contrast, so legibility never depends on the
   role colour. That matters because orange on a light background is
   hard — the value Grok proposed measured 3.17 against the off-white
   strip and mine 3.53, both under the 4.5 AA needs for text. As a box
   and a glow rather than as letterforms, the colour carries meaning
   without carrying the reading. */
.ls-part.is-role{
  cursor:pointer;
  padding:3px 8px;
  border-radius:var(--r-sm);
  /* Dashed and neutral until she has answered: it says tappable without
     claiming a role colour it has not earned yet. */
  border:1.5px dashed rgb(var(--shade-rgb) / .38);
  transition:box-shadow .16s ease, border-color .16s ease;
}
.ls-part.is-role:hover{
  border-color:rgb(var(--shade-rgb) / .6);
  background:rgb(var(--shade-rgb) / .07);
}
.ls-part.is-role:focus-visible{
  outline:2px solid var(--flame); outline-offset:2px;
}

/* Coloured — either because the step scaffolds, or because she has
   answered and all three are being shown at once. */
/* TWO SEPARATE PALETTES, AND THEY NEVER SHARE A SCREEN.

   Sentence parts — Steven's permanent scheme:
       subject    blue      a participant
       object     green     the other participant
       verb       orange    the action between them, so a warm contrast
       adjective  violet    attaches to the blue/green nouns, so a hue
                            next to blue
       adverb     teal      attaches to the orange verb, so its cool
                            complement
   The palette encodes WHAT MODIFIES WHAT, not just five arbitrary
   colours: cool for the noun side, warm for the verb, and each modifier
   sits beside what it modifies.

   Negation and agreement — Steven, 09 Sep: "We can have negation and
   agreement be a separate lesson, green, red. Not related to sentence
   parts." So they are their own lesson with their own two colours, red
   for negation and green for agreement, and green meaning two things is
   not a clash because the two lessons never appear together. Same call
   as Max the Scorpion and Alina's brother Max.

   Red is #B3243C, a crimson, deliberately NOT `--no` (#FF8B7B, the
   wrong-answer colour, 36 uses) and not `--flame` (the accent, 133).
   A negated word has to read as a stop, not as a mistake she made. */
/* THE ROLE IS THE BOX AND THE GLOW. Text colour is untouched.

   `--role-*` draws the border, `--role-*-rgb` the glow and the faint
   inner wash. Two shadows: a tight one that reads as a rim and a wider
   soft one that reads as neon. Kept restrained — a sentence with three
   glowing boxes should look labelled, not lit up like a fairground. */
.ls-part.is-role[class*=" is-"]{ border-style:solid }

.ls-part.is-subj{
  border-color:var(--role-subj);
  background:rgb(var(--role-subj-rgb) / .10);
  box-shadow:0 0 0 1px rgb(var(--role-subj-rgb) / .35),
             0 0 9px rgb(var(--role-subj-rgb) / .45);
}
.ls-part.is-verb{
  border-color:var(--role-verb);
  background:rgb(var(--role-verb-rgb) / .10);
  box-shadow:0 0 0 1px rgb(var(--role-verb-rgb) / .35),
             0 0 9px rgb(var(--role-verb-rgb) / .45);
}
.ls-part.is-obj{
  border-color:var(--role-obj);
  background:rgb(var(--role-obj-rgb) / .10);
  box-shadow:0 0 0 1px rgb(var(--role-obj-rgb) / .35),
             0 0 9px rgb(var(--role-obj-rgb) / .45);
}
.ls-part.is-adj{
  border-color:var(--role-adj);
  background:rgb(var(--role-adj-rgb) / .10);
  box-shadow:0 0 0 1px rgb(var(--role-adj-rgb) / .35),
             0 0 9px rgb(var(--role-adj-rgb) / .45);
}
.ls-part.is-adv{
  border-color:var(--role-adv);
  background:rgb(var(--role-adv-rgb) / .10);
  box-shadow:0 0 0 1px rgb(var(--role-adv-rgb) / .35),
             0 0 9px rgb(var(--role-adv-rgb) / .45);
}
.ls-part.is-neg{
  border-color:var(--mark-neg);
  background:rgb(var(--mark-neg-rgb) / .10);
  box-shadow:0 0 0 1px rgb(var(--mark-neg-rgb) / .35),
             0 0 9px rgb(var(--mark-neg-rgb) / .45);
}
.ls-part.is-agree{
  border-color:var(--mark-agree);
  background:rgb(var(--mark-agree-rgb) / .10);
  box-shadow:0 0 0 1px rgb(var(--mark-agree-rgb) / .35),
             0 0 9px rgb(var(--mark-agree-rgb) / .45);
}

/* The role name under the word takes the colour, since it is a label
   rather than the sentence and has nothing to stay legible for. */
.ls-part.is-subj .ls-part-tag{ color:var(--role-subj) }
.ls-part.is-verb .ls-part-tag{ color:var(--role-verb) }
.ls-part.is-obj  .ls-part-tag{ color:var(--role-obj) }
.ls-part.is-adj  .ls-part-tag{ color:var(--role-adj) }
.ls-part.is-adv  .ls-part-tag{ color:var(--role-adv) }
.ls-part.is-neg  .ls-part-tag{ color:var(--mark-neg) }
.ls-part.is-agree .ls-part-tag{ color:var(--mark-agree) }

/* The glow, once the roles are revealed. Same technique as the tour
   highlight, so it is a pattern already proven on all twelve themes. */
/* No bolding: the box and glow already mark the word, and bolding
   inside a sentence changes its rhythm as well as its colour. */
.ls-part.is-chosen{ background:rgb(var(--shade-rgb) / .14) }

/* THE ONE SHE IS BEING ASKED FOR, on a scaffolded step only.

   Every part is already boxed and coloured; this one glows harder and
   lifts slightly, so the eye lands on it without the colour having to
   carry that job alone. It is a pointer, not a hint to decode —
   level one is a demonstration and this is what makes it one.

   The wider spread is deliberately built from the part's OWN role colour
   via `currentColor`, so one rule covers all seven roles instead of
   seven near-identical ones. */
.ls-part.is-asked{
  box-shadow:0 0 0 2px currentColor,
             0 0 16px 2px currentColor;
  transform:translateY(-1px);
}
/* `currentColor` inside the glow needs the role colour on the text, and
   the text is deliberately strip-ink. So the colour is restated here
   for the glow only, and immediately overridden for the letterforms. */
.ls-part.is-asked.is-subj{  color:var(--role-subj) }
.ls-part.is-asked.is-verb{  color:var(--role-verb) }
.ls-part.is-asked.is-obj{   color:var(--role-obj) }
.ls-part.is-asked.is-adj{   color:var(--role-adj) }
.ls-part.is-asked.is-adv{   color:var(--role-adv) }
.ls-part.is-asked.is-neg{   color:var(--mark-neg) }
.ls-part.is-asked.is-agree{ color:var(--mark-agree) }

@media (prefers-reduced-motion: reduce){
  .ls-part.is-asked{ transform:none }
}

/* The role name, printed under the word once answered. */
.ls-part-tag{
  display:block;
  font:700 .6rem/1.2 var(--ff-ui);
  text-transform:uppercase; letter-spacing:.1em;
  opacity:.85;
}

/* The speaker, in the strip's top left corner. Icon only and small: a
   full pill in there competes with the sentence, which is the thing she
   is supposed to be reading. Colours come from the strip rather than the
   theme, like everything else inside it. */
.is-mark-say{
  position:absolute; top:8px; left:10px;
  gap:0;
  padding:6px 10px;
  background:rgb(var(--shade-rgb) / .08);
  border:1.5px solid rgb(var(--shade-rgb) / .22);
  color:var(--strip-ink);
  font-size:.8rem;
}
.is-mark-say:hover{ background:rgb(var(--shade-rgb) / .16) }
.is-mark-say.is-speaking{
  background:rgb(var(--shade-rgb) / .22);
  border-color:rgb(var(--shade-rgb) / .45);
  color:var(--strip-ink);
}

.ls-mark-find{
  margin:0 0 12px;
  font:700 .9rem/1.4 var(--ff-ui);
  color:var(--fg-accent);
}

@media (prefers-reduced-motion: reduce){
  .ls-part.is-role{ transition:none }
  /* The glow is decoration; the border still carries the role. */
  .ls-part[class*="is-"]{ box-shadow:none }
}
.ls-card{
  padding:22px 16px;margin-bottom:16px;
  border-radius:var(--r-lg);
  background:var(--paper);
  text-align:center;
  font-family:var(--ff-display);
  font-size:clamp(1.5rem,6vw,2.1rem);line-height:1.2;
  color:var(--ink-on-light);
}
.ls-bins{display:grid;grid-template-columns:repeat(2, minmax(0,1fr));gap:9px}
.ls-bin{
  padding:18px 10px;border-radius:var(--r-md);
  border:2px solid var(--sand-deep);
  background:var(--sand-wash);
  font-family:var(--ff-display);font-size:1.15rem;
  color:var(--ink-on-light);cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  transition:transform .1s ease,background .12s ease;
}
.ls-bin:hover{transform:translateY(-2px);background:var(--sand)}
.ls-bin:active{transform:scale(.97)}

.ls-sentence{
  padding:16px;margin-bottom:6px;
  border-radius:var(--r-md);
  background:var(--paper);
  position:relative;
}
.ls-sentence-de{
  display:block;padding-right:34px;
  font-family:var(--ff-display);
  font-size:clamp(1.1rem,4.2vw,1.4rem);line-height:1.45;
  color:var(--fg);
}
.ls-sentence .speak{position:absolute;top:12px;right:11px}
.ls-gloss{margin:0 0 14px;font:400 .86rem/1.4 var(--ff-ui);color:var(--fg-soft)}

.ls-opts{display:grid;grid-template-columns:repeat(2, minmax(0,1fr));gap:8px}
.ls-opts.is-four{grid-template-columns:repeat(2, minmax(0,1fr))}
.ls-opt{
  padding:15px 8px;border-radius:var(--r-md);
  border:2px solid var(--sand-deep);
  background:var(--paper);
  font-family:var(--ff-display);font-size:1.1rem;
  color:var(--ink-on-light);cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  transition:transform .1s ease,background .12s ease;
}
.ls-opt:hover{transform:translateY(-2px);background:var(--sand-wash)}
.ls-opt:active{transform:scale(.97)}

.ls-type{display:flex;gap:8px;align-items:stretch}
.ls-input{
  flex:1;padding:14px 15px;border-radius:var(--r-md);
  border:2px solid var(--sand-deep);
  background:var(--paper);
  font-family:var(--ff-display);font-size:1.2rem;
  color:var(--ink-on-light);min-width:0;
}
.ls-input:focus{outline:none;border-color:var(--flame)}

.ls-verdict{
  display:flex;align-items:flex-start;gap:11px;
  margin-top:14px;padding:13px 15px;
  border-radius:var(--r-md);
  animation:run-in .16s ease;
}
.ls-verdict.is-right{background:rgb(var(--yes-rgb) / .18);border:1.5px solid rgb(var(--yes-rgb) / .5)}
.ls-verdict.is-wrong{background:rgb(var(--no-rgb) / .16);border:1.5px solid rgb(var(--no-rgb) / .45)}
.ls-verdict-mark{font:700 1.3rem/1 var(--ff-ui)}
.ls-verdict.is-right .ls-verdict-mark{color:var(--yes)}
.ls-verdict.is-wrong .ls-verdict-mark{color:var(--no)}
.ls-verdict-body{display:flex;flex-direction:column;gap:2px}
.ls-verdict-main{font:700 1rem/1.3 var(--ff-ui);color:var(--fg)}
.ls-verdict-sub{font:400 0.99rem/1.3 var(--ff-ui);color:var(--fg-soft)}

.tile.is-done .tile-foot{color:var(--fg-soft)}

/* the card before a lesson starts */
.ls-intro-glyph{
  font-size:2.6rem;line-height:1;text-align:center;margin-bottom:16px;
}
.ls-facts{
  display:grid;grid-template-columns:repeat(3, minmax(0,1fr));gap:9px;margin-bottom:16px;
}
.ls-fact{
  padding:14px 8px;border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .2);
  display:flex;flex-direction:column;align-items:center;gap:3px;text-align:center;
}
.ls-fact-n{font:700 1.6rem/1 var(--ff-ui);color:var(--fg-accent)}
.ls-fact-l{font:400 0.85rem/1.25 var(--ff-ui);color:var(--fg-soft)}
.ls-intro-day{
  margin:0 0 4px;text-align:center;
  font:700 0.99rem/1.4 var(--ff-ui);color:var(--fg-soft);
}
.ls-intro-again{
  margin:10px 0 0;text-align:center;
  font:400 0.97rem/1.4 var(--ff-ui);color:var(--fg-soft);
}

/* --- the placement game -------------------------------------------- */

.pl-ask{
  padding:14px 15px;margin-bottom:14px;
  border-radius:var(--r-md);
  background:var(--paper);position:relative;
}
.pl-ask-de{
  display:block;padding-right:34px;
  font-family:var(--ff-display);
  font-size:clamp(1.05rem,4vw,1.3rem);line-height:1.4;
  color:var(--fg);
}
.pl-ask .speak{position:absolute;top:11px;right:11px}

/* ---- the board ----

   A CARD IS ONE SIZE. It used to be `repeat(2, minmax(0,1fr))`, so the width of a card
   was set by how many were on the board: two items on an iPad gave two
   enormous half-screen cards, four gave small ones, and the overlapping
   stack gave something different again. The picture dictated the card
   instead of the card containing the picture.

   Now the card has a fixed footprint and the row centres and wraps around
   it, which also puts the spare horizontal space on a wide screen to use
   rather than growing the board downward past the viewport. */
.pl-grid{
  display:flex; flex-wrap:wrap; justify-content:center; align-items:flex-end;
  gap:10px; margin-top:4px;
}
.pl-stack{
  display:flex;justify-content:center;align-items:flex-end;
  padding:6px 0 2px;min-height:150px;
}
.pl-stack .pl-card{margin-left:-30px}
.pl-stack .pl-card:first-child{margin-left:0}

/* One width for a card and for a hole, in BOTH layouts, so a gap in the
   board reads as a missing card rather than as a different kind of object.
   That was most of why the drop targets did not explain themselves. */
.pl-card, .pl-slot{
  width:clamp(104px, 30vw, 150px);
  flex:0 0 auto;
  box-sizing:border-box;
}
.pl-depth-0{z-index:1}
.pl-depth-1{z-index:2}
.pl-depth-2{z-index:3}

.pl-card{
  position:relative;
  border:2px solid var(--sand-deep);
  background:var(--paper);
  border-radius:var(--r-md);
  padding:7px 7px 5px;
  cursor:pointer;overflow:hidden;
  -webkit-tap-highlight-color:transparent;
  transition:transform .1s ease,border-color .12s ease;
}
.pl-card:not(:disabled):hover{transform:translateY(-2px);border-color:var(--flame)}
.pl-card:not(:disabled):active{transform:scale(.97)}
.pl-card .sp{border-radius:var(--r-sm)}
.pl-card-name{
  display:block;margin-top:5px;
  font:600 0.92rem/1.2 var(--ff-display);
  color:var(--fg);text-align:center;
}
.pl-card.is-anchor{border-color:var(--mauve);box-shadow:0 0 0 3px rgb(var(--mauve-rgb) / .3)}
.pl-card.is-right{border-color:var(--yes);box-shadow:0 0 0 3px rgb(var(--yes-rgb) / .35)}
.pl-card.is-wrong{border-color:var(--no);box-shadow:0 0 0 3px rgb(var(--no-rgb) / .3)}
.pl-card.is-held{border-color:var(--flame);box-shadow:0 0 0 3px rgb(var(--flame-rgb) / .3)}

.pl-slot{
  border:2px dashed var(--sand-deep);
  background:rgb(var(--shade-rgb) / .14);
  border-radius:var(--r-md);
  /* the same shape as the card that goes in it — the sprite is 2/3, plus
     the room the card's name line takes */
  aspect-ratio:2 / 2.55;
  min-height:0;
  display:flex;align-items:center;justify-content:center;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  transition:background .12s ease,border-color .12s ease;
}
/* A target that says it is a target. The dashes alone read as decoration;
   pulsing once on arrival is what makes them look droppable. */
.pl-slot:not(.is-past):not(.is-wrong){
  animation:plSlotWake 1.6s ease-in-out 2;
}
@keyframes plSlotWake{
  0%,100%{ border-color:var(--sand-deep); background:rgb(var(--shade-rgb) / .14) }
  50%    { border-color:var(--flame);     background:rgb(var(--flame-rgb) / .16) }
}
@media (prefers-reduced-motion: reduce){
  .pl-slot:not(.is-past):not(.is-wrong){ animation:none; border-color:var(--flame) }
}
/* where she put it, on a miss */
.pl-slot.is-wrong{
  border-style:solid; border-color:var(--no);
  background:rgb(var(--no-rgb) / .16);
}
.pl-slot.is-wrong .pl-slot-mark{ color:var(--no) }
.pl-slot:hover{border-color:var(--flame);background:rgb(var(--flame-rgb) / .12)}
.pl-slot-mark{font:700 1.8rem/1 var(--ff-ui);color:var(--fg-soft)}
.pl-stack .pl-slot{width:96px;margin-left:-30px}
.pl-stack .pl-slot:first-child{margin-left:0}

.pl-hand{
  display:flex;align-items:center;gap:11px;
  padding:9px 11px;margin-bottom:12px;
  border-radius:var(--r-md);
  background:rgb(var(--flame-rgb) / .14);
  border:1px solid rgb(var(--flame-rgb) / .3);
}
.pl-hand-l{font:700 0.94rem/1.3 var(--ff-ui);color:var(--fg-soft)}
.pl-hand .pl-card{width:88px;flex:0 0 auto}

/* --- pet pictures ---------------------------------------------------

   These are individual webp files rather than sprite-sheet cells, so the
   .sp rules above do not reach them. Without a width an <img> renders
   at its natural size, which for these drawings is enormous — the store
   showed one life-size pet per card.

   Sized here to match the sprite widths used elsewhere: 70px on a store
   card, 52px cheering on the end screen. */

.pet-img{
  display:block;
  width:70px;
  height:auto;
  margin:0 auto;
  border-radius:var(--r-sm);
}
/* ---------- Alina and Stella ----------

   The picture is shown whole. `width:100%; height:auto` and no
   aspect-ratio, deliberately: two of the forty-six comics are 4:3 and the
   rest 3:2, and forcing one ratio would letterbox or crop them. Nothing
   here depends on where a panel sits inside the image. */
.cm-intro{ margin-bottom:14px }
.cm-lede{ margin:6px 0 0; font-size:0.95rem; color:var(--fg-soft) }

.cm-unit{ margin:0 0 22px }
.cm-unit-h{
  margin:0 0 8px; padding-bottom:4px;
  font:var(--t-section); letter-spacing:var(--t-section-ls);
  color:var(--fg);
  border-bottom:2px solid rgb(var(--sand-rgb) / .35);
}
.cm-grid{ display:grid; grid-template-columns:repeat(auto-fill,minmax(150px,1fr)); gap:10px }

.cm-card{
  display:block; text-align:left; padding:0; cursor:pointer;
  border:1.5px solid var(--sand-deep); border-radius:var(--r-md);
  background:var(--paper); overflow:hidden;
  -webkit-tap-highlight-color:transparent;
}
.cm-card:hover{ border-color:var(--ink-on-light) }
.cm-thumb{ background:#fff; border-bottom:1px solid var(--sand-deep) }
/* The height is declared so the space is reserved before the picture
   arrives. Without this, every lazy thumbnail reflowed the whole list as
   it landed — which is what made the index jitter under her thumb, and it
   was never a scrolling problem. Two of the forty-six are 4:3 and are
   trimmed a little at this size; the full page is never cropped. */
.cm-thumb img{
  display:block; width:100%; height:auto;
  aspect-ratio:3 / 2; object-fit:cover;
}
.cm-card-cap{ display:flex; flex-direction:column; gap:2px; padding:8px 10px 10px }
.cm-card-n{ font:800 1rem/1.2 var(--ff-ui); color:var(--fg) }
/* Clamped to two lines. A grid row is as tall as its tallest card, so
   one comic with a long scene description made every card beside it
   grow to match and left the others half empty. Two lines is enough to
   tell them apart. */
.cm-card-scene{
  font-size:0.85rem; line-height:1.3; color:var(--fg-soft);
  display:-webkit-box; -webkit-line-clamp:2; -webkit-box-orient:vertical;
  overflow:hidden;
}
.cm-card-meta{ font-size:0.85rem; color:var(--fg-soft) }

/* ROOM FOR THE BACK BUTTON, WHICH IS NOT IN THE FLOW.

   Steven, 09 Sep: "the title for Unit 1 is under the BACK button, put
   some space above the title so the back button doesn't overlap."

   `.backlink` is `position:fixed` in the top-left corner, so it takes no
   space in the bar it is a child of — the title therefore starts at the
   left edge and lands underneath it. `comic.js` passes a `cm-back` class
   for exactly this and NO RULE FOR IT EVER EXISTED, so the class did
   nothing.

   Padding on the title rather than a margin on the button: the button is
   fixed and a margin on it would move the button, not make room. The
   index page's own `.cm-bar{ padding-top:52px }` further up the file
   handles the vertical case; this handles the horizontal one, which is
   what actually overlapped. */
.cm-bar{ display:flex; align-items:center; gap:10px; flex-wrap:wrap; margin:0 0 10px }
/* THE COMIC'S OWN COPY OF THE `.practice-title` OFFSET. The comic builds
   `.cm-bar` rather than using `.practice-head`, so the global rule does
   not reach it. Same 52px, for the same measured reason — see the note
   by `.practice-title`. If the comic bar is ever rebuilt on the standard
   header, delete this. */
.cm-bar-t{
  font:700 1rem/1 var(--ff-ui); color:var(--fg);
  padding-left:52px;
}
.cm-swap{
  margin-left:auto; padding:7px 12px; border-radius:999px; cursor:pointer;
  font:700 0.88rem/1 var(--ff-ui);
  border:1.5px solid var(--sand-deep); background:none; color:var(--fg);
  -webkit-tap-highlight-color:transparent;
}
.cm-swap:hover{ background:rgb(var(--sand-rgb) / .2) }

/* TAP THE PAGE TO SEE IT FULL SIZE.

   Steven, 09 Sep: "the comic has no lightbox to zoom into the comic, you
   can only see it on the tiny size."

   A comic page is lettering as well as drawing, and lettering at tile
   size is not readable. `GH.lightbox.openPic()` already existed and is
   already what the word list and the end screen use, so this is a
   wire-up rather than a new viewer.

   `cursor:zoom-in` and the corner hint say it is tappable — a picture
   that silently happens to be a button is a feature nobody finds. */
.cm-page{ cursor:zoom-in; position:relative }
.cm-zoom-hint{
  position:absolute; bottom:8px; right:8px;
  padding:4px 9px; border-radius:999px;
  background:rgb(var(--shade-rgb) / .62); color:#fff;
  font:700 .68rem/1 var(--ff-ui);
  text-transform:uppercase; letter-spacing:.09em;
  pointer-events:none;
}
.cm-page.is-gone{ cursor:default }
.cm-page.is-gone .cm-zoom-hint{ display:none }

.cm-page{
  max-width:900px; margin:0 auto 14px;
  border:1px solid var(--sand-deep); border-radius:var(--r-md);
  overflow:hidden; background:#fff;
}
.cm-page img{ display:block; width:100%; height:auto }
.cm-page.is-gone{
  aspect-ratio:3/2; display:flex; align-items:center; justify-content:center;
  border:2px dashed var(--sand-deep);
}
.cm-gone{ margin:0; padding:16px; text-align:center; color:var(--fg-soft) }

.cm-read{ max-width:900px; margin:0 auto }
.cm-where{ margin:0 0 4px; font-size:0.85rem; color:var(--fg-soft);
           font-variant-numeric:tabular-nums }
.cm-who{
  margin:0 0 4px; font:800 0.92rem/1.2 var(--ff-ui);
  text-transform:uppercase; letter-spacing:.06em; color:var(--fg-accent);
}
.cm-who.is-narr{ color:var(--fg-soft); font-weight:700; letter-spacing:.04em }
.cm-who.is-thought{ font-style:italic; text-transform:none; letter-spacing:0 }

/* the German is the largest thing here, and it is a button because
   tapping it says it again */
.cm-de{
  display:block; width:100%; text-align:left; margin:0;
  border:0; background:none; padding:0; cursor:pointer;
  font-family:var(--ff-display); font-size:1.35rem; line-height:1.35;
  color:var(--fg);
  -webkit-tap-highlight-color:transparent;
}
.cm-de:hover{ text-decoration:underline; text-underline-offset:3px }

/* The comic-to-comic row, under both views. Two buttons pushed to the
   ends, so prev sits left and next sits right whichever one is present —
   the hidden one holds its space rather than letting the other slide. */
.cm-cnav{
  display:flex; justify-content:space-between; align-items:center;
  gap:10px; margin:16px 0 4px;
}
.cm-cprev, .cm-cnext{ min-width:8.5em }
.cm-de.is-pending{ cursor:default; color:var(--fg-soft) }
.cm-de.is-pending:hover{ text-decoration:none }

/* From reading a song's words to being tested on them. Full width, above
   the cards, because it is the action the page now offers rather than a
   footnote to it. */
.sw-match{ display:block; width:100%; margin:4px 0 14px }

/* ---- two songs that are one thing ----

   Side by side where there is room, stacked on a phone. A pair of lyric
   sheets in two columns on a 380px screen is unreadable, and the point of
   the box is that they belong together — which the box says whether they
   sit beside each other or above each other. */
/* The heading above each language's group in the song list. Drawn only
   when more than one language is showing — see the note in songbook.js.
   Styled as a section eyebrow rather than a page title, because it
   divides a list rather than naming the screen. */
/* Steven, 09 Sep: "put a thin darker colored bar under it so it stands
   out a little bit more, not just a tiny blob of text that is easy to
   miss" — and bump the size by 4px.

   SIZE IS SET HERE, NOT THROUGH `--t-eyebrow`. That token is the app's
   eyebrow size and is used in dozens of places; raising it would move
   every one of them. 1.07rem is .82rem plus about 4px at a 16px root.

   THE BAR IS A SOLID DARKER RULE, not the faint hairline this had. It
   uses `--shade-rgb`, which is the token that darkens against the page
   on every theme — a fixed dark colour would vanish on the three dark
   themes. 3px, full width of the list, so the heading reads as a
   section divider rather than a caption. */
.sg-lang-head{
  margin:26px 0 12px;
  font:700 1.07rem/1.25 var(--ff-ui);
  letter-spacing:var(--t-eyebrow-ls);
  text-transform:uppercase;
  color:var(--fg);
  padding-bottom:9px;
  border-bottom:3px solid rgb(var(--shade-rgb) / .55);
}
.sg-lang-head:first-child{ margin-top:4px }

/* A FULL ROW OF THE TILE GRID, not a cell in it.

   `grid-column:1/-1` is what lets the box live inside `.tiles` at its
   own position in the list. Without it the box takes one cell and the
   two songs inside are squeezed to half width — which is why it used to
   sit outside the grid, at the cost of cutting the grid in two and
   leaving a part-filled row above it. See the note in songbook.js.

   `margin` is 0 now: the grid's own 14px gap spaces it, and a margin on
   a grid item adds to that rather than replacing it. */
.sg-pair{
  grid-column:1 / -1;
  margin:0;
  padding:14px;
  border-radius:var(--r-lg);
  background:rgb(var(--shade-rgb) / .30);
  border:1px solid rgb(var(--sand-rgb) / .55);
}
.sg-pair-l{
  margin:0 0 10px;
  font-size:.76rem; font-weight:700; letter-spacing:.10em;
  text-transform:uppercase; color:var(--fg-soft);
}
.sg-pair-two{ display:grid; grid-template-columns:1fr 1fr; gap:12px }
.sg-pair-half{ display:flex; flex-direction:column; gap:6px; min-width:0 }
.sg-pair-half .tile{ height:100% }
.sg-pair-voice{
  font-size:.74rem; font-weight:700; letter-spacing:.08em;
  text-transform:uppercase; color:var(--fg-accent);
}
@media (max-width:640px){
  .sg-pair-two{ grid-template-columns:1fr }
}

/* ================= the purchase flare =================

   Four intensities, one per tier, because the tiers cost wildly different
   things — three days and 500 Kronen against five months and 15,000 with
   three legendaries already owned. Same puff for both and the biggest
   moment in the app feels like the smallest.

   Everything here is decoration and `pointer-events:none`, so the card
   underneath stays fully operable while it plays. */

.ptd-card.is-bought{
  animation:ptdRise .5s cubic-bezier(.2,.9,.25,1.03) both;
}
@keyframes ptdRise{
  from{ transform:scale(.9) translateY(10px); opacity:0 }
  to  { transform:none; opacity:1 }
}

/* ---- the glow, per tier ---- */
.ptd-card.tier-common{
  box-shadow:0 0 0 1px rgb(var(--sand-rgb) / .7), 0 6px 30px rgb(var(--sand-rgb) / .45);
  animation:ptdRise .5s cubic-bezier(.2,.9,.25,1.03) both,
            ptdGlowSoft 2.2s ease-in-out 1;
}
.ptd-card.tier-rare{
  animation:ptdRise .5s cubic-bezier(.2,.9,.25,1.03) both,
            ptdGlowRare 2.4s ease-in-out 1;
}
.ptd-card.tier-epic{
  animation:ptdRise .5s cubic-bezier(.2,.9,.25,1.03) both,
            ptdGlowEpic 2.6s ease-in-out 1;
}
.ptd-card.tier-legendary{
  animation:ptdRise .5s cubic-bezier(.2,.9,.25,1.03) both,
            ptdGlowLegend 3s ease-in-out 1;
}
@keyframes ptdGlowSoft{
  0%,100%{ box-shadow:0 6px 24px rgb(0 0 0 / .28) }
  40%    { box-shadow:0 6px 30px rgb(var(--sand-rgb) / .55) }
}
@keyframes ptdGlowRare{
  0%,100%{ box-shadow:0 6px 24px rgb(0 0 0 / .28) }
  40%    { box-shadow:0 0 0 2px rgb(var(--mauve-rgb) / .55),
                      0 8px 44px rgb(var(--mauve-rgb) / .5) }
}
@keyframes ptdGlowEpic{
  0%,100%{ box-shadow:0 6px 24px rgb(0 0 0 / .28) }
  35%    { box-shadow:0 0 0 3px rgb(var(--flame-rgb) / .5),
                      0 10px 56px rgb(var(--flame-rgb) / .5) }
  70%    { box-shadow:0 0 0 2px rgb(var(--flame-rgb) / .35),
                      0 8px 40px rgb(var(--flame-rgb) / .35) }
}
@keyframes ptdGlowLegend{
  0%,100%{ box-shadow:0 6px 24px rgb(0 0 0 / .28) }
  25%    { box-shadow:0 0 0 3px rgb(var(--flame-rgb) / .75),
                      0 0 70px 10px rgb(var(--flame-rgb) / .55) }
  55%    { box-shadow:0 0 0 4px rgb(var(--sand-rgb) / .8),
                      0 0 90px 14px rgb(var(--flame-rgb) / .45) }
  80%    { box-shadow:0 0 0 3px rgb(var(--flame-rgb) / .6),
                      0 0 60px 8px rgb(var(--flame-rgb) / .4) }
}

/* the name lit, legendary only */
.ptd-card.tier-legendary .ptd-name{
  animation:ptdNameLit 2.6s ease-in-out 1;
}
@keyframes ptdNameLit{
  0%,100%{ text-shadow:none }
  45%    { text-shadow:0 0 18px rgb(var(--flame-rgb) / .85) }
}

/* Ember. The capstone gets one thing nothing else has: a slow aura that
   keeps turning after the sparks have gone. */
.ptd-card.is-capstone::before{
  content:''; position:absolute; inset:-30px; z-index:-1;
  border-radius:50%;
  background:conic-gradient(from 0deg,
    rgb(var(--flame-rgb) / .45), transparent 30%,
    rgb(var(--sand-rgb) / .35) 55%, transparent 80%,
    rgb(var(--flame-rgb) / .45));
  filter:blur(18px);
  animation:ptdAura 5.5s linear 2;
  pointer-events:none;
}
@keyframes ptdAura{ to{ transform:rotate(360deg) } }

/* ---- the sparks ---- */
.ptd-burst{
  position:absolute; inset:0; overflow:visible; pointer-events:none;
  display:grid; place-items:center;
}
.ptd-spark{
  position:absolute; width:8px; height:8px; border-radius:50%;
  background:var(--flame);
  box-shadow:0 0 10px 2px rgb(var(--flame-rgb) / .7);
  opacity:0;
  animation:ptdThrow 1.25s cubic-bezier(.15,.75,.2,1) var(--d) both;
}
/* every third one in the pale tone, so the burst is not one flat colour */
.ptd-spark:nth-child(3n){
  background:var(--sand);
  box-shadow:0 0 10px 2px rgb(var(--sand-rgb) / .7);
}
@keyframes ptdThrow{
  0%  { opacity:0; transform:rotate(var(--a)) translateY(0) scale(.3) }
  18% { opacity:1 }
  100%{ opacity:0;
        transform:rotate(var(--a)) translateY(calc(-1 * var(--r))) scale(var(--s)) }
}

/* ---- the rings ---- */
.ptd-ring{
  position:absolute; width:40px; height:40px; border-radius:50%;
  border:2px solid rgb(var(--flame-rgb) / .8);
  opacity:0; pointer-events:none;
  animation:ptdRing 1.4s ease-out .1s both;
}
.ptd-ring.is-two{
  border-color:rgb(var(--sand-rgb) / .85);
  animation-delay:.34s; animation-duration:1.6s;
}
@keyframes ptdRing{
  0%  { opacity:.9; transform:scale(.4) }
  100%{ opacity:0;  transform:scale(7) }
}

/* The card needs a stacking context for the aura's negative z-index, and
   the burst must be allowed outside its bounds. */
.ptd-card{ isolation:isolate }

/* Asked to stop animating: nothing moves, and the JS spawns no sparks at
   all rather than spawning them and hiding them. A single steady ring is
   left so a purchase still looks like an event. */
@media (prefers-reduced-motion: reduce){
  .ptd-card.is-bought,
  .ptd-card.tier-common, .ptd-card.tier-rare,
  .ptd-card.tier-epic, .ptd-card.tier-legendary,
  .ptd-card.tier-legendary .ptd-name,
  .ptd-card.is-capstone::before{ animation:none }
  .ptd-card.tier-rare     { box-shadow:0 0 0 2px rgb(var(--mauve-rgb) / .55) }
  .ptd-card.tier-epic     { box-shadow:0 0 0 3px rgb(var(--flame-rgb) / .5) }
  .ptd-card.tier-legendary{ box-shadow:0 0 0 3px rgb(var(--flame-rgb) / .75) }
}

/* The loud reset. Distinct from the ordinary ghost buttons beside it,
   because "erase everything" should not look like "show the welcome". */
.st-danger{
  border-color:var(--no) !important;
  color:var(--no) !important;
  margin-top:8px;
}
.st-danger:hover{
  background:linear-gradient(rgb(var(--no-rgb) / .12), rgb(var(--no-rgb) / .12)), var(--paper);
}
/* the item-by-item list of what goes: "everything" is not a list */
.st-warn-big{
  margin:0 0 6px; font-weight:700; font-size:1.02rem; color:var(--no);
}

/* ---- a word's example sentences, in the dictionary ----

   The search has always looked inside these; the row never showed them. So
   she could find a word because of a sentence and not be shown the sentence
   that matched. One word at a time — 773 rows with two sentences each is
   not a list any more.

   Its own declared ink: opaque light surface inside whatever is behind it. */
.ref-ex{
  /* Its own full-width line — see the note on .ref-row. */
  flex:0 0 100%;
  margin:2px 0 8px 0;
  padding:10px 12px;
  border-left:3px solid var(--flame);
  border-radius:0 var(--r-sm) var(--r-sm) 0;
  background:var(--sand-wash);
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
  display:flex; flex-direction:column; gap:8px;
}
.ref-ex-line{ display:flex; flex-direction:column; gap:2px }
.ref-ex-de{
  border:0; background:transparent; padding:0; cursor:pointer; text-align:left;
  font-family:var(--ff-display); font-size:1.02rem; line-height:1.35;
  color:var(--fg);
}
.ref-ex-de:hover{ text-decoration:underline }
.ref-ex-tr{ font-size:.88rem; line-height:1.35; color:var(--fg-soft) }
.ref-ex-none{
  flex:0 0 100%;
  margin:2px 0 8px; padding:8px 12px;
  font-size:.88rem; color:var(--fg-soft);
}

/* ================= WORD LAB — her course words =====================

   Two exercises, three rounds each: learn, match with colour, match
   without. Only the last one scores — the colour round can be finished with
   no German at all, which is what a scaffold is and exactly why it cannot
   count.

   THE FIVE TINTS ARE COMPOSITED, NOT HARDCODED. The first attempt used flat
   pale hexes and they were a bright patch on every dark theme. A tint over
   the theme's own surface darkens with the theme and the ink stays the
   theme's ink. Same technique as the pet tiers.

   The separator is the one fixed colour in here: a mid grey reads on light
   and dark alike, which is why a rule beats a panel. A panel needed two
   values and 12px of padding to do the same job. */

.wl-groups{ display:flex; flex-wrap:wrap; gap:8px; margin-top:10px }
.wl-group{
  flex:1 1 130px; min-width:0;
  display:flex; flex-direction:column; gap:2px; align-items:flex-start;
  border:1.5px solid var(--sand-deep); border-radius:var(--r-md);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
  color:var(--fg);
  padding:10px 12px; cursor:pointer; text-align:left;
  -webkit-tap-highlight-color:transparent;
}
.wl-group:hover{ border-color:var(--flame) }
.wl-group-n{ font:600 1rem/1.25 var(--ff-display) }
.wl-group-s{ font-size:.82rem; color:var(--fg-soft) }

/* ---- round 1, the learn phase ---- */

/* Next button + speed dial above the tiles. Same pill look as
   .autotoggle so this reads as one family of small controls rather than
   a new visual language. */
.wl-learn-controls{
  display:flex; align-items:center; justify-content:space-between;
  flex-wrap:wrap; gap:8px; margin:10px 0;
}
.wl-next-btn{
  border:1.5px solid rgb(var(--sand-rgb) / .4);
  background:transparent;
  color:var(--fg-soft);
  font:600 0.92rem/1 var(--ff-ui);
  letter-spacing:.04em;
  padding:9px 13px;border-radius:999px;cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.wl-next-btn:hover{ border-color:var(--flame); color:var(--fg) }
.wl-speed-dial{ display:flex; gap:4px }
.wl-speed-btn{
  border:1.5px solid rgb(var(--sand-rgb) / .4);
  background:transparent;
  color:var(--fg-soft);
  font:600 0.92rem/1 var(--ff-ui);
  width:34px; height:34px; border-radius:999px; cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.wl-speed-btn.is-active{
  background:rgb(var(--sand-rgb) / .22);
  color:var(--ink-on-dark);
  border-color:var(--soft-on-dark);
}
.wl-learn{ display:flex; flex-direction:column; gap:10px; min-height:210px }
/* A BUTTON NOW, so a sentence can be heard twice — see paintLearn(). The
   reset is what a button needs and a div did not: a browser default border,
   centred text and its own font would all arrive with the tag change. */
.wl-tile{
  border-radius:var(--r-md); padding:12px 14px;
  transition:opacity .7s ease;
  display:block; width:100%;
  border:0; font:inherit; color:inherit;
  text-align:left; cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.wl-tile:active{ transform:scale(.995) }

/* ---- the pair she just made, held for one reading ---- */
.wl-demo{
  display:flex; align-items:center;
  /* Matches `.wl-learn` so the board does not jump when this replaces it. */
  min-height:210px;
}
.wl-demo-tile{
  width:100%;
  border-radius:var(--r-md);
  padding:18px 16px;
  /* WAS invisible. This tile's colour comes entirely from the five
     `.is-<tint>` rules below, same as `.wl-w`/`.wl-p` on the board it just
     came from — but `.wl-demo-tile` was never added to those selector
     lists, and with no border of its own here either, the pair she'd just
     matched landed on plain page background: no bubble, no border,
     nothing to say "this is the thing you got right." Steven, live:
     "it holds it by itself on a plain screen... highlight what it's
     showing you." Border matches `.wl-w`/`.wl-p`'s base so the tint
     rules' `border-color` has something to override. */
  border:1.5px solid var(--sand-deep);
  background:var(--paper);
}
/* 60%, NOT 35%. At 35% the leaving tile is unreadable while it is still on
   the screen, which is worse than either being there or gone. */
.wl-tile.is-leaving{ opacity:.6 }
.wl-tile-w{ margin:0 0 3px; font-family:var(--ff-display); font-weight:600; line-height:1.25 }
.wl-tile-d{ margin:0; font-size:1.02rem; line-height:1.4 }
.wl-tile-s{ margin:0; font-size:1.02rem; line-height:1.4 }
.wl-in{ font-weight:700 }

/* ---- rounds 2 and 3, the board ---- */
/* WAS `display:flex` with `flex:1 1 0; min-width:0` on each tile — that
   combination has no floor, so with four words up top (BOARD = 4) the
   browser never wraps to a second row at all: it just keeps shrinking
   every tile until all four fit on one line, clipping the text against
   its neighbour rather than moving it down. Steven, live, screenshot in
   hand: "they clearly don't fit the text, so that should never have
   collapsed to just four in one row — that should've been 2x2."

   A fixed two-column grid is what BOARD = 4 actually wants: always two
   per row, so a long compound noun gets a full column's width to wrap
   into instead of being squeezed against three others. */
.wl-words{ display:grid; grid-template-columns:1fr 1fr; gap:8px; margin:14px 0 0 }
.wl-w{
  min-height:52px;
  display:flex; align-items:center; justify-content:center;
  border:1.5px solid var(--sand-deep); border-radius:var(--r-md);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  padding:8px 10px; cursor:pointer;
  font-family:var(--ff-display); font-weight:600; text-align:center;
  -webkit-tap-highlight-color:transparent;
  transition:border-color .12s ease, transform .12s ease;
}
/* A word too long even for a full column spans both — same pattern as
   `.option.is-wide` above. */
.wl-w.is-wide{ grid-column:1 / -1 }
.wl-w:hover{ border-color:var(--flame) }
.wl-w:active{ transform:scale(.98) }
.wl-w.is-picked{ border-color:var(--flame); border-width:2.5px }

/* the rule */
.wl-rule{
  height:4px; border-radius:2px;
  background:var(--sand-deep);
  margin:10px 0;
}

.wl-parts{ display:flex; flex-direction:column; gap:8px }
.wl-p{
  border:1.5px solid var(--sand-deep); border-radius:var(--r-md);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  padding:12px 13px; cursor:pointer; text-align:left;
  font-size:1.02rem; line-height:1.4;
  -webkit-tap-highlight-color:transparent;
  transition:border-color .12s ease;
}
/* ---- STAGE 4's COPY SCREEN --------------------------------------------

   The line above Continue, saying the word is coming back. Plain text on
   the surface it sits on, so it reads --fg and cannot go pale-on-pale. */
.wl-set{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  text-transform:uppercase;
  color:var(--fg-soft);
  margin:0 0 14px;
}
.wl-again{
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
  margin:14px 0 6px; text-align:center;
}

/* WHAT SHE TYPED, MARKED UP.

   OPAQUE, per the rule: a box with text in it is never even partially
   transparent, so this is a solid --paper ground and it DECLARES --fg
   rather than reading whatever it inherits.

   Monospace and wide tracking because the job is comparing one character
   against one character, and a proportional face puts an i and an m at
   different widths in two words that should line up.

   THE FOUR COLOURS ARE MEASURED, and against --paper in all twelve
   themes rather than the nine the table at the top of this file knows
   about. Worst case in each set:

     light themes (9)   wrong #A3123F  6.67    missing #A34A00  5.12
     dark themes (3)    wrong #F5A3BD  8.62    missing #F0B07A  8.89

   Dark pink and dark orange are unreadable on a dark ground, which is
   why the three dark themes get their own pair below rather than
   inheriting these. Do not add a colour here without measuring it the
   same way. */
.wl-diff{
  display:inline-flex; flex-wrap:wrap;
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  border:1.5px solid var(--sand-deep); border-radius:var(--r-md);
  padding:10px 12px; margin:10px 0;
  font-family:ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
  font-size:1.25rem; letter-spacing:.06em; line-height:1.5;
}
.wl-diff-slot{ display:block }
.wl-d-ok{ color:var(--fg) }
.wl-d-bad{ color:#A3123F; font-weight:700 }
.wl-d-miss{ color:#A34A00; font-weight:700; text-decoration:underline }
.wl-d-extra{ color:#A3123F; font-weight:700; text-decoration:line-through }

[data-theme="midnight"] .wl-d-bad,
[data-theme="ember"] .wl-d-bad,
[data-theme="pine"] .wl-d-bad,
[data-theme="midnight"] .wl-d-extra,
[data-theme="ember"] .wl-d-extra,
[data-theme="pine"] .wl-d-extra{ color:#F5A3BD }

[data-theme="midnight"] .wl-d-miss,
[data-theme="ember"] .wl-d-miss,
[data-theme="pine"] .wl-d-miss{ color:#F0B07A }


/* ---- THE BLANK, WHILE SHE IS TYPING IN IT ---------------------------

   The gap in the sentence becomes a field in place rather than sending
   her to a box elsewhere on the page. Inline, so the sentence keeps its
   shape around it and the line she is answering about stays on screen.

   OPAQUE, per the rule: it holds text, so it declares --fg and sits on a
   solid --paper ground. Nothing here is translucent. */
.wl-slot-live{
  display:inline-flex; align-items:center; flex-wrap:wrap; gap:6px;
  vertical-align:middle; margin:2px 0;
}
.wl-slot-word{
  font:var(--t-card); letter-spacing:var(--t-card-ls);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  border:1.5px solid var(--sand-deep); border-radius:var(--r-sm);
  padding:4px 8px;
}
.wl-slot-in{
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  border:1.5px solid var(--flame); border-radius:var(--r-sm);
  padding:8px 10px;
  font:var(--t-card); letter-spacing:var(--t-card-ls);
  min-width:6ch;
}
.wl-slot-in::placeholder{ color:var(--fg-soft) }
.wl-slot-go{ padding:8px 14px; font-size:.95rem }

.wl-p:hover{ border-color:var(--flame) }

/* ---- STAGE 3's HELP, AFTER THE WAIT ----------------------------------

   The listening board opens plain and stays plain for HELP_MS
   (js/activities/wordlab.js), then the right answer takes this. It is
   help, so it has to be unmissable; it is not one of the five group
   tints, so it must not look like one.

   OPAQUE, AND NOT NEGOTIABLE. Steven's rule: a box containing text is
   never even partially transparent, no exceptions. So this is a solid
   background and it DECLARES --fg rather than reading one, which is the
   test the sweep at the foot of this file settled on.

   --on-flame on --flame is the one emphasis pair in this stylesheet that
   has actually been measured, and unlike the table above it I checked it
   against all TWELVE themes rather than the nine that table knows about:

     sand mint plum  4.65    orchid moss lagoon tobacco  7.26
     citrus  7.03    grove  5.33    midnight  7.12
     ember  7.67     pine  8.99

   Worst case 4.65, so it clears AA for body text in every theme the app
   can be set to. Do not swap the colours for something prettier without
   measuring the replacement the same way. */
.wl-p.is-help{
  background:var(--flame);
  border-color:var(--flame);
  --fg:var(--on-flame); --fg-soft:var(--on-flame);
  color:var(--fg);
  font-weight:600;
}

/* ---------- THE ANSWER MESSAGE, WHERE SHE CAN ACTUALLY SEE IT ----------

   Two faults, both visible in one screenshot.

   IT WAS APPENDED TO THE END OF THE SCREEN. `flash()` adds it after the
   last answer tile, so on a four-choice listening round it landed below
   the fold — and scrolling to it put it under iOS Safari's floating
   toolbar. The one message telling her she was wrong was the one thing she
   could not read.

   AND IT FAILED CONTRAST BADLY. `--no` is #FF8B7B, a light coral chosen to
   read as tinted text ON A DARK CARD. As plain text on the light page it
   is pale-on-pale — Steven's "look how bad the contrast fails".

   So it is a fixed chip instead: always on screen wherever she has
   scrolled, sitting above the toolbar using the same 72px clearance `main`
   already reserves for it, and a solid ground with ink on top rather than
   coloured text on paper. */
.wl-flash{
  position:fixed;
  left:50%;
  transform:translateX(-50%);
  bottom:calc(84px + env(safe-area-inset-bottom, 0px));
  z-index:50;
  margin:0;
  max-width:calc(100% - 32px);
  padding:11px 20px;
  border-radius:999px;
  font-size:.95rem; font-weight:700;
  text-align:center;
  background:rgb(var(--ink-rgb));
  color:var(--paper);
  box-shadow:0 8px 26px rgb(var(--ink-rgb) / .4);
}
/* Coral is right for "wrong" — it just has to be the GROUND, with ink on
   it, rather than the text colour. */
.wl-flash.is-no{ background:var(--no); color:rgb(var(--ink-rgb)) }
.wl-flash.is-hint{ background:rgb(var(--ink-rgb) / .82); color:var(--paper); font-weight:600 }

/* ---- the five tints ----

   ROUND 2 ONLY. Round 3 has no color, which is the whole point of it.

   THESE ARE THE EXERCISE'S COLORS, NOT THE THEME'S. Steven's ruling. The
   version before this composited the app's own palette variables over
   `--paper` at 20% so the tints would follow whatever theme was on — and
   on the three dark themes that meant a near-black hue over a near-black
   surface. All five tiles came out the same color, so the scaffold was
   invisible in the one round whose entire job is the scaffold.

   The rest of the interface still follows the theme. These five do not: a
   color that says "these two belong together" has to read the same on
   every theme or it says nothing.

   ------------------------------------------------------------------
   THE DARK SET IS NOT A DARKENED PASTEL, AND HERE IS WHY IT COULD NOT BE

   The first dark attempt simply darkened Steven's five and desaturated
   them, and his verdict was "remarkably drab". It was, and the reason is
   measurable: coral sits at hue 17 degrees and amber at 37. Twenty degrees
   apart. Light and washed out that is two distinguishable pastels; dark
   and desaturated it is two identical browns.

   So the dark set keeps saturation UP — 55% rather than 34% — and pushes
   the two offenders apart: coral to 8 degrees, amber to 44. Thirty-six
   degrees between them instead of twenty. The other three keep their own
   hues, since nothing was crowding them.

   All three dark themes share the one set. Steven's call, and it is right:
   the color means the same thing on each of them, so it should look the
   same on each of them.

   Light grounds carry Steven's own ink. Dark grounds carry light ink,
   which is what a dark theme is for. */

.wl-tile.is-green, .wl-w.is-green, .wl-p.is-green, .wl-demo-tile.is-green{
  background:#DCEBC4; border-color:#BFD897;
  --fg:#173404; --fg-soft:#3B6D11;
  color:var(--fg);
}
.wl-p.is-green{ color:#3B6D11 }
.wl-tile.is-blue, .wl-w.is-blue, .wl-p.is-blue, .wl-demo-tile.is-blue{
  background:#CFE4F7; border-color:#97B9D8;
  --fg:#042C53; --fg-soft:#185FA5;
  color:var(--fg);
}
.wl-p.is-blue{ color:#185FA5 }
.wl-tile.is-pink, .wl-w.is-pink, .wl-p.is-pink, .wl-demo-tile.is-pink{
  background:#F7D8E3; border-color:#D897AE;
  --fg:#4B1528; --fg-soft:#993556;
  color:var(--fg);
}
.wl-p.is-pink{ color:#993556 }
.wl-tile.is-coral, .wl-w.is-coral, .wl-p.is-coral, .wl-demo-tile.is-coral{
  background:#FADBCF; border-color:#D8A997;
  --fg:#4A1B0C; --fg-soft:#993C1D;
  color:var(--fg);
}
.wl-p.is-coral{ color:#993C1D }
.wl-tile.is-amber, .wl-w.is-amber, .wl-p.is-amber, .wl-demo-tile.is-amber{
  background:#FBE3BC; border-color:#D8BF97;
  --fg:#412402; --fg-soft:#854F0B;
  color:var(--fg);
}
.wl-p.is-amber{ color:#854F0B }

/* the definition line inside a learn tile takes the softer of the pair */
.wl-tile-d{ color:var(--fg-soft) }
.wl-tile-s{ color:var(--fg) }

/* ---- the same five on the three dark themes ---- */
[data-theme="midnight"] .wl-tile.is-green,
[data-theme="ember"] .wl-tile.is-green,
[data-theme="pine"] .wl-tile.is-green,
[data-theme="midnight"] .wl-w.is-green,
[data-theme="ember"] .wl-w.is-green,
[data-theme="pine"] .wl-w.is-green,
[data-theme="midnight"] .wl-p.is-green,
[data-theme="ember"] .wl-p.is-green,
[data-theme="pine"] .wl-p.is-green,
[data-theme="midnight"] .wl-demo-tile.is-green,
[data-theme="ember"] .wl-demo-tile.is-green,
[data-theme="pine"] .wl-demo-tile.is-green{
  background:#314F17; border-color:#5E932F;
  --fg:#E4F7D4; --fg-soft:#BFE59E;
  color:var(--fg);
}
[data-theme="midnight"] .wl-p.is-green,
[data-theme="ember"] .wl-p.is-green,
[data-theme="pine"] .wl-p.is-green{ color:#BFE59E }
[data-theme="midnight"] .wl-tile.is-blue,
[data-theme="ember"] .wl-tile.is-blue,
[data-theme="pine"] .wl-tile.is-blue,
[data-theme="midnight"] .wl-w.is-blue,
[data-theme="ember"] .wl-w.is-blue,
[data-theme="pine"] .wl-w.is-blue,
[data-theme="midnight"] .wl-p.is-blue,
[data-theme="ember"] .wl-p.is-blue,
[data-theme="pine"] .wl-p.is-blue,
[data-theme="midnight"] .wl-demo-tile.is-blue,
[data-theme="ember"] .wl-demo-tile.is-blue,
[data-theme="pine"] .wl-demo-tile.is-blue{
  background:#17364F; border-color:#2F6693;
  --fg:#D4E7F7; --fg-soft:#9EC5E5;
  color:var(--fg);
}
[data-theme="midnight"] .wl-p.is-blue,
[data-theme="ember"] .wl-p.is-blue,
[data-theme="pine"] .wl-p.is-blue{ color:#9EC5E5 }
[data-theme="midnight"] .wl-tile.is-pink,
[data-theme="ember"] .wl-tile.is-pink,
[data-theme="pine"] .wl-tile.is-pink,
[data-theme="midnight"] .wl-w.is-pink,
[data-theme="ember"] .wl-w.is-pink,
[data-theme="pine"] .wl-w.is-pink,
[data-theme="midnight"] .wl-p.is-pink,
[data-theme="ember"] .wl-p.is-pink,
[data-theme="pine"] .wl-p.is-pink,
[data-theme="midnight"] .wl-demo-tile.is-pink,
[data-theme="ember"] .wl-demo-tile.is-pink,
[data-theme="pine"] .wl-demo-tile.is-pink{
  background:#4F172F; border-color:#932F5A;
  --fg:#F7D4E3; --fg-soft:#E59EBD;
  color:var(--fg);
}
[data-theme="midnight"] .wl-p.is-pink,
[data-theme="ember"] .wl-p.is-pink,
[data-theme="pine"] .wl-p.is-pink{ color:#E59EBD }
[data-theme="midnight"] .wl-tile.is-coral,
[data-theme="ember"] .wl-tile.is-coral,
[data-theme="pine"] .wl-tile.is-coral,
[data-theme="midnight"] .wl-w.is-coral,
[data-theme="ember"] .wl-w.is-coral,
[data-theme="pine"] .wl-w.is-coral,
[data-theme="midnight"] .wl-p.is-coral,
[data-theme="ember"] .wl-p.is-coral,
[data-theme="pine"] .wl-p.is-coral,
[data-theme="midnight"] .wl-demo-tile.is-coral,
[data-theme="ember"] .wl-demo-tile.is-coral,
[data-theme="pine"] .wl-demo-tile.is-coral{
  background:#4F1E17; border-color:#933C2F;
  --fg:#F7D8D4; --fg-soft:#E5A89E;
  color:var(--fg);
}
[data-theme="midnight"] .wl-p.is-coral,
[data-theme="ember"] .wl-p.is-coral,
[data-theme="pine"] .wl-p.is-coral{ color:#E5A89E }
[data-theme="midnight"] .wl-tile.is-amber,
[data-theme="ember"] .wl-tile.is-amber,
[data-theme="pine"] .wl-tile.is-amber,
[data-theme="midnight"] .wl-w.is-amber,
[data-theme="ember"] .wl-w.is-amber,
[data-theme="pine"] .wl-w.is-amber,
[data-theme="midnight"] .wl-p.is-amber,
[data-theme="ember"] .wl-p.is-amber,
[data-theme="pine"] .wl-p.is-amber,
[data-theme="midnight"] .wl-demo-tile.is-amber,
[data-theme="ember"] .wl-demo-tile.is-amber,
[data-theme="pine"] .wl-demo-tile.is-amber{
  background:#4F4017; border-color:#93782F;
  --fg:#F7EED4; --fg-soft:#E5D29E;
  color:var(--fg);
}
[data-theme="midnight"] .wl-p.is-amber,
[data-theme="ember"] .wl-p.is-amber,
[data-theme="pine"] .wl-p.is-amber{ color:#E5D29E }

/* ================= Listen and Speak — the pronunciation lab =================

   Every opaque light surface here declares its own dark ink. That is the
   trap that made the Jukebox song list render blank earlier: a cream
   background inside a dark card, with text still coloured for the card. */

.sp-h{
  margin:16px 0 8px; font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  text-transform:uppercase; color:var(--fg-soft);
}
.sp-h:first-child{ margin-top:0 }

.sp-opts{ display:flex; flex-wrap:wrap; gap:8px }
.sp-opt{
  flex:1 1 140px; min-width:0;
  display:flex; flex-direction:column; gap:2px; align-items:flex-start;
  border:2px solid var(--sand-deep); border-radius:var(--r-md);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
  color:var(--fg);
  padding:10px 12px; cursor:pointer; text-align:left;
  -webkit-tap-highlight-color:transparent;
  transition:border-color .12s ease, background .12s ease;
}
.sp-opt:hover:not(:disabled){ border-color:var(--flame) }
.sp-opt:disabled{ opacity:.45; cursor:not-allowed }
.sp-opt.is-on{
  border-color:var(--flame);
  background:linear-gradient(rgb(var(--flame-rgb) / .12), rgb(var(--flame-rgb) / .12)), var(--paper);
}
.sp-opt-n{ font:600 1rem/1.25 var(--ff-display) }
.sp-opt-s{ font-size:.82rem; color:var(--fg-soft) }

/* Break the line up without leaving the round. The setup screen asks the
   same question, but she finds out a line is too much AFTER she is looking
   at it, and going back would abandon the round. */
.sp-cuts{
  display:flex; align-items:center; flex-wrap:wrap; gap:6px;
  margin:0 0 12px;
}
.sp-cuts-l{
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  text-transform:uppercase; color:var(--fg-soft); margin-right:2px;
}
.sp-cut{
  border:1.5px solid var(--sand-deep); border-radius:999px;
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
  color:var(--fg);
  padding:5px 12px; cursor:pointer;
  font:600 .85rem/1 var(--ff-ui); white-space:nowrap;
  -webkit-tap-highlight-color:transparent;
}
.sp-cut:hover{ border-color:var(--flame) }
.sp-cut.is-on{
  border-color:var(--flame);
  background:linear-gradient(rgb(var(--flame-rgb) / .14), rgb(var(--flame-rgb) / .14)), var(--paper);
}

/* the line she is saying */
.sp-line{
  margin:14px 0 2px; padding:16px 14px;
  border-radius:var(--r-md);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
  display:flex; flex-wrap:wrap; gap:8px; justify-content:center;
}
.sp-chunk{
  font-family:var(--ff-display); font-size:1.35rem; line-height:1.35;
  color:var(--fg-soft);
}
/* the piece she is on, when the line is split */
.sp-chunk.is-now{
  color:var(--fg); font-weight:600;
  border-bottom:3px solid var(--flame);
}
/* What the line means. Larger and closer than the source line below it,
   because it is about the sentence and the source is only provenance. */
.sp-mean{
  margin:8px 0 2px; text-align:center;
  font-size:1.02rem; line-height:1.4; color:var(--fg);
}
/* The label, so a title is never read as a translation — which is exactly
   what happened when both lines were bare. */
.sp-lab{
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  text-transform:uppercase; color:var(--fg-soft);
  margin-right:2px;
}
.sp-mean-t{ color:var(--fg) }
.sp-from{
  margin:0 0 14px; text-align:center;
  font-size:.85rem; color:var(--fg-soft);
}

.sp-hear, .sp-rec{ display:block; width:100%; margin-bottom:9px }

/* Recording. A red pulse, because she needs to know at a glance that the
   microphone is live — an app that is listening should look like it. */
.sp-rec.is-rec{
  border-color:var(--no);
  background:linear-gradient(rgb(var(--no-rgb) / .18), rgb(var(--no-rgb) / .18)), var(--paper);
  --fg:var(--ink);
  animation:spRecPulse 1.1s ease-in-out infinite;
}
@keyframes spRecPulse{
  0%,100%{ box-shadow:0 0 0 0 rgb(var(--no-rgb) / .5) }
  50%    { box-shadow:0 0 0 7px rgb(var(--no-rgb) / 0) }
}
@media (prefers-reduced-motion: reduce){
  .sp-rec.is-rec{ animation:none; box-shadow:0 0 0 3px rgb(var(--no-rgb) / .5) }
}

.sp-modes{ display:flex; flex-wrap:wrap; gap:8px }
.sp-mode{
  flex:1 1 auto;
  border:1.5px solid var(--sand-deep); border-radius:999px;
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
  color:var(--fg);
  padding:8px 14px; cursor:pointer;
  font:600 .9rem/1 var(--ff-ui); white-space:nowrap;
  -webkit-tap-highlight-color:transparent;
}
.sp-mode:hover{ border-color:var(--flame) }
.sp-mode:active{ transform:scale(.97) }

.sp-note{ margin:12px 0 0; font-size:.9rem; line-height:1.45; color:var(--fg-soft) }
/* the https warning: the accent tone, because it is the answer rather than
   a footnote */
.sp-warn{
  margin:12px 0 0; padding:10px 12px;
  border-radius:var(--r-sm);
  background:linear-gradient(rgb(var(--no-rgb) / .14), rgb(var(--no-rgb) / .14)), var(--paper);
  --fg:var(--ink);
  color:var(--fg); font-size:.9rem; line-height:1.45; font-weight:600;
}

/* ================= COURSE WORDS — the redesigned lesson =================

   Three rounds, support removed each time. The tints are set inline from
   tanyawords.js because they are per-word and rotate; each one declares its
   own ink there, which is the rule for an opaque light surface.

   NOTE FOR LATER: those tints are hardcoded light values and this app has
   twelve themes. On a dark card they will read as bright patches. The fix
   is compositing the tint over the theme surface —
   `linear-gradient(rgb(tint / .14), same), var(--paper)` — the pattern the
   pet tiers already use. Left as-is for the first test because Steven
   chose these exact five by eye. */

.tw-h{
  margin:0 0 10px; font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  text-transform:uppercase; color:var(--fg-soft);
}
.tw-none{ margin:0; color:var(--fg-soft); line-height:1.5 }

.tw-set{
  display:flex; flex-direction:column; gap:2px; align-items:flex-start;
  width:100%; margin-bottom:8px;
  border:1.5px solid var(--sand-deep); border-radius:var(--r-md);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
  color:var(--fg);
  padding:12px 14px; cursor:pointer; text-align:left;
  -webkit-tap-highlight-color:transparent;
}
.tw-set:hover{ border-color:var(--flame) }
.tw-set-n{ font:600 1.05rem/1.25 var(--ff-display) }
.tw-set-s{ font-size:.85rem; color:var(--fg-soft) }

/* ---- round 1: the learn tile ----
   Word and meaning together, the oldest of three fading out at 60%. The
   first attempt used 35% and the word was unreadable while still there. */
.tw-learn{ display:flex; flex-direction:column; gap:10px; margin-bottom:16px }
.tw-tile{
  border-radius:var(--r-md); padding:12px 14px;
  transition:opacity .45s ease;
}
.tw-tile-w{ margin:0 0 3px; font-family:var(--ff-display); font-weight:600; line-height:1.3 }
.tw-tile-d{ margin:0; font-size:.95rem; line-height:1.4 }

/* ---- rounds 2 and 3: the board ----
   2x2 preferred, a full row where a word will not fit beside another. */
.tw-words{ display:flex; flex-wrap:wrap; gap:8px }
.tw-word{
  flex:1 1 0; min-width:0;
  min-height:52px; display:flex; align-items:center; justify-content:center;
  padding:8px 10px; cursor:pointer; text-align:center;
  border:1.5px solid var(--sand-deep); border-radius:var(--r-md);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  font-family:var(--ff-display); font-weight:600; line-height:1.25;
  -webkit-tap-highlight-color:transparent;
  transition:border-color .12s ease, transform .1s ease;
}
/* a long word claims its own row */
.tw-word.is-wide{ flex:1 1 100% }
.tw-word.is-on{ border-color:var(--flame); border-width:2.5px }
.tw-word:active{ transform:scale(.98) }

/* the separator: a rule, not a panel. A mid grey is the one colour that
   works unchanged on light and dark, which a dark panel does not. */
.tw-rule{
  height:4px; border-radius:2px;
  background:var(--sand-deep);
  margin:10px 0;
}

.tw-defs{ display:flex; flex-direction:column; gap:8px }
.tw-def{
  width:100%; text-align:left;
  padding:12px 13px; cursor:pointer;
  border:1.5px solid var(--sand-deep); border-radius:var(--r-md);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  font-size:1.05rem; line-height:1.4;
  -webkit-tap-highlight-color:transparent;
}
.tw-def:hover{ border-color:var(--flame) }
.tw-def.is-wrong{
  border-color:var(--no);
  animation:twShake .3s ease;
}
@keyframes twShake{
  0%,100%{ transform:translateX(0) }
  25%{ transform:translateX(-4px) }
  75%{ transform:translateX(4px) }
}
@media (prefers-reduced-motion: reduce){ .tw-def.is-wrong{ animation:none } }

.tw-note{ margin:14px 0 0; font-size:.9rem; line-height:1.45; color:var(--fg-soft) }

/* ---- round 4: heard, not seen ----
   A blank at the top that the word drops into once she has chosen. Fixed
   height, so revealing the word does not shift the buttons under her
   thumb mid-tap. */
.tw-slot{
  min-height:64px; display:flex; align-items:center; justify-content:center;
  margin:14px 0 10px; padding:8px 12px;
  border:2px dashed var(--sand-deep); border-radius:var(--r-md);
  background:transparent;
}
.tw-slot.is-shown{
  border-style:solid; border-color:var(--yes);
  background:linear-gradient(rgb(var(--yes-rgb) / .12), rgb(var(--yes-rgb) / .12)), var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
}
.tw-slot-w{
  font-family:var(--ff-display); font-weight:600; line-height:1.25;
  color:var(--fg);
}
.tw-slot-q{ font-size:1.6rem; color:var(--fg-soft); opacity:.5 }

.tw-ear{ display:block; width:100%; margin-bottom:10px }
.tw-again{ margin:0 0 10px; font-size:.92rem; font-weight:600; color:var(--no) }
.tw-later{ margin:0 0 10px; font-size:.92rem; line-height:1.4; color:var(--fg-soft) }

/* ---- round 5: typing it ----
   The first typed production in the app. The box sits at the top, what she
   is looking at sits under it. */
.tw-type-box{
  margin:14px 0 12px;
  border:2px solid var(--sand-deep); border-radius:var(--r-md);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  transition:border-color .15s ease;
}
.tw-type-box.is-wrong{ border-color:var(--no); animation:twShake .3s ease }
.tw-type-box.is-right{ border-color:var(--yes) }
@media (prefers-reduced-motion: reduce){ .tw-type-box.is-wrong{ animation:none } }
.tw-type-in{
  width:100%; box-sizing:border-box;
  border:0; background:transparent; outline:none;
  padding:14px 14px;
  font-family:var(--ff-display); font-size:1.35rem; font-weight:600;
  color:var(--fg);
}
.tw-type-in::placeholder{ font-weight:400; font-size:1rem; color:var(--fg-soft) }
.tw-type-in:disabled{ opacity:.7 }

.tw-mid{
  min-height:76px; display:flex; flex-direction:column;
  align-items:center; justify-content:center; gap:4px;
  margin-bottom:12px; padding:10px 12px;
  border-radius:var(--r-md);
  background:var(--sand-wash);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  text-align:center;
}
.tw-mid-w{
  margin:0; font-family:var(--ff-display); font-weight:600; line-height:1.25;
  color:var(--fg);
}
.tw-mid-d{ margin:0; font-size:1rem; line-height:1.4; color:var(--fg-soft) }
.tw-mid-note{ margin:0; font-size:.85rem; color:var(--fg-soft) }
.tw-send{ display:block; width:100% }

/* ---- a question asked in German, and the price of translating it ----

   Some topics borrow their whole vocabulary from English, so the translated
   question answers itself. She may still translate — she is told first what
   it costs, and never after. */
.rd-q-tr{ margin:0 0 12px; font-size:.9rem }
.rd-q-warn{
  margin:0 0 12px; padding:12px 14px;
  border-radius:var(--r-sm);
  background:linear-gradient(rgb(var(--no-rgb) / .14), rgb(var(--no-rgb) / .14)), var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  display:flex; flex-wrap:wrap; gap:8px;
}
.rd-q-warn p{
  flex:1 1 100%; margin:0 0 4px;
  font-size:.92rem; line-height:1.45; font-weight:600;
}
.rd-q-void{
  margin:0 0 12px; padding:8px 12px;
  border-radius:var(--r-sm);
  background:var(--sand-wash);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg-soft); font-size:.88rem;
}

/* ---- the word list for a reading piece ----
   Identical to the song word list; the two screens do the same job.
   Cloned rather than shared so a change to one cannot silently
   restyle the other. */
.rw-match{ display:block; width:100%; margin:4px 0 14px }
.rw-count{
  margin:14px 0 0;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft); text-transform:uppercase;
}
.rw-list{
  display:grid;
  grid-template-columns:1fr;
  gap:8px;
  margin:10px 0 0;
}
.rw-card{
  display:flex; gap:12px; align-items:flex-start;
  padding:11px 13px;
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .14);
  border-left:3px solid var(--sand-deep);
}
.rw-pic{
  flex:0 0 auto;
  width:62px;
  border:0; padding:0; margin:0; background:none;
  border-radius:10px;
  overflow:hidden;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.rw-pic.rw-lens{
  box-shadow:0 0 0 1px rgb(var(--shade-rgb) / .3);
  transition:box-shadow .12s, transform .12s;
}
.rw-pic.rw-lens:hover{
  box-shadow:0 0 0 2px var(--sand-deep);
  transform:scale(1.04);
}
.rw-body{ flex:1 1 auto; min-width:0 }
.rw-de{
  display:block; width:100%; text-align:left;
  border:0; background:none; padding:0; margin:0;
  color:var(--fg); cursor:pointer;
  font:var(--t-de); letter-spacing:var(--t-de-ls);
  -webkit-tap-highlight-color:transparent;
}
.rw-de:hover{ text-decoration:underline }
.rw-gloss{
  margin:2px 0 0;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg);
}
.rw-other{
  margin:1px 0 0;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}
.rw-def{
  margin:5px 0 0;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}
.rw-senses{ list-style:none; margin:4px 0 0; padding:0 }
.rw-sense + .rw-sense{
  margin-top:5px; padding-top:5px;
  border-top:1px solid rgb(var(--shade-rgb) / .28);
}
.rw-sense-gloss{
  display:block;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg);
}
.rw-sense-def{
  display:block; margin-top:1px;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}
.rw-missing{
  margin:16px 0 0; padding:11px 14px;
  border-radius:var(--r-md);
  border:1px solid var(--no);
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}
.rw-de.is-phrase{ font-style:normal }
.rw-de.is-slot{ font-style:italic }

/* free, unlike the translation beside it */
.rd-words{ }

/* ---- jump to a tier, at the top of the reader ----

   Three filled tiers put the poems below the fold, so the row of tiles she
   wants is the one she cannot see. Built from the same SECTIONS list that
   draws the sections, so an empty tier has no button. */
.rd-jump{
  display:flex; flex-wrap:wrap; gap:8px; margin:2px 0 16px;
}
.rd-jump-b{
  border:1.5px solid var(--sand-deep);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
  color:var(--fg);
  border-radius:999px; padding:7px 15px; cursor:pointer;
  font:600 .92rem/1 var(--ff-ui);
  -webkit-tap-highlight-color:transparent;
  transition:border-color .12s ease, background .12s ease;
}
.rd-jump-b:hover{ border-color:var(--flame) }
.rd-jump-b:active{ transform:scale(.97) }

/* ---- one pet, over the grid ----

   A panel rather than a screen change. Tapping a pet used to jump to the
   store, which from mid-round cost her the round for what is only a
   question: what is this one, and can I have it. */
.ptd-wrap{
  position:fixed; inset:0; z-index:758
  /* 758, not 80 — above the tour scrim (700), below the
     bubble (900). the pet detail panel. A panel below the scrim gets
     dimmed during a tour and its contents cannot be lit, because a
     child cannot escape its ancestor’s stacking context. */;
  display:flex; align-items:center; justify-content:center;
  padding:18px; background:rgb(0 0 0 / .6); overflow-y:auto;
}
.ptd-card{
  position:relative; max-width:420px; width:100%;
  text-align:center; padding:20px 18px 16px;
}
.ptd-img{ width:150px; height:150px; object-fit:contain; margin:0 auto 4px; display:block }
.ptd-name{ margin:0; font:var(--t-section); letter-spacing:var(--t-section-ls) }
.ptd-de{
  margin:2px 0 10px; font-family:var(--ff-display); font-size:1.05rem;
  color:var(--fg-accent);
}
.ptd-line{
  border:0; background:transparent; cursor:pointer; padding:0;
  font-family:var(--ff-display); font-size:1.1rem; line-height:1.35;
  color:var(--fg); text-align:center;
}
.ptd-line:hover{ text-decoration:underline }
.ptd-tr{ margin:4px 0 0; font-size:.92rem; line-height:1.4; color:var(--fg-soft) }
.ptd-acts{ display:flex; flex-direction:column; gap:8px; margin-top:16px }
.ptd-go{ width:100% }
/* The gate and the price. `pre-line` because needText() joins its lines
   with \n — one line per requirement, each with her own number on it. */
/* the shortfall, and the plain "not yet" — said in the warning colour so it
   reads as the answer rather than as more requirements */
.ptd-short{
  margin:0; font-size:.95rem; font-weight:700; color:var(--fg-accent);
}
/* the card body is a button now; it must not look like one */
.pt-ask{
  border:0; background:transparent; padding:0; cursor:pointer;
  text-align:inherit; color:inherit; font:inherit; display:block; width:100%;
}
.ptd-need{
  margin:0; white-space:pre-line;
  font-size:.95rem; line-height:1.5; color:var(--fg-soft);
}
.ptd-more{
  margin-top:14px; border:0; background:transparent; cursor:pointer;
  font-size:.86rem; font-weight:700; color:var(--fg-accent);
  text-decoration:underline;
}
.ptd-x{
  position:absolute; top:6px; right:10px;
  border:0; background:transparent; cursor:pointer;
  font-size:1.5rem; line-height:1; color:var(--fg-soft); padding:4px 8px;
}

/* ---- a grammar rule read WITHOUT leaving the exercise ----

   Same pattern as .howto-overlay: it covers the page, nothing behind it is
   destroyed, and closing it puts her back mid-round with her streak intact.
   The alternative — rendering the topic into the activity's own container —
   is what used to throw the round away. */
.gr-overlay{
  position:fixed; inset:0; z-index:755
  /* 755, not 70 — above the tour scrim (700), below the
     bubble (900). the grammar rule overlay, openable mid-round. A panel below the scrim gets
     dimmed during a tour and its contents cannot be lit, because a
     child cannot escape its ancestor’s stacking context. */;
  display:none; overflow-y:auto;
  background:rgb(0 0 0 / .55);
  padding:16px;
}
.gr-overlay.is-open{ display:block }
.gr-over-box{
  max-width:760px; margin:0 auto;
  background:var(--page); border-radius:var(--r-lg);
  padding:14px 14px 22px;
  /* the page's own ink, because the box is the page colour */
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
}
.gr-over-bar{
  display:flex; align-items:center; gap:10px;
  margin-bottom:10px; flex-wrap:wrap;
}
/* Return is the primary action and sits beside Back, because Back is what
   she will reach for and this is the one that keeps her place. */
.gr-over-return{ margin-left:auto }
.gr-over-body > .card:first-child{ margin-top:0 }

/* ---- her pet, in the coach's panels ----

   The face at the size the end screen uses, not a thumbnail: the point is
   that somebody is there. The German is a button because tapping it hears
   the pet say it, the same contract as the hub and the end screen. */
.co-pet{ display:flex; align-items:flex-start; gap:12px; margin-bottom:4px; text-align:left }
.co-pet-img{ width:76px; height:76px; object-fit:contain; flex:0 0 auto }
.co-pet-body{ display:flex; flex-direction:column; gap:3px; min-width:0 }
.co-pet-name{
  font-size:.78rem; font-weight:700; letter-spacing:.08em;
  text-transform:uppercase; color:var(--fg-soft);
}
.co-pet-de{
  border:0; background:transparent; padding:0; cursor:pointer; text-align:left;
  font-family:var(--ff-display); font-size:1.12rem; font-weight:600;
  line-height:1.3; color:var(--fg);
}
.co-pet-de:hover{ text-decoration:underline }
.co-pet-tr{ margin:0; font-size:.9rem; line-height:1.35; color:var(--fg-soft) }

/* ================= Jukebox ================= */

.jb-h{
  margin:0; font:var(--t-section); letter-spacing:var(--t-section-ls); color:var(--fg);
}
.jb-head{ display:flex; align-items:center; gap:10px; margin-bottom:10px }
.jb-clear{
  margin-left:auto; border:0; background:transparent; cursor:pointer;
  font-size:.86rem; font-weight:700; color:var(--fg-accent);
  text-decoration:underline; padding:4px 2px;
}
.jb-note{ margin:10px 0 0; font-size:.86rem; line-height:1.45; color:var(--fg-soft) }

/* ---- the transport ---- */
.jb-now-l{
  margin:0; font-size:.78rem; font-weight:700; letter-spacing:.08em;
  text-transform:uppercase; color:var(--fg-soft);
}
.jb-now{
  margin:4px 0 0; font-family:var(--ff-display); font-size:1.35rem;
  font-weight:600; line-height:1.25; color:var(--fg);
}
.jb-pos{ margin:4px 0 0; font-size:.86rem; font-weight:700; color:var(--fg-accent) }
.jb-controls{ display:flex; align-items:center; gap:14px; margin-top:16px }
.jb-btn{
  width:56px; height:56px; border-radius:999px;
  border:1.5px solid var(--sand-deep); background:var(--paper);
  /* opaque light surface -> declare --fg so the currentColor SVG icon stays
     visible on light themes (same rule as .jb-song). Steven, Sep. */
  --fg:var(--ink);
  font-size:1.2rem; line-height:1; color:var(--fg); cursor:pointer;
}
.jb-btn:disabled{ opacity:.4; cursor:not-allowed }
.jb-btn:hover:not(:disabled){ border-color:var(--ink-on-light) }
/* Bigger, and the only filled one: with the screen off this is the button
   she will have been reaching for. */
.jb-play{
  width:68px; height:68px; font-size:1.5rem;
  background:var(--flame); color:var(--on-flame); border-color:var(--flame);
}
.jb-opts{ display:flex; flex-wrap:wrap; gap:8px; margin-top:16px }
.jb-opts .chip:disabled{ opacity:.45; cursor:not-allowed }

/* ---- the queue ---- */
.jb-list{ list-style:none; margin:0; padding:0 }
.jb-row{
  display:flex; align-items:center; gap:4px;
  border-top:1px solid rgb(var(--sand-rgb) / .45);
}
.jb-row:first-child{ border-top:0 }
.jb-row.is-here{ background:rgb(var(--flame-rgb) / .10) }
.jb-row.is-broken .jb-row-t{ color:var(--fg-soft); text-decoration:line-through }
.jb-row-go{
  flex:1; display:flex; align-items:baseline; gap:10px;
  border:0; background:transparent; cursor:pointer;
  padding:12px 6px; text-align:left; color:inherit;
}
.jb-row-n{ font-size:.86rem; font-weight:700; color:var(--fg-soft); min-width:1.4em }
.jb-row-t{ font-size:1rem; font-weight:600; line-height:1.3; color:var(--fg) }
.jb-row-x{ font-size:.78rem; font-weight:700; color:var(--no) }
.jb-move{
  border:0; background:transparent; cursor:pointer;
  font-size:1.05rem; line-height:1; color:var(--fg-soft);
  padding:12px 8px;
}
.jb-move:disabled{ opacity:.28; cursor:not-allowed }
.jb-move:hover:not(:disabled){ color:var(--fg) }
.jb-rm:hover{ color:var(--no) }

/* ---- choosing songs ---- */
.jb-songs{ display:flex; flex-direction:column; gap:8px }
.jb-song{
  display:flex; align-items:center; gap:10px;
  padding:12px; text-align:left; cursor:pointer;
  border:1.5px solid var(--sand-deep); border-radius:var(--r-md);
  /* DECLARE --fg on this opaque light surface, per the app's rule (see the
     note at "an opaque light background must DECLARE --fg"). Without it the
     song name inherited a light --fg from a dark-surface ancestor and went
     unreadable on light themes. Steven, Sep. */
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--ink);
  background:var(--paper); color:var(--fg);
}
.jb-song:hover{ border-color:var(--ink-on-light) }
.jb-song.is-on{ border-color:var(--flame); background:linear-gradient(rgb(var(--flame-rgb) / .10), rgb(var(--flame-rgb) / .10)), var(--paper) }
.jb-song-mark{
  width:1.5em; text-align:center; font-weight:700; color:var(--fg-accent);
}
.jb-song-t{ font-size:1rem; font-weight:600; line-height:1.3 }

/* ================= Word Matching ================= */

.wm-h{
  margin:18px 0 8px; font:var(--t-section); letter-spacing:var(--t-section-ls);
  color:var(--fg);
}
.wm-h:first-child{ margin-top:0 }
.wm-note{ margin:8px 0 0; font-size:.86rem; color:var(--fg-soft) }
.wm-count{ margin:6px 0 0; font-size:.9rem; font-weight:700; color:var(--fg-accent) }
.wm-lens{ display:flex; flex-wrap:wrap; gap:8px; margin-top:4px }
.wm-toggle{ margin-left:0 }
.wm-go{ display:block; width:100%; margin-top:22px }

/* ---- the listening stage ----

   Fixed rather than in the flow: the picture is meant to fill the window,
   and one tap anywhere on it is the whole control surface. */
.wm-stage{
  /* 750, not 60. The tour points at `.wm-board` and `.wm-go`, which live
     inside here. A child's z-index is relative to its stacking context,
     so `.bt-lit` at 810 inside a z-60 fixed parent still painted at 60 —
     under the tour's dim at 700, which meant the highlighted thing was
     the one thing being greyed out. Above the scrim, below the bubble. */
  position:fixed; inset:0; z-index:750;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:18px; padding:24px;
  background:var(--paper); cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
/* No picture — either the word has none, or she asked for none. Dark, so a
   loop running in a quiet room at night is not a lit screen. */
.wm-stage.is-dark{ background:#1b1b1d }
.wm-stage.is-dark .wm-word{ color:#f4f2ee }
.wm-stage.is-dark .wm-meta{ color:rgb(244 242 238 / .55) }

/* A PICTURE, NOT A POSTER.

   This was `width:min(78vw, 62vh)` with `max-height:70vh`, sized for a
   phone held up while she listens with her hands free. On an iPad 78vw is
   most of the screen and the drawing became a billboard — the viewport
   units had no ceiling, so the bigger the screen the more absurd it got.

   Now a real maximum in pixels. `min()` still lets it shrink on a narrow
   phone, where 78vw is genuinely about right; it simply stops growing past
   the size of a large card. */
.wm-pic{ width:min(78vw, 62vh, 340px); max-height:min(70vh, 460px) }
.wm-pic .sp{ width:100%; border-radius:var(--r-lg) }
.wm-word{
  margin:0; text-align:center;
  font-family:var(--ff-display); font-size:2rem; font-weight:600;
  color:var(--fg);
}
/* On the dark screen the word IS the screen, so it is set as large as the
   longest German compound allows. */
.wm-word.is-big{ font-size:clamp(2rem, 11vw, 4.5rem); line-height:1.1 }
.wm-meta{
  position:absolute; top:16px; right:18px; margin:0;
  font-size:.95rem; font-weight:700; letter-spacing:.06em;
  color:var(--fg-soft);
}
/* `top:auto` IS THE WHOLE FIX AND IT IS NOT OPTIONAL.

   `.backlink` is `position:fixed` with a `top`, and this rule moved the
   button to the bottom by adding `bottom` — leaving BOTH set. A fixed
   element given a top and a bottom does not sit at either: it stretches
   to span the distance between them, which on a phone is a back button
   roughly 1500px tall lying across the picture. */
.wm-exit{ position:fixed; top:auto; left:16px; bottom:16px; z-index:61 }

.wm-paused{
  position:absolute; inset:0; z-index:1;
  display:flex; flex-direction:column; align-items:center; justify-content:center;
  gap:14px; padding:24px;
  background:rgb(0 0 0 / .62);
}
.wm-paused-t{
  margin:0; font-family:var(--ff-display); font-size:1.6rem; color:#f4f2ee;
}
.wm-paused-n{ margin:0; font-size:.9rem; color:rgb(244 242 238 / .7); text-align:center }

/* ---- the question after the loop ---- */
.wm-ask-n{ margin:0; font-size:1.15rem; font-weight:700; color:var(--fg) }
.wm-ask-paid{ margin:6px 0 0; font-size:.95rem; font-weight:700; color:var(--fg-accent) }
.wm-acts{ display:flex; flex-direction:column; gap:10px; margin-top:6px }

/* ---- the board ----

   Two columns, and they are shuffled independently in the JS so no row
   ever sits beside its own answer. */
.wm-board{ display:grid; grid-template-columns:1fr 1fr; gap:10px }
.wm-col{ display:flex; flex-direction:column; gap:10px }
.wm-cell{
  padding:14px 12px; min-height:52px;
  border:1.5px solid var(--sand-deep); border-radius:var(--r-md);
  background:var(--paper); color:var(--fg);
  font-size:1rem; font-weight:600; line-height:1.25; text-align:center;
  cursor:pointer;
  /* `box-shadow` joined the list so the glow fades in with the border
     instead of snapping on. Same .12s, so the two move together. */
  transition:opacity .35s ease, border-color .12s ease, box-shadow .12s ease;
}
.wm-cell:hover:not(:disabled){ border-color:var(--ink-on-light) }
/* A GLOW, NOT A FILL. This was flame at 14% over paper, which measures a
   contrast ratio of 1.18 against an unpicked tile — 1.0 being identical.
   Steven: "I have selected food is ready which you could see very
   clearly Given it's .00001% different color than the other buttons."

   A solid flame fill was tried first and works, but Steven's call was
   better: "put a very, very obvious glow effect when you click a button
   that way it wouldn't interfere with legibility."

   The tile keeps its paper background and its ink, so the word reads
   exactly as it did — the change is entirely OUTSIDE the text. It also
   removes a real problem the fill created: a wrong pair keeps
   `is-picked` and adds `is-wrong`, and `is-wrong` only changes the border
   and the text colour, so against a flame fill the word went pale grey on
   orange. With a glow there is nothing to resolve.

   Two shadows: a hard 4px ring for the edge and a soft 20px bloom for the
   glow. `.wm-board` is a plain grid with no `overflow`, so neither is
   clipped, and the 10px gap means the bloom bleeds slightly into the
   space around the tile rather than onto its neighbours. */
/* PINK, so it cannot be confused with either answer state. Steven asked
   for yellow or pink; pink wins on the cream paper every theme uses for a
   tile, where a yellow glow washes out. Fixed rather than themed, for the
   same reason the store's tier colours are: this is a signal, and a
   signal that changes hue with the palette is not one.

   Same two shadows as before — a hard 4px ring for the edge, a soft 20px
   bloom for the glow — and the tile keeps its paper background and its
   ink, so the word reads exactly as it did. */
.wm-cell.is-picked{
  border-color:#FF4098;
  box-shadow:0 0 0 4px rgb(255 64 152 / .60),
             0 0 20px 6px rgb(255 64 152 / .50);
}

/* RIGHT: green for one second, then `.is-gone` fades it. wordmatch.js
   holds `hit` for 1000ms before setting `gone`, so this is what she sees
   when a pair lands. */
.wm-cell.is-hit{
  border-color:var(--yes);
  box-shadow:0 0 0 4px rgb(var(--yes-rgb) / .70),
             0 0 22px 7px rgb(var(--yes-rgb) / .55);
}
/* WRONG: red for two seconds, and the pair stays where it is.
   wordmatch.js clears `wrong` after 2000ms. No `color` change any more —
   dimming the text made a wrong pair harder to read at the exact moment
   she is trying to work out what went wrong. */
.wm-cell.is-wrong{
  border-color:var(--no);
  box-shadow:0 0 0 4px rgb(var(--no-rgb) / .70),
             0 0 22px 7px rgb(var(--no-rgb) / .55);
}
/* A cell can be both — a wrong pair keeps `is-picked` — and two glows on
   one tile is no signal at all. Wrong wins. */
.wm-cell.is-picked.is-wrong{
  border-color:var(--no);
  box-shadow:0 0 0 4px rgb(var(--no-rgb) / .70),
             0 0 22px 7px rgb(var(--no-rgb) / .55);
}
/* A matched pair fades but keeps its slot, so the board does not jump
   under her hand. The JS refills only once two or more are free. */
.wm-cell.is-gone{ opacity:0; pointer-events:none }
.wm-col-right .wm-cell{ font-weight:700 }

/* ---- quizzes kept for later ---- */
.wm-saved-row{ display:flex; align-items:center; gap:8px; margin-top:8px }
.wm-saved-go{ flex:1; text-align:left }
.wm-saved-x{
  border:0; background:transparent; color:var(--fg-soft);
  font-size:1.4rem; line-height:1; padding:6px 10px; cursor:pointer;
}
.wm-saved-x:hover{ color:var(--no) }

/* ---------- the comic's whole text, on one screen ----------

   The reference view. Panels are separated because a panel is the unit of
   the drawing above it, and a wall of lines with no breaks in it cannot be
   matched up against the picture. */
.cm-tools{ flex-wrap:wrap }
/* Two toggles in one row. `.mode-toggle` carries `margin-left:auto`, which
   with two of them splits the free space between the pair and leaves both
   floating in the middle. The first is pinned left so the view sits by the
   picture and the narrator sits at the far end. */
.cm-tools .mode-toggle:first-child{ margin-left:0 }
.cm-voice button{ letter-spacing:.06em }
.cm-all{ display:flex; flex-direction:column; gap:18px; margin-top:14px }
.cm-all-panel{
  display:flex; flex-direction:column; gap:10px;
  padding-left:12px;
  border-left:2px solid rgb(var(--sand-rgb) / .45);
}
.cm-all-line{ display:flex; flex-direction:column; gap:2px }
/* Tighter than the single-line view, which sets one line at reading size
   in the middle of a card. Thirteen of those is a scroll. */
.cm-all-line .cm-de{ font-size:1.02rem; line-height:1.4; text-align:left }
.cm-all-line .cm-who{ margin:0 }
.cm-all-line .cm-mean{ margin:0 }
.cm-all-line .cm-de:disabled{ cursor:default; opacity:1 }
.cm-playall{ margin-bottom:4px }

/* A narrator language this comic has no text for. Dimmed and inert rather
   than hidden, so the German being unwritten is visible instead of the
   button silently not existing. */
.mode-toggle button:disabled{ opacity:.38; cursor:not-allowed }
.cm-pending-note{ margin:6px 0 0; font-size:0.86rem; color:var(--fg-soft) }
.cm-mean{ margin:10px 0 0; font:400 1.05rem/1.45 var(--ff-ui); color:var(--fg-soft) }

.cm-acts{ display:flex; gap:8px; margin-top:12px; flex-wrap:wrap }
.cm-btn{
  padding:8px 13px; border-radius:999px; cursor:pointer;
  font:700 0.92rem/1 var(--ff-ui);
  border:1.5px solid var(--sand-deep); background:none; color:var(--fg);
  -webkit-tap-highlight-color:transparent;
}
.cm-btn:hover:not(:disabled){ background:rgb(var(--sand-rgb) / .25) }
.cm-btn:disabled{ opacity:.45; cursor:default }
/* Auto is a switch, not an action, so it has to look held down when it
   is on. Filled rather than merely outlined: an outline-only 'on' state
   is indistinguishable from Play sitting beside it. */
.cm-btn[aria-pressed="true"]{
  background:var(--sand); color:var(--ink-on-sand);
  border-color:var(--sand);
}
.cm-btn[aria-pressed="true"]:hover{ background:var(--sand-deep) }

.cm-nav{ display:flex; gap:10px; margin-top:16px }
.cm-nav .btn{ flex:1 }

@media (max-width:430px){
  .cm-grid{ grid-template-columns:1fr 1fr }
  .cm-de{ font-size:1.2rem }
}

/* A word with no picture. Abstract vocabulary — hoffentlich, sogar, auf
   jeden Fall — cannot be drawn, and the alternative was showing image 1
   and teaching a wrong meaning silently. */
/* `.sprite-word` and `.sprite-word-text` stood here: a two-element
   design — an outer box and an inner text node — that sprite.js does not
   build. It makes ONE div, `sp sp-noart`, with a `sp-word` span inside,
   and live rules for both of those names already exist at the foot of
   this file. Deleted rather than renamed, for the same reason as the
   orphan base rule above: renaming them would have produced a second
   `.sp-word` and a second `.sp-noart`, half of whose properties then lose
   to the live ones on specificity or order. */

.pt-card .pet-img{width:140px}
.pt-lens{
  display:block;padding:0;border:0;background:none;cursor:zoom-in;
  -webkit-tap-highlight-color:transparent;
}
.pt-lens:focus-visible{outline:2px solid var(--flame);outline-offset:2px;border-radius:var(--r-sm)}
.pt-lens .pet-img{width:140px}
.pt-lens:active .pet-img{transform:scale(.97)}
/* the default, for anywhere that does not say how many */
.pt-cheer-one .pet-img{width:104px}

/* What the pet says. A bubble rather than a caption: it is speech, and it
   is the one thing on this screen written to be read for pleasure. */
.pt-say{
  position:relative;
  max-width:32rem; margin:0 auto 16px; padding:14px 16px 12px;
  border-radius:var(--r-md);
  background:var(--paper);
  border:2px solid var(--sand-deep);
  text-align:left;
}
.pt-say::before{
  content:''; position:absolute; top:-9px; left:26px;
  width:14px; height:14px; transform:rotate(45deg);
  background:var(--paper);
  border-left:2px solid var(--sand-deep); border-top:2px solid var(--sand-deep);
}
.pt-say-who{
  display:block; margin-bottom:3px;
  font:700 0.88rem/1.2 var(--ff-ui); text-transform:uppercase;
  letter-spacing:.06em; color:var(--fg-soft);
}
.pt-say-de{
  display:block; width:100%; text-align:left;
  border:0; background:none; padding:0; cursor:pointer;
  font-family:var(--ff-display); font-size:1.3rem; line-height:1.35;
  color:var(--fg);
  -webkit-tap-highlight-color:transparent;
}
.pt-say-de:hover{ text-decoration:underline; text-underline-offset:3px }
.pt-say-tr{
  margin:8px 0 0; font:400 1.02rem/1.45 var(--ff-ui);
  color:var(--fg-soft);
}
.pt-say-acts{ display:flex; gap:8px; margin-top:10px }
.pt-say-btn{
  padding:7px 12px; border-radius:999px; cursor:pointer;
  font:700 0.9rem/1 var(--ff-ui);
  border:1.5px solid var(--sand-deep);
  background:none; color:var(--fg);
  -webkit-tap-highlight-color:transparent;
}
.pt-say-btn:hover:not(:disabled){ background:rgb(var(--sand-rgb) / .25) }
.pt-say-btn:disabled{ opacity:.45; cursor:default }
.pl-card .pet-img,
.pt-hand .pet-img{width:100%}

.pet-blank{
  display:block;
  font-size:2rem;line-height:1;text-align:center;
  color:var(--fg-soft);
}
.pt-cheer-one .pet-blank{font-size:1.6rem;color:var(--fg-accent)}

/* ============================================================
   THE READER — short stories, medium stories, poems

   Uses the type roles and the contextual ink throughout, so nothing
   here names a font family or a colour. Everything sits inside .card,
   which is a dark surface, so --fg is already correct.
   ============================================================ */

.rd-sec{ margin:0 0 26px }
.rd-sec-h{
  font:var(--t-section); letter-spacing:var(--t-section-ls);
  color:var(--fg); margin:0 0 10px;
  padding-bottom:5px; border-bottom:2px solid rgb(var(--sand-rgb) / .35);
}

/* A tile whose questions are resting is still a tile — she can open it
   and read. Dimmed, not disabled: the piece is available, the questions
   are not, and those are different things. */
.tile.is-resting{ opacity:.72 }

/* --- the piece itself ---------------------------------------

   A story is set as rows with air between them. A poem is set tight,
   because the line break is the form and a paragraph gap between every
   line destroys the shape. `verse` on the piece decides which.
   ----------------------------------------------------------- */
.rd-body{ display:flex; flex-direction:column; gap:8px; margin:0 0 18px }
.rd-body.is-verse{ gap:2px }

.rd-line{
  display:flex; gap:10px; align-items:baseline; width:100%;
  text-align:left; background:none; border:0;
  border-radius:var(--r-sm); padding:7px 9px; cursor:pointer;
  color:var(--fg);
  -webkit-tap-highlight-color:transparent;
}
.rd-line:hover{ background:rgb(var(--sand-rgb) / .14) }

.rd-n{
  flex:none; min-width:1.6em; text-align:right;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft); opacity:.65;
}
.rd-line-body{ display:flex; flex-direction:column; min-width:0 }
.rd-de{ font:var(--t-de); letter-spacing:var(--t-de-ls); color:var(--fg) }
/* the poem's lines are shorter and there are more of them, so the German
   sits a size down from a story's and the block still reads as verse */
.is-verse .rd-de{ font-size:clamp(1.1rem,4.2vw,1.45rem); line-height:1.35 }
.rd-tr{ font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft); margin-top:1px }

.rd-note{
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft); margin:14px 0 0;
}

/* --- a question --------------------------------------------- */

.rd-kind{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  text-transform:uppercase; color:var(--fg-accent); margin:0 0 8px;
}
.rd-q{
  font:var(--t-section); letter-spacing:var(--t-section-ls);
  color:var(--fg); margin:0 0 18px;
}

.rd-choices{ display:flex; flex-direction:column; gap:9px }

.rd-choice{
  display:flex; gap:10px; align-items:center; width:100%;
  text-align:left; cursor:pointer;
  padding:12px 14px; border-radius:var(--r-md);
  border:1.5px solid rgb(var(--sand-rgb) / .4);
  background:rgb(var(--shade-rgb) / .16);
  color:var(--fg);
  font:var(--t-body); letter-spacing:var(--t-body-ls);
  -webkit-tap-highlight-color:transparent;
  transition:border-color .12s ease, background .12s ease;
}
.rd-choice:hover:not(:disabled){ border-color:var(--sand-deep) }
.rd-choice.is-picked{
  border-color:var(--sand); background:rgb(var(--sand-rgb) / .18);
}
.rd-choice:disabled{ cursor:default }

/* Right and wrong are never theme accents — green is right and red is
   wrong in every palette, which is the one reading a theme may not
   override. Same rule the rest of the app follows. */
.rd-choice.is-right{
  border-color:var(--yes); background:rgb(var(--yes-rgb) / .18); color:var(--fg);
}
.rd-choice.is-wrong{
  border-color:var(--no); background:rgb(var(--no-rgb) / .18); color:var(--fg);
}

.rd-choice-t{ flex:1; min-width:0 }

/* the tick on a choose-all-that-apply option */
.rd-box{
  flex:none; width:1.5em; height:1.5em; border-radius:5px;
  border:1.5px solid rgb(var(--sand-rgb) / .55);
  display:flex; align-items:center; justify-content:center;
  font:var(--t-btn); color:var(--fg);
}

/* the position she has given a line in an ordering question, and — after
   she answers — the position it should have had */
.rd-pos, .rd-was{
  flex:none; min-width:1.7em; height:1.7em; border-radius:999px;
  display:flex; align-items:center; justify-content:center;
  font:var(--t-btn); letter-spacing:var(--t-btn-ls);
}
.rd-pos{ background:var(--sand); color:var(--ink-on-sand) }
.rd-was{ border:1.5px dashed var(--yes); color:var(--yes); margin-left:auto }

/* --- a song whose languages break differently -----------------

   Two columns, German left and hers right, each keeping its own line
   count. Not a table: nothing here should imply the third line of one
   column is the third line of the other, so the rows do not align and
   are not meant to.
   ----------------------------------------------------------- */
.sg-par-note{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  text-transform:none; color:var(--fg-soft); margin-left:9px; opacity:.8;
}
.sg-cols{ display:grid; grid-template-columns:1fr 1fr; gap:14px }
@media (max-width:520px){ .sg-cols{ grid-template-columns:1fr; gap:16px } }

.sg-col{ display:flex; flex-direction:column; min-width:0 }
.sg-col-h{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  text-transform:uppercase; color:var(--fg-accent);
  margin:0 0 6px; padding-bottom:4px;
  border-bottom:1px solid rgb(var(--sand-rgb) / .3);
}
.sg-par-line{
  text-align:left; background:none; border:0; padding:5px 6px;
  border-radius:var(--r-sm); color:var(--fg);
  font:var(--t-de); letter-spacing:var(--t-de-ls);
  font-size:clamp(.98rem,3.4vw,1.12rem); line-height:1.35;
  cursor:pointer; -webkit-tap-highlight-color:transparent;
}
.sg-par-line:hover:not(:disabled){ background:rgb(var(--sand-rgb) / .14) }
/* the translation column is the meaning, not something to hear — a de-DE
   voice reading Russian is noise */
.sg-par-line:disabled{ cursor:default; color:var(--fg-soft) }

/* in the Zeilen view, a line that exists in one language only */
.sg-line-one .sg-tr{ font-style:italic; opacity:.7 }

/* --- a cheering pet is tappable ------------------------------

   `.pt-cheer-one` becomes a button when there is a picture to enlarge,
   which drags in two rules that were written for the shelf: .pt-lens
   sets display:block, and .pt-lens .pet-img sets 140px. Neither is
   wanted here — the end screen stacks the name and the word under the
   drawing, and its size is set by how many pets turned up. Two classes
   beat one, so these win regardless of source order.
   ----------------------------------------------------------- */
.pt-cheer-one.pt-lens{
  display:flex; flex-direction:column; align-items:center; gap:3px;
  padding:0; border:0; background:none; cursor:zoom-in; font:inherit;
  color:inherit; text-align:center;
  -webkit-tap-highlight-color:transparent;
}
/* One pet is active and full size; the others are shrunk. Nothing else
   on screen says which one is speaking, so the size has to. The active
   size is the same as it was when there was only one speaker, so a single
   pet looks exactly as it did. */
.pt-cheer-1 .pt-cheer-one.is-on .pet-img{ width:140px }
.pt-cheer-2 .pt-cheer-one.is-on .pet-img{ width:130px }
.pt-cheer-3 .pt-cheer-one.is-on .pet-img{ width:118px }
.pt-cheer-one.is-off .pet-img{ width:62px }

/* Dimmed as well as small, and it lifts on hover so it reads as something
   to press rather than something disabled. */
.pt-cheer-one.is-off{ opacity:.62 }
.pt-cheer-one.is-off:hover{ opacity:1 }
.pt-cheer-one.is-off .pt-cheer-name,
.pt-cheer-one.is-off .pt-cheer-word{ font-size:.74rem }

/* The row is bottom-aligned so the shrunk pets stand on the same ground
   as the full-size one rather than floating at its shoulder. */
.pt-cheer{ align-items:flex-end }

/* The bubble is replaced in place when she rotates, so it needs a slot
   that does not collapse and shift the buttons below it. */
.pt-say-slot{ min-height:1px }

.pt-cheer-one.pt-lens .pet-img{ transition:width .14s ease }
.pt-cheer-one.pt-lens:active .pet-img{ transform:scale(.97) }
.pt-cheer-one.pt-lens:focus-visible{
  outline:2px solid var(--flame); outline-offset:3px; border-radius:var(--r-sm);
}

/* --- what the blanked word means -----------------------------

   Shown only after she has answered. The German is a button because
   hearing it is the point — she has just written it, and the sound is
   what makes it stick. The gloss sits beside it rather than under it: it
   is one short line, not a paragraph.
   ----------------------------------------------------------- */
.fb-word{
  display:flex; flex-wrap:wrap; align-items:baseline; gap:10px;
  margin:10px 0 0; padding:9px 12px;
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .16);
  border-left:3px solid var(--sand-deep);
}
.fb-word-de{
  border:0; background:none; padding:0; cursor:pointer;
  font:var(--t-card); letter-spacing:var(--t-card-ls);
  color:var(--fg);
  -webkit-tap-highlight-color:transparent;
}
.fb-word-de:hover{ text-decoration:underline }
.fb-word-gloss{
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
}

/* ==============================================================
   THE DICTIONARY
   --------------------------------------------------------------
   Two surfaces. The headword list is buttons on the page; a sense
   is a .card, so it takes the dark ground and its own --fg with
   it and the text inside needs no colour of its own.

   The type roles do the sizing: --t-card for the German, --t-sub
   for her gloss, --t-body for the definition. Nothing here sets a
   px font size, so the four data-type variants keep working.
   ============================================================== */

.dc-list{
  display:flex; flex-direction:column; gap:8px;
  margin:14px 0 0;
}
.dc-entry{
  display:grid;
  grid-template-columns:1fr auto;
  grid-template-areas:"de n" "gloss gloss";
  gap:2px 12px;
  width:100%; text-align:left;
  padding:12px 14px;
  border:0; border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .14);
  border-left:3px solid var(--sand-deep);
  color:var(--fg);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.dc-entry:hover{ background:rgb(var(--shade-rgb) / .22) }
.dc-entry-de{
  grid-area:de;
  font:var(--t-card); letter-spacing:var(--t-card-ls);
}
.dc-entry-n{
  grid-area:n;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
  white-space:nowrap;
}
/* Every meaning on one line, so the list answers the question
   without her opening anything. */
.dc-entry-gloss{
  grid-area:gloss;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
}

/* The lessons overview list — Steven: "A button at the top that lets
   you get an overview of all lessons would be helpful," so the ~20
   lesson cards on the hub have a compact, scannable alternative. Same
   row shape as .dc-entry (the dictionary list) rather than a new one. */
.ls-list{
  display:flex; flex-direction:column; gap:8px;
  margin:14px 0 0;
}
.ls-entry{
  display:flex; align-items:center; gap:12px;
  width:100%; text-align:left;
  padding:12px 14px;
  border:0; border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .14);
  border-left:3px solid var(--sand-deep);
  color:var(--fg);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.ls-entry:hover{ background:rgb(var(--shade-rgb) / .22) }
.ls-entry-glyph{ font-size:1.3rem; flex:0 0 auto }
.ls-entry-body{ display:flex; flex-direction:column; gap:2px; min-width:0; flex:1 1 auto }
.ls-entry-title{ font:var(--t-card); letter-spacing:var(--t-card-ls) }
.ls-entry-sub{ font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft) }
.ls-entry-status{ flex:0 0 auto; color:var(--fg-accent); font-weight:700 }

.dc-say{ margin:14px 0 4px }

.dc-sense{ margin:12px 0 0; padding:16px 18px }
.dc-sense-num{
  margin:0 0 6px;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
  text-transform:uppercase;
}
.dc-sense-de{
  display:block;
  border:0; background:none; padding:0; margin:0 0 6px;
  text-align:left; cursor:pointer;
  font:var(--t-de); letter-spacing:var(--t-de-ls);
  color:var(--fg);
  -webkit-tap-highlight-color:transparent;
}
.dc-sense-de:hover{ text-decoration:underline }
.dc-sense-gloss{
  margin:0;
  font:var(--t-card); letter-spacing:var(--t-card-ls);
  color:var(--fg);
}
/* The other language, quieter. Nazar reads the English. */
.dc-sense-alt{
  margin:2px 0 0;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
}
/* Sense-level, so a drawing never implies it covers the other
   meanings. Narrow, because it illustrates rather than tests. */
.dc-sense-pic{
  width:min(46%, 190px);
  margin:12px 0 4px;
  border-radius:var(--r-md);
  overflow:hidden;
}
.dc-sense-def{
  margin:10px 0 0;
  font:var(--t-body); letter-spacing:var(--t-body-ls);
  color:var(--fg);
}
/* The German definition second and dimmer: there when she wants
   it, not competing with the one she can read. */
.dc-sense-defde{
  margin:6px 0 0;
  padding-top:6px;
  border-top:1px solid rgb(var(--shade-rgb) / .28);
  font:var(--t-body); letter-spacing:var(--t-body-ls);
  color:var(--fg-soft);
}

/* --- the same thing inside fill-blank ------------------------
   A word with several meanings cannot be explained on one line,
   so the flex row above gets a list under it. Same surface, no
   card: it sits inside one. */
.fb-senses{
  list-style:none;
  margin:8px 0 0; padding:9px 12px;
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .16);
  border-left:3px solid var(--sand-deep);
}
.fb-sense{ margin:0 }
.fb-sense + .fb-sense{
  margin-top:8px; padding-top:8px;
  border-top:1px solid rgb(var(--shade-rgb) / .28);
}
.fb-sense-gloss{
  display:block;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg);
}
.fb-sense-def{
  display:block; margin-top:2px;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}

/* ==============================================================
   THE DIALOGUES
   --------------------------------------------------------------
   A conversation, not a list. The two speakers are told apart
   three ways at once — which side the turn sits on, the A / B
   mark, and the accent down the edge — because on a phone in
   sunlight one signal is not enough.

   `.is-live` marks the line being spoken. It is moved by class
   rather than by repaint, so playback does not rebuild eight
   buttons and lose the scroll position.
   ============================================================== */

/* TWO PER ROW, same reasoning as the hub tiles.

   A dialogue card holds a German title, an English one and `8 lines` —
   about 100px of stacked text given the full width of a phone for the sake
   of a title that needs half of it. Eighteen dialogues at one per row is
   most of a screen each.

   Grid rather than the column flex it was, `minmax(0,1fr)` so a long German
   compound breaks instead of pushing the column wide, and the original
   single column back above 620px where a card has room to breathe. */
.dg-list{
  display:grid;
  grid-template-columns:repeat(2, minmax(0,1fr));
  gap:8px;
  margin:14px 0 0;
}
@media (min-width:620px){
  .dg-list{ grid-template-columns:repeat(auto-fill, minmax(240px,1fr)) }
}
.dg-card{
  display:flex; flex-direction:column; gap:3px;
  width:100%; min-width:0; overflow-wrap:break-word;
  text-align:left;
  padding:13px 15px;
  border:0; border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .14);
  border-left:3px solid var(--sand-deep);
  color:var(--fg); cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.dg-card:hover{ background:rgb(var(--shade-rgb) / .22) }
.dg-card-de{ font:var(--t-card); letter-spacing:var(--t-card-ls) }
.dg-card-mine{ font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft) }
.dg-card-meta{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}

/* Listen / Practise */
.dg-modes{ display:flex; gap:8px; margin:14px 0 0 }
.dg-mode{
  flex:1;
  padding:9px 14px;
  border:1px solid rgb(var(--shade-rgb) / .3);
  border-radius:999px;
  background:transparent; color:var(--fg-soft);
  font:var(--t-btn); letter-spacing:var(--t-btn-ls);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.dg-mode.is-on{
  background:var(--sand); color:var(--liver);
  border-color:var(--sand);
}

.dg-play{ width:100%; margin:12px 0 0 }

.dg-talk{ display:flex; flex-direction:column; gap:10px; margin:16px 0 0 }

.dg-row{
  display:flex; gap:10px; align-items:flex-start;
  padding:10px 13px;
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .12);
  border-left:3px solid transparent;
}
/* Turn-taking, three signals deep. */
.dg-a{ border-left-color:var(--sand-deep); margin-right:14% }
.dg-b{ border-left-color:var(--flame); margin-left:14%;
       background:rgb(var(--shade-rgb) / .2) }

.dg-who{
  flex:0 0 auto;
  width:1.5em; height:1.5em; line-height:1.5em;
  text-align:center; border-radius:999px;
  background:rgb(var(--shade-rgb) / .3);
  color:var(--fg-soft);
  font:var(--t-eyebrow); letter-spacing:0;
}
.dg-a .dg-who{ background:var(--sand-deep); color:var(--liver) }
.dg-b .dg-who{ background:var(--flame); color:var(--on-flame) }

.dg-body{ flex:1 1 auto; min-width:0 }
.dg-line{
  display:block; width:100%; text-align:left;
  border:0; background:none; padding:0; margin:0;
  color:var(--fg); cursor:pointer;
  font:var(--t-de); letter-spacing:var(--t-de-ls);
  -webkit-tap-highlight-color:transparent;
}
.dg-line:hover{ text-decoration:underline }

/* ---- CRYSTALS ------------------------------------------------------

   Balance, a seven-day bar chart, records, and the earning rules. Every
   surface here is opaque and declares its ink, per the rule; the bars are
   the one exception in spirit and not in fact — they are solid fills with
   no text on them. */
.cr-top{ text-align:center }
.cr-have{ display:flex; align-items:baseline; justify-content:center; gap:8px }
.cr-have-mark{ font-size:1.6rem; color:var(--fg-accent) }
.cr-have-n{ font:700 clamp(2.4rem,11vw,3.6rem)/1 var(--ff-display); color:var(--fg) }
.cr-have-l{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  text-transform:uppercase; color:var(--fg-soft); margin:6px 0 0;
}
.cr-life{ display:flex; align-items:baseline; justify-content:center; gap:8px; margin:12px 0 0 }
.cr-life-n{ font:var(--t-card); letter-spacing:var(--t-card-ls); color:var(--fg) }
.cr-life-l{ font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft) }

/* The banner is a CROP of a gallery sheet now, not a standalone image,
   so it is sized here rather than by the <img>'s own dimensions. `.sp`
   inside it fills the width and takes its height from the base rule's
   1/1 aspect ratio. */
/* ---- THE CURRENCY MARK -----------------------------------------------

   `GH.coins.mark()` builds `<span class="crystal-mark"><img></span>`, and
   `markWith(n)` wraps that plus a number in `.crystal-n`. Sized in `em`
   so one rule covers every place it appears — 14px beside the header
   balance, 16px on an achievement row, 24px+ on the Crystals page — which
   is the reason the art is an SVG and not a WebP.

   The span carries the character as a fallback when the SVG 404s, so it
   needs no size of its own; the img does. */
.crystal-mark{ display:inline-flex; align-items:center; line-height:1 }
.crystal-mark img{
  width:1em; height:1em; display:block;
  /* `contain`, so a mark drawn to any aspect is never stretched. */
  object-fit:contain;
}
.crystal-n{ display:inline-flex; align-items:center; gap:.35em; line-height:1 }

/* SPACE ABOVE IT. The section's own paragraph sits directly above, and
   with no margin the text ran straight into the top of the picture.
   Steven: "Put a bit of space so the text there doesn't get smashed
   against the image of the geode." */
.cr-banner{ margin:18px 0 8px }

/* TALLER, AND NOT FULL WIDTH.

   `.sp` is a 3x3 sheet crop and its base rule is `aspect-ratio:1/1`,
   which is right for a square cell and wrong here — the crystals are
   drawn vertical, so a square box cut the base of the geode off.

   The box's aspect has to match the CELL's aspect, not the sheet's:
   `background-size:300% 300%` handles the 3x3 division whatever shape the
   box is, so this only needs to be the right shape for one cell. 3/4 for
   a portrait crystal.

   IF THE SHEET CELLS ARE ACTUALLY SQUARE, this is the one line to change
   back to 1/1 — the symptom would be white space above and below the
   crystal instead of a clipped base.

   Capped and centred rather than full width: at 380px wide a full-bleed
   banner is most of the first screen, and the numbers are the point of
   the page. */
.cr-banner .sp{
  width:100%; max-width:300px; margin:0 auto;
  aspect-ratio:3 / 4;
  border-radius:var(--r-lg);
}
.cr-banner-cap{
  display:flex; align-items:baseline; justify-content:center;
  gap:10px; flex-wrap:wrap; margin:0 0 14px;
}
.cr-banner-de{ font:var(--t-card); letter-spacing:var(--t-card-ls); color:var(--fg) }
.cr-banner-gl{ font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft) }
.cr-quest-pic .sp{ width:34px; border-radius:var(--r-sm) }

/* A quest row is a real route into the activity it names. Keep the existing
   card look, but reset native button chrome and give keyboard/touch feedback. */
.cr-quest-sub{
  font:var(--t-body); letter-spacing:var(--t-body-ls);
  color:var(--fg-soft); margin:0 0 10px;
}
.cr-quest{
  display:flex; align-items:center; gap:10px;
  width:100%; padding:10px 12px; margin-top:8px;
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg); text-align:left; font:inherit;
  border:1.5px solid var(--sand-deep); border-radius:var(--r-md);
  appearance:none; -webkit-appearance:none; cursor:pointer;
}
.cr-quest:hover{ border-color:var(--flame) }
.cr-quest:active{ transform:translateY(1px) }
.cr-quest:focus-visible{ outline:3px solid var(--flame); outline-offset:2px }
.cr-quest.is-done{ border-color:var(--flame) }
.cr-quest-pic{ flex:0 0 auto; width:34px; height:34px; border-radius:var(--r-sm) }
.cr-quest-mark{ flex:0 0 auto; font-size:1.1rem; line-height:1; color:var(--fg-soft) }
.cr-quest.is-done .cr-quest-mark{ color:var(--fg-accent) }
.cr-quest-l{
  flex:1; min-width:0;
  font:var(--t-body); letter-spacing:var(--t-body-ls); color:var(--fg);
}
.cr-quest.is-done .cr-quest-l{ color:var(--fg-soft) }
.cr-quest-of{
  flex:0 0 auto;
  font:var(--t-eyebrow); letter-spacing:0; color:var(--fg-accent);
}
.cr-quest-pay{
  flex:0 0 auto;
  font:var(--t-eyebrow); letter-spacing:0; color:var(--fg-soft);
}
.cr-quest-go{
  flex:0 0 auto;
  font-size:1.45rem; line-height:1; color:var(--fg-accent);
}
.cr-quest-all{
  margin:12px 0 0;
  font:var(--t-card); letter-spacing:var(--t-card-ls);
  color:var(--fg-accent);
}

.cr-store{ display:block; width:100%; margin:0 0 4px }

.cr-h{
  font:var(--t-section); letter-spacing:var(--t-section-ls);
  color:var(--fg); margin:0 0 12px;
}
.cr-none{ font:var(--t-body); letter-spacing:var(--t-body-ls); color:var(--fg-soft); margin:0 }

/* Seven fixed columns. A grid rather than flex so a day with no bar still
   holds its width and the week never reflows into six. */
.cr-chart{
  display:grid; grid-template-columns:repeat(7, minmax(0,1fr));
  gap:6px; align-items:end;
}
.cr-col{ display:flex; flex-direction:column; align-items:center; gap:4px; min-width:0 }
.cr-col-n{
  font:var(--t-eyebrow); letter-spacing:0; color:var(--fg-soft);
  min-height:1em;
}
.cr-track{
  width:100%; height:96px;
  display:flex; align-items:flex-end;
  background:rgb(var(--ink-rgb) / .06);
  border-radius:var(--r-sm);
}
.cr-bar{
  width:100%; min-height:0;
  background:var(--sand-deep);
  border-radius:var(--r-sm);
}
/* A day that hit the target. The measured emphasis pair, so the bar reads
   as different rather than merely darker. */
.cr-bar.is-full{ background:var(--flame) }
.cr-col.is-today .cr-col-d{ color:var(--fg); font-weight:700 }
.cr-col-d{
  font:var(--t-eyebrow); letter-spacing:0;
  color:var(--fg-soft); text-transform:uppercase;
  white-space:nowrap; overflow:hidden; text-overflow:ellipsis; max-width:100%;
}

.cr-rec{
  display:flex; align-items:baseline; justify-content:space-between;
  gap:10px; padding:8px 0;
  border-top:1px solid rgb(var(--ink-rgb) / .1);
}
.cr-rec:first-of-type{ border-top:0 }
.cr-rec-l{ font:var(--t-body); letter-spacing:var(--t-body-ls); color:var(--fg) }
.cr-rec-v{ display:flex; align-items:baseline; gap:8px; flex:0 0 auto }
.cr-rec-n{ font:var(--t-card); letter-spacing:var(--t-card-ls); color:var(--fg) }
.cr-rec-d{ font:var(--t-eyebrow); letter-spacing:0; color:var(--fg-soft); text-transform:uppercase }
.cr-rec-none{ font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft) }

.cr-way{
  display:flex; align-items:flex-start; gap:12px;
  padding:10px 0; border-top:1px solid rgb(var(--ink-rgb) / .1);
}
.cr-way:first-of-type{ border-top:0 }
.cr-way-pay{
  flex:0 0 4.6em; display:flex; align-items:baseline; gap:4px;
  justify-content:flex-end;
}
.cr-way-mark{ color:var(--fg-accent); font-size:.9rem }
.cr-way-n{ font:var(--t-card); letter-spacing:var(--t-card-ls); color:var(--fg) }
.cr-way-body{ display:flex; flex-direction:column; gap:2px; min-width:0 }
.cr-way-l{ font:var(--t-body); letter-spacing:var(--t-body-ls); color:var(--fg) }
.cr-way-s{ font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft) }

/* The Table of Contents. Text rows, not tiles — Steven: "a separate
   looking page with text and links and specific behaviors", distinct
   from the hub's grid. Opaque and declares its ink, per the rule. */
.toc-list{ display:flex; flex-direction:column; gap:6px; margin-top:14px }
.toc-row{
  display:flex; flex-direction:column; gap:2px;
  text-align:left; width:100%;
  padding:12px 14px;
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  border:1.5px solid var(--sand-deep); border-radius:var(--r-md);
  cursor:pointer;
}
.toc-row:disabled{ cursor:default }
.toc-row-t{ font:var(--t-card); letter-spacing:var(--t-card-ls) }
.toc-row-s{ font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft) }

/* ---- SAY IT YOURSELF, ON A DIALOGUE LINE ----------------------------

   The mic sits under the line it belongs to, with the two comparison
   buttons appearing beside it once there is something to compare.

   OPAQUE, per the rule: these hold text or a glyph and sit on --paper
   with --fg declared, never on a tint that takes its colour from
   whatever is behind it. */
.dg-rec{
  display:flex; align-items:center; flex-wrap:wrap;
  gap:8px; margin:8px 0 2px;
}
.dg-mic{
  min-width:44px; min-height:44px;
  display:inline-flex; align-items:center; justify-content:center;
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  border:1.5px solid var(--sand-deep); border-radius:999px;
  font-size:1.15rem; line-height:1; cursor:pointer;
}
/* Recording. --on-flame on --flame is the measured emphasis pair, 4.65
   worst case across all twelve themes. */
.dg-mic.is-rec{
  background:var(--flame); border-color:var(--flame);
  --fg:var(--on-flame); --fg-soft:var(--on-flame);
  color:var(--fg);
}
.dg-cmp{ padding:8px 14px; font-size:.92rem; min-height:44px }
.dg-mic-note{
  font:var(--t-body); letter-spacing:var(--t-body-ls);
  color:var(--fg-soft);
  margin:10px 0 0;
}
.dg-mine{
  margin:3px 0 0;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
}

/* The line being spoken. */
.dg-row.is-live{
  background:rgb(var(--flame-rgb) / .26);
  border-left-color:var(--flame);
}
/* The line under test. */
.dg-row.is-ask{ border-left-color:var(--flame); border-left-width:4px }

.dg-note{
  margin:12px 0 0;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}

.dg-choices{
  display:flex; flex-wrap:wrap; gap:8px;
  margin:14px 0 0;
}
.dg-choice{
  flex:1 1 auto;
  padding:11px 15px;
  border:1px solid rgb(var(--shade-rgb) / .3);
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .1);
  color:var(--fg); cursor:pointer;
  font:var(--t-btn); letter-spacing:var(--t-btn-ls);
  -webkit-tap-highlight-color:transparent;
}
.dg-choice:hover{ background:rgb(var(--shade-rgb) / .2) }
.dg-choice.is-wrong{ border-color:var(--no); color:var(--fg-soft); opacity:.6 }

.dg-right{ margin:14px 0 0; font:var(--t-card); letter-spacing:var(--t-card-ls) }
.dg-wrong{ margin:12px 0 0; font:var(--t-sub); letter-spacing:var(--t-sub-ls);
           color:var(--fg-soft) }
.dg-next, .dg-again{ margin:12px 0 0 }
.dg-done{ margin:18px 0 0; font:var(--t-card); letter-spacing:var(--t-card-ls) }

/* --- dialogue stages C and D -------------------------------- */
.dg-step{
  margin:14px 0 0;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft); text-transform:uppercase;
}
/* The conversation while a question is being asked: present, but not
   competing with the question for attention. */
.dg-talk-quiet .dg-row{ padding:7px 11px }
.dg-talk-quiet .dg-line{ font:var(--t-sub); letter-spacing:var(--t-sub-ls) }
.dg-talk-quiet .dg-mine{ font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls) }

.dg-q{
  margin:16px 0 0;
  font:var(--t-card); letter-spacing:var(--t-card-ls);
  color:var(--fg);
}
/* The missing turn in stage C. */
.dg-gap{
  margin:0;
  font:var(--t-de); letter-spacing:var(--t-de-ls);
  color:var(--fg-soft);
}
/* Whole replies and whole answers are sentences, so they stack instead of
   sharing a row — four of them side by side is four columns of two words. */
.dg-choices-long{ flex-direction:column; flex-wrap:nowrap }
.dg-choices-long .dg-choice{ flex:0 0 auto; width:100%; text-align:left }
.dg-choice.is-right{ border-color:var(--yes); background:rgb(var(--shade-rgb) / .24) }

/* --- what a listen is worth, and the rest it starts ---------- */
.dg-paid{
  margin:14px 0 0; padding:11px 14px;
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .18);
  border-left:3px solid var(--sand-deep);
}
.dg-paid-n{
  margin:0;
  font:var(--t-card); letter-spacing:var(--t-card-ls);
  color:var(--fg);
}
.dg-paid-day{
  margin:3px 0 0;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
}
/* Not a lockout: the conversation is still open, only the gaps wait. */
.dg-rest{
  margin:16px 0 0; padding:12px 15px;
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .14);
  border-left:3px solid var(--flame);
  font:var(--t-body); letter-spacing:var(--t-body-ls);
  color:var(--fg-soft);
}
.dg-card-rest{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}

/* ==============================================================
   THE TOTAL ON THE END SCREEN
   --------------------------------------------------------------
   Below the achievements, because it counts them. The purse panel
   above shows where the coins came from; this shows how many
   there were altogether, which are two different jobs and were
   being done by one number.
   ============================================================== */
.co-total{
  margin:16px 0 0; padding:14px 16px;
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .2);
  border:1px solid var(--sand-deep);
}
.co-total-line{
  display:flex; align-items:baseline; justify-content:space-between;
  gap:12px;
}
.co-total-l{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft); text-transform:uppercase;
}
.co-total-n{
  font:var(--t-title); letter-spacing:var(--t-title-ls);
  color:var(--fg);
}
.co-total-shop{ width:100%; margin:12px 0 0 }

/* ==============================================================
   THE READER'S TRANSLATION, WHICH COSTS SOMETHING
   --------------------------------------------------------------
   A spent control stays on the screen, dimmed and dead. One that
   disappears reads as a bug; one that is visibly spent reads as
   a rule.
   ============================================================== */
.btn.is-spent{
  opacity:.42;
  cursor:default;
  pointer-events:none;
}
.rd-translate{ white-space:nowrap }

/* The warning sits above the buttons, inside the card, so it is
   read before the finger is already moving. */
.rd-warn{
  margin:16px 0 0; padding:14px 16px;
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .24);
  border:1px solid var(--flame);
}
.rd-warn-t{
  margin:0 0 12px;
  font:var(--t-body); letter-spacing:var(--t-body-ls);
  color:var(--fg);
}
.rd-warn-yes, .rd-warn-no{ width:100% }
.rd-warn-yes{ margin:0 0 8px }

/* ==============================================================
   A SONG'S WORD LIST
   --------------------------------------------------------------
   A picture where one exists, at the left, small. The card is a
   reference entry rather than a test, so the drawing illustrates
   and does not need to fill the row.

   Two columns on a wide screen, one on a phone. 88 words is a
   long scroll either way and the list is meant to be scrolled.
   ============================================================== */
.sw-count{
  margin:14px 0 0;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft); text-transform:uppercase;
}
.sw-list{
  display:grid;
  grid-template-columns:1fr;
  gap:8px;
  margin:10px 0 0;
}
@media (min-width:720px){
  .sw-list{ grid-template-columns:1fr 1fr; gap:8px 14px }
}
.sw-card{
  display:flex; gap:12px; align-items:flex-start;
  padding:11px 13px;
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .14);
  border-left:3px solid var(--sand-deep);
}
/* A button now, not a div, so it can be tapped to enlarge. Every button
   default has to be undone or it arrives with a border, a background and
   padding that push the drawing off centre. */
.sw-pic{
  flex:0 0 auto;
  width:62px;
  border:0; padding:0; margin:0; background:none;
  border-radius:10px;
  overflow:hidden;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
/* Something has to say it is tappable. The drawings are small and a
   pointer cursor is invisible on a phone. */
.sw-pic.sw-lens{
  box-shadow:0 0 0 1px rgb(var(--shade-rgb) / .3);
  transition:box-shadow .12s, transform .12s;
}
.sw-pic.sw-lens:hover{
  box-shadow:0 0 0 2px var(--sand-deep);
  transform:scale(1.04);
}
@media (prefers-reduced-motion: reduce){
  .sw-pic.sw-lens{ transition:none }
  .sw-pic.sw-lens:hover{ transform:none }
}
.sw-body{ flex:1 1 auto; min-width:0 }
.sw-de{
  display:block; width:100%; text-align:left;
  border:0; background:none; padding:0; margin:0;
  color:var(--fg); cursor:pointer;
  font:var(--t-de); letter-spacing:var(--t-de-ls);
  -webkit-tap-highlight-color:transparent;
}
.sw-de:hover{ text-decoration:underline }
.sw-gloss{
  margin:2px 0 0;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg);
}
/* the third language, quieter — Nazar reads the English */
.sw-other{
  margin:1px 0 0;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}
.sw-def{
  margin:5px 0 0;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}
/* A multi-sense word shows every meaning. One of two is half a word. */
.sw-senses{ list-style:none; margin:4px 0 0; padding:0 }
.sw-sense + .sw-sense{
  margin-top:5px; padding-top:5px;
  border-top:1px solid rgb(var(--shade-rgb) / .28);
}
.sw-sense-gloss{
  display:block;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg);
}
.sw-sense-def{
  display:block; margin-top:1px;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}
/* A reference that resolved to nothing is a typo between two files. */
.sw-missing{
  margin:16px 0 0; padding:11px 14px;
  border-radius:var(--r-md);
  border:1px solid var(--no);
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}
.sg-words{ margin-left:8px }

/* A phrase reads differently from a word, so it is set differently: the
   German is the same size but a gap-phrase is marked, because the gap is
   the thing being taught and must not look like a typo. */
.sw-de.is-phrase{ font-style:normal }
.sw-de.is-slot{ font-style:italic }

/* The free pet token. Distinct from a price button, because using one is
   a choice she cannot undo — not a cheaper way to buy. */
.pt-buy.is-token{
  background:var(--sand);
  color:var(--liver);
  border-color:var(--sand);
  font-weight:700;
}
.pt-tokens{
  margin:12px 0 0;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-accent, var(--fg));
}

/* ==============================================================
   WHAT THESE GAMES ARE
   --------------------------------------------------------------
   A page of cards, one per activity, with the first rule as the
   summary and the rest on tap. The Play button is on the card,
   because reading about a game and starting it should not be two
   screens.
   ============================================================== */
/* The three section guides — `gd-open` under Games, `rg-open` under
   Reference, `rl-open` under Read and listen — moved into `.hub-head`
   on 10 Sep to match the Lesson Guide: title left, guide link right.
   The flex header does the placement, so the old full-width rule is
   gone; only the classes remain, kept because the Full Tour points at
   `.gd-open`. Long labels may wrap under the title on narrow phones,
   which is what the Lesson Guide's own label already does. */
.gd-open, .rg-open, .rl-open{ flex-shrink:1; text-align:right }
.gd-sec{
  margin:20px 0 0;
  font:var(--t-section); letter-spacing:var(--t-section-ls);
  color:var(--fg);
}
.gd-list{ display:flex; flex-direction:column; gap:10px; margin:10px 0 0 }
.gd-card{
  padding:13px 15px;
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .14);
  border-left:3px solid var(--sand-deep);
}
.gd-head{
  display:flex; gap:12px; align-items:flex-start;
  width:100%; text-align:left;
  border:0; background:none; padding:0; margin:0;
  color:var(--fg); cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.gd-glyph{ flex:0 0 auto; font-size:1.5rem; line-height:1.2 }
.gd-head-body{ flex:1 1 auto; min-width:0; display:flex; flex-direction:column; gap:2px }
.gd-name{ font:var(--t-card); letter-spacing:var(--t-card-ls) }
.gd-what{
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
}
/* the expand mark, quiet — the card is readable without opening it */
.gd-more{
  flex:0 0 auto;
  width:1.6em; height:1.6em; line-height:1.6em; text-align:center;
  border-radius:999px;
  background:rgb(var(--shade-rgb) / .3);
  color:var(--fg-soft);
  font:var(--t-eyebrow); letter-spacing:0;
}
.gd-rules{
  margin:10px 0 0; padding-left:1.4em;
  font:var(--t-body); letter-spacing:var(--t-body-ls);
  color:var(--fg-soft);
}
.gd-rule{ margin:4px 0 0 }

/* The prose a LESSON expands into, where a game expands into its rules.
   Same roles and the same contextual ink as .gd-rules above, so it reads
   as the other half of one mechanism rather than as a second style.
   Declares nothing of its own: it sits inside .gd-card, which is opaque
   and sets --fg, and reading --fg here is correct because the surface
   above it declares one. */
.gd-detail{ margin:10px 0 0 }
.gd-detail-h{
  font:var(--t-card); letter-spacing:var(--t-card-ls);
  color:var(--fg);
  margin:0 0 4px;
}
.gd-detail-p{
  font:var(--t-body); letter-spacing:var(--t-body-ls);
  color:var(--fg-soft);
  margin:0;
}
.gd-go{ width:100%; margin:12px 0 0 }

/* ==============================================================
   TANYA LESSONS
   --------------------------------------------------------------
   Five stages against one story, each taking more of it away. A
   shut stage stays on screen, dimmed — a control that vanishes
   reads as a bug, one that is visibly shut reads as a rule.
   ============================================================== */
.tl-list{ display:flex; flex-direction:column; gap:8px; margin:14px 0 0 }
.tl-card{
  display:flex; flex-direction:column; gap:3px;
  width:100%; text-align:left; padding:13px 15px;
  border:0; border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .14);
  border-left:3px solid var(--sand-deep);
  color:var(--fg); cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.tl-card.is-done{ border-left-color:var(--yes) }
.tl-card-name{ font:var(--t-card); letter-spacing:var(--t-card-ls) }
.tl-card-meta,.tl-card-wait{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}
.tl-card-wait{ color:var(--flame) }
.tl-words{
  margin:12px 0 0;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft); text-transform:uppercase;
}

.tl-stages{ display:flex; flex-direction:column; gap:8px; margin:14px 0 0 }
.tl-stage{
  display:flex; gap:12px; align-items:center;
  width:100%; text-align:left; padding:12px 14px;
  border:0; border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .14);
  border-left:3px solid var(--sand-deep);
  color:var(--fg); cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.tl-stage.is-done{ border-left-color:var(--yes) }
.tl-stage.is-shut{ opacity:.42; cursor:default }
.tl-stage-n{
  flex:0 0 auto; width:1.8em; height:1.8em; line-height:1.8em;
  text-align:center; border-radius:999px;
  background:rgb(var(--shade-rgb) / .3); color:var(--fg-soft);
  font:var(--t-eyebrow); letter-spacing:0;
}
.tl-stage-body{ flex:1 1 auto; min-width:0; display:flex; flex-direction:column }
.tl-stage-name{ font:var(--t-card); letter-spacing:var(--t-card-ls) }
.tl-stage-sub{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}
.tl-stage-tick{ flex:0 0 auto; color:var(--yes) }

/* the story, and the one stage that shows a translation */
.tl-body{ display:flex; flex-direction:column; gap:9px; margin:16px 0 0 }
.tl-row{
  display:flex; gap:10px; align-items:flex-start;
  padding:9px 12px; border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .12);
  border-left:3px solid transparent;
}
.tl-row.is-ask{ border-left-color:var(--flame) }
.tl-n{
  flex:0 0 auto; min-width:1.4em; text-align:right;
  font:var(--t-eyebrow); letter-spacing:0; color:var(--fg-soft);
}
.tl-line-body{ flex:1 1 auto; min-width:0 }
.tl-de{
  display:block; width:100%; text-align:left;
  border:0; background:none; padding:0; margin:0;
  color:var(--fg); cursor:pointer;
  font:var(--t-de); letter-spacing:var(--t-de-ls);
  -webkit-tap-highlight-color:transparent;
}
.tl-de:hover{ text-decoration:underline }
.tl-tr{
  margin:3px 0 0;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft);
}
.tl-line-en{ margin:5px 0 0; display:flex; gap:10px;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft) }

.tl-step{
  margin:14px 0 0; font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft); text-transform:uppercase;
}
.tl-q{ margin:16px 0 0; font:var(--t-card); letter-spacing:var(--t-card-ls) }
.tl-heard{
  margin:14px 0 0; font:var(--t-de); letter-spacing:var(--t-de-ls);
  color:var(--fg);
}
.tl-choices{ display:flex; flex-wrap:wrap; gap:8px; margin:14px 0 0 }
.tl-choices-long{ flex-direction:column; flex-wrap:nowrap }
.tl-choices-long .tl-choice{ width:100%; text-align:left }
.tl-choice{
  flex:1 1 auto; padding:11px 15px;
  border:1px solid rgb(var(--shade-rgb) / .3);
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .1);
  color:var(--fg); cursor:pointer;
  font:var(--t-btn); letter-spacing:var(--t-btn-ls);
  -webkit-tap-highlight-color:transparent;
}
.tl-choice:hover{ background:rgb(var(--shade-rgb) / .2) }
.tl-choice.is-wrong{ border-color:var(--no); color:var(--fg-soft); opacity:.6 }
.tl-choice.is-right{ border-color:var(--yes); background:rgb(var(--shade-rgb) / .24) }

.tl-order-built{ margin:14px 0 0; padding-left:1.5em }
.tl-order-item{ margin:5px 0 0 }
.tl-order-btn{
  width:100%; text-align:left; padding:9px 12px;
  border:1px solid var(--sand-deep); border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .18);
  color:var(--fg); cursor:pointer;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
}
.tl-right{ margin:14px 0 0; font:var(--t-card); letter-spacing:var(--t-card-ls) }
.tl-wrong{ margin:12px 0 0; font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft) }
.tl-next,.tl-play{ width:100%; margin:12px 0 0 }
.tl-note{ margin:12px 0 0; font:var(--t-eyebrow);
  letter-spacing:var(--t-eyebrow-ls); color:var(--fg-soft) }
.tl-finish{ margin:16px 0 0; padding:18px }
.tl-finish-t{ margin:0; font:var(--t-section); letter-spacing:var(--t-section-ls) }
.tl-finish-all{ margin:8px 0 0; font:var(--t-card); letter-spacing:var(--t-card-ls) }
.tl-complete{ margin:16px 0 0; font:var(--t-card); letter-spacing:var(--t-card-ls) }
.tl-token{
  margin:10px 0 0; padding:11px 14px;
  border-radius:var(--r-md); border:1px solid var(--sand-deep);
  background:rgb(var(--shade-rgb) / .2);
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
}
/* a lesson still waiting on its German */
.tl-wait{
  margin:16px 0 0; padding:14px 16px;
  border-radius:var(--r-md); border:1px solid var(--flame);
  background:rgb(var(--shade-rgb) / .14);
}
.tl-wait-t{ margin:0 0 8px; font:var(--t-sub); letter-spacing:var(--t-sub-ls) }
.tl-wait-list{ margin:0; padding-left:1.4em;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls); color:var(--fg-soft) }
.tl-wait-item{ margin:3px 0 0 }

/* why a stage is shut, and where the lesson game builds itself */
.tl-stage-why{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--flame);
}
.tl-game{ margin:14px 0 0 }

/* ==============================================================
   LIGHT PANELS THAT NEVER DECLARED THEIR INK
   --------------------------------------------------------------
   A rule that paints itself a light background is a light
   surface, and has to say so — otherwise, nested inside a .card,
   it keeps the dark surface's --fg, --fg-soft and --fg-accent and
   prints pale ink and a lime accent on cream.

   That is exactly what happened to the lesson examples: `.ls-eg`
   set background:var(--paper) and inherited --fg-accent:
   var(--flame-pale) from the .card around it, so the highlighted
   German word came out lime green on cream, and the rest of
   the line came out barely there.

   Found by auditing every rule that paints one of --paper,
   --sand-wash, --sand or #fff without setting a colour or an ink:
   thirty-four of them. Restated here in one place rather than
   thirty-four edits, and harmless where the parent was light
   already — the values are identical.

   The audit, worth re-running after any colour work:

     rules painting a light background, setting no colour, and
     declaring no --fg  ->  every one of them belongs in this list
   ============================================================== */
.cw-cell.cw-up, .cw-review-item,
.cg-table, .cg-both,
.wf-cell.wf-up,
.howto-box, .es-item,
.gr-stat, .gr-pair, .gr-pair:hover, .gr-sent:hover,
.gr-past-row, .gr-past-row:hover, .gr-mark, .gr-mark:hover, .gr-fut-now,
.sc-answer,
.co-panel,
.cv-card, .cv-bin,
.pt-card, .pt-rare, .pt-epic, .pt-legendary,
.pt-buy:hover:not(:disabled), .pt-pick:hover,
.ls-eg, .ls-sentence, .ls-bin:hover, .ls-opt:hover,
.pl-card,
.cm-card,
/* ---- added 01 Sep 2026, after the Wo-oder-wohin grammar page ----

   `.ww-pair` sets `background:var(--sand-wash)` and declared no ink, so on
   the grammar page — where it sits inside a dark `.card` — every German
   line was --on-dark on a pale wash and effectively invisible. In the GAME
   the same element sits on the page and looked fine, which is why it
   survived.

   Its own siblings were all already in this list: .gr-stat, .gr-pair,
   .gr-past-row, .gr-mark, .gr-fut-now, .sc-answer, .cg-table, .cg-both.
   It was an omission from a known list rather than a new problem.

   So the list was swept properly. THE RULE IS ABSOLUTE: an OPAQUE light
   background — var(--paper), var(--sand), var(--sand-wash) — always needs
   dark ink, whatever it is nested inside. There is no surface in any theme
   where those three carry pale text. Every rule in the stylesheet setting
   one of them and declaring no ink of its own is now here.

   NOT swept, deliberately: the `rgb(var(--shade-rgb) / .12)` tints. Those
   are semi-transparent by design and take the colour of whatever is behind
   them — over a dark card they are still dark, and forcing dark ink onto
   them would break them the other way round. They are correct as they are. */
.ww-pair,
.sc-slot,
.cw-cell.cw-shot,
.es-review,
.pt-common, .pt-buy, .pt-pick, .pt-buy.is-token, .pt-say,
.pt-buy:hover:not(:disabled), .pt-pick:hover,
.pl-ask,
.gw-guess, .gw-cat, .gw-val, .gw-size,
.who-input, .st-input, .ref-search-in,
.rd-pos,
.bt-btn, .purse, .petstrip-btn,
.wm-stage,
.done, .backlink, .tile,
.speak.is-speaking,
.dg-mode.is-on,
.cm-btn[aria-pressed="true"],
.mode-toggle button[aria-pressed="true"],
.typeswitch button[aria-pressed="true"],
.backswitch button[aria-pressed="true"],
/* `.sp-noart` is the tile sprite.js builds for a word with NO drawing —
   the German set large on a pale dashed box — and `.sp-word` paints it
   with --fg. NAMED WRONG UNTIL NOW: this note said `.sprite-word` and
   `.sprite-word-text`, neither of which sprite.js has ever emitted, so
   the entry it added to the list below matched nothing and the 133
   pictureless words were never actually swept. `.sp-noart` is NOT in the
   list, deliberately — it is transparent by design and takes the colour
   of the card behind it, so declaring `--fg:var(--ink)` on it would print
   dark ink on a dark card, which is the bug this sweep exists to stop. 133 of the 743 words have no picture, so inside a card that
   is roughly one word in six rendered pale-on-pale. Worse than the grammar
   page that started this, and it will grow as the vocabulary outpaces the
   art. The rest are picture frames with no text of their own; listed so the
   next sweep finds nothing left to argue about. */
.pt-say::before, .pt-strip-face, .petstrip-mini, .ptg-img, .fp,
/* ---- added again, 01 Sep 2026, after the Jukebox came up blank ----

   MY FIRST SWEEP HAD A FLAW AND THESE SIX ESCAPED IT. It skipped any rule
   whose body mentioned `--fg`, on the assumption that mentioning it meant
   declaring it. `.jb-song` mentions it as a CONSUMER — `color:var(--fg)` —
   on an opaque `var(--paper)` background. Using the variable is not
   declaring it, so inside a dark `.card` the text was --on-dark on white
   and the song list rendered blank until a row was selected, which is
   exactly what it looked like.

   The test is now: an opaque light background must DECLARE `--fg:`, not
   merely read it.

   Four of the six were mine from this session — the two Jukebox rules and
   the Word Matching quiz cell, which would have made that whole board
   invisible too. `.pt-strip-say` and `.ptg-cell` were already there. */
.jb-btn, .jb-song,
.wm-cell,
.pt-strip-say, /* Said only when a tap is refused because every carrier is occupied —
   see the note in petstrip.js. Empty otherwise, so it takes no space. */
/* What a tap in the grid does, said once at the top. Without it the grid
   is sixteen pictures with no stated affordance — see the note in
   petstrip.js. */
.ptg-how{
  margin:0 2px 12px;
  text-align:center;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
}

/* To the Store, at the foot of the grid. `go()` existed from the start
   and nothing called it, so there was no route from here to the place
   that sells the locked half of what this screen displays.

   A real button, wide enough to be an obvious target on a phone rather
   than a line of text under a wall of pictures. Capped so it does not
   stretch the full width of a tablet. */
.ptg-shop{
  display:block;
  width:min(280px, 100%);
  margin:16px auto 4px;
}

.ptg-note{
  margin:12px 2px 0;
  min-height:1.2em;
  text-align:center;
  font:700 .86rem/1.3 var(--ff-ui);
  color:var(--flame);
}

/* The refused cell, for a moment. A refusal that looks identical to a
   no-op is the dead-grid bug this whole change replaces, so the tap has
   to land visibly even when nothing is saved. */
.ptg-cell.is-full{
  border-color:var(--flame);
  box-shadow:0 0 0 2px rgb(var(--flame-rgb) / .45);
}
@media (prefers-reduced-motion: no-preference){
  .ptg-cell.is-full{ animation:ptg-nudge .3s ease }
}
@keyframes ptg-nudge{
  0%,100%{ transform:translateX(0) }
  30%    { transform:translateX(-3px) }
  70%    { transform:translateX(3px) }
}

.ptg-cell{
  --fg:       var(--ink);
  --fg-soft:  var(--ink-soft);
  --fg-accent:var(--ink);
}

/* The highlighted word in a lesson example.

   With the ink fixed, --fg-accent on a light surface is --ink —
   the same colour as the rest of the line, so the emphasis
   disappeared along with the bad contrast. Bold alone is thin
   for the one word the whole step is about.

   So it gets a highlighter: dark ink, bold, on a soft flame
   wash. Same idea as the marker pen in Tanya's own notes, and it
   reads at a glance without relying on colour to carry meaning. */
.ls-eg-de b{
  color:var(--ink);
  font-weight:700;
  background:rgb(var(--flame-rgb) / .18);
  border-radius:5px;
  padding:0 4px;
  margin:0 -1px;
  box-decoration-break:clone;
  -webkit-box-decoration-break:clone;
}

/* ==============================================================
   HER PETS, AT THE TOP OF THE HUB
   --------------------------------------------------------------
   Small faces above the greeting, and a grid behind them. The
   pet was only ever on the end screen, so a bought pet was
   absent from the screen she actually opens.

   The strip is a light panel, so it declares its ink — the
   thirty-four-rule lesson from v200.
   ============================================================== */
.pt-strip{
  display:flex; flex-direction:column; gap:8px;
  margin:0 0 12px;
  --fg:       var(--ink);
  --fg-soft:  var(--ink-soft);
  --fg-accent:var(--ink);
}
/* What the pet says. A button, because the German is spoken on tap. */
/* The pet speaking, on the hub. A row now: portrait, then words.

   The 26px face in the header is a control. A card holding a sentence of
   speech needs a portrait, or it reads as a caption with a bullet point
   rather than an animal talking. */
/* NOT A FLEX ROW. Steven, 10 Sep: "text is squished into a narrow
   column — the dragon should be an image wrap so text can flow below
   her."

   Flex put the picture and the words in two fixed columns, so on a phone
   the words got whatever was left of 380px minus a 64px face and a gap:
   a ravine of two or three words per line.

   A FLOAT instead. The face sits in the top-left corner and the text runs
   beside it for three lines, then reclaims the full width underneath —
   which is what "wrap" means and what flex cannot do. */
.pt-strip-say{
  display:block;
  width:100%; text-align:left;
  padding:12px 14px;
  border:0; border-radius:var(--r-md);
  background:var(--sand-wash);
  border-left:3px solid var(--sand-deep);
  color:var(--fg); cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
/* 64px, and it does not shrink: flex would happily squash it to nothing
   next to a long German sentence. */
.pt-strip-face{
  float:left;
  margin:0 12px 6px 0;
  width:64px; height:64px;
  border-radius:16px;
  object-fit:cover;
  background:var(--paper);
  box-shadow:0 0 0 1.5px var(--sand-deep);
}
/* `display:inline` so the lines flow around the floated face rather
   than forming a block beside it. */
.pt-strip-words{
  display:block; overflow:visible;

  flex:1 1 auto; min-width:0;
  display:flex; flex-direction:column; gap:2px;
}
.pt-strip-who{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft); text-transform:uppercase;
}
.pt-strip-de{ font:var(--t-de); letter-spacing:var(--t-de-ls) }
/* What is due, said by the pet rather than by a panel beside it. Sits
   under the translation, quieter than the German but readable — it is
   information, not an aside. */
/* The buttons, inside the pet's card and below what it says. Wrapping,
   so two on a narrow phone stack instead of squeezing. Outside the
   speaking element — see the note in app.js. */
/* The buttons sit under everything, clear of the floated face — without
   `clear:both` a short greeting would leave them beside her. */
.pt-strip-acts{
  clear:both;
  display:flex; flex-wrap:wrap; gap:8px;
  margin:12px 0 0;
}
/* THE TWO SUGGESTIONS, AS BUTTONS THAT STAND OFF THE CARD.

   Steven, 09 Sep: "have 'start spot the wrong form' in a rounded
   rectangle and maybe a slightly different color from the pet window so
   it stands out."

   The card is `--sand-wash`, so a button sharing that background
   disappears into it. Rounded rectangle rather than the pill the rest of
   the app uses — it reads as a panel inside a panel, which is what it is.

   Crystals stays flame because it is the paying action and the one that
   expires tonight. The activity button takes `--paper`, a step off the
   card without competing with it. */
.pt-strip-go{
  flex:1 1 auto;
  min-width:44%;
  border-radius:var(--r-md);
  padding:11px 14px;
  font-weight:700;
}
.pt-strip-go.btn-ghost{
  background:var(--paper);
  color:var(--fg);
  border:1.5px solid rgb(var(--sand-rgb) / .55);
}
.pt-strip-go.btn-ghost:hover{
  border-color:var(--sand-deep);
}

/* THE DAILY PET TASK BUTTON GETS ITS OWN COLOUR. Steven, 10 Sep: it must
   not be the card colour it sits on, and must not match Crystal Quests
   (which is the accent, `btn-primary`). `--mauve` is the theme's secondary
   — a real, distinct hue in every one of the twelve palettes, never the
   accent and never the card. Dark text (`--liver-deep`, the deepest
   neutral per theme) stays readable across all of them. Overrides the
   ghost fill/border above, so this wins wherever it applies. */
.pt-strip-go.pt-strip-task.btn-ghost{
  background:var(--mauve);
  color:var(--liver-deep);
  border:1.5px solid var(--mauve);
}
.pt-strip-go.pt-strip-task.btn-ghost:hover{
  border-color:var(--liver-deep);
}

/* What the pet has to tell her, one line each. Only the FIRST carries
   the rule above it — three lines with three dividers would read as three
   separate cards inside one card. */
.pt-strip-due{
  display:block;
  margin-top:4px;
  font:600 .88rem/1.4 var(--ff-ui);
  color:var(--fg-accent);
}
.pt-strip-due:first-of-type{
  margin-top:10px;
  padding-top:10px;
  border-top:1px solid rgb(var(--sand-rgb) / .28);
}

.pt-strip-tr{
  font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft);
}

/* Sent here from the grid: marked as well as scrolled to, because a
   scroll alone leaves her guessing which one she asked for. */
.pt-card.is-focus{
  outline:3px solid var(--flame);
  outline-offset:2px;
}

/* ==============================================================
   THE PET STRIP IN THE HEADER
   --------------------------------------------------------------
   Up to three faces beside the theme and language pickers, so the
   pet is present while she works rather than only on the screen
   where she picks what to do.

   26px: big enough to tell which animal, small enough that it
   does not become the header.
   ============================================================== */
.petstrip{
  display:flex; align-items:center;
  --fg:       var(--ink);
  --fg-soft:  var(--ink-soft);
  --fg-accent:var(--ink);
}
/* THE THEME CONTROL IS A 40px BOX AROUND A 22px DOT, so it read as the
   smallest thing in the row even though its tap target already matched.
   Giving it the same border and wash as the others makes the four boxes
   the same size to the eye, not just to the layout. */
.themeswitch{
  min-height:40px;
  border-radius:999px;
}

/* ONE HEIGHT ACROSS THE TOOLBAR ROW.

   Steven, 09 Sep: "the pet pill is misaligned from the color circle and
   the crystal — make all of them same vertical size."

   Measured, they were three different heights: the purse about 30px
   (6px padding, 1.5px border, ~15px of text), the pet pill 36px (4px
   padding, 1px border, a 26px face), and the theme swatch an explicit
   40px. Nothing lined up because nothing agreed on a height.

   40px is the right common value: the swatch already uses it, and it is
   the usual minimum tap target. `min-height` rather than `height`, so a
   control whose contents ever grow pushes past it instead of clipping. */
.petstrip-btn{
  /* CENTRED, NOT TOP-ALIGNED. Steven, 09 Sep: "the 3 pets should be
     vertically centered in their pill, they are at the top."

     `align-items:center` was already here and was not enough: the row
     rule pins the button to 40px, and with `padding:4px 6px` the content
     box is 32px inside it — so a 26px face centres within THAT rather
     than within the pill. `justify-content` and a zero vertical padding
     put the faces in the middle of the 40, which is what lines them up
     with the crystal and the theme dot beside them. */
  /* `gap:-6px` was here and is INVALID — negative gap is not allowed, so
     the browser dropped the whole declaration. The overlap it was meant
     to create actually comes from `.petstrip-mini + .petstrip-mini`'s
     negative margin further down, which does work. Removed rather than
     left as a line that reads like it does something. */
  /* MEASURED, 10 Sep. Steven's console: pill 40px, face 26px, gap above
     1.01px, gap below 13px — the face pinned to the top of the content
     box while `align-items` computed to `center`.

     That is the signature of a WRAPPING flex container. With
     `flex-wrap:wrap` the item is centred within its LINE, the line is
     only as tall as the item, and `align-content` (default `flex-start`)
     then parks that line at the top. Centring inside a 26px line inside a
     38px box moves nothing.

     `nowrap` because three faces on one row is the whole design, and
     `align-content:center` so a line — however it is formed — sits in the
     middle. Both, because either alone leaves the other failure open. */
  display:flex; align-items:center; justify-content:center;
  flex-wrap:nowrap;
  align-content:center;
  padding:0 6px;
  border:1px solid rgb(var(--sand-rgb) / .45);
  border-radius:999px;
  background:var(--sand-wash);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
/* Overlapped, so three faces take the space of about two. A header has
   no room to spare and the pile reads as "a group" rather than a list. */
/* CENTRED ON THE FACES THEMSELVES, not left to the parent.

   Steven, 10 Sep: "the pets need to be centered, they are at the top of
   the container." The pill already said `align-items:center`, which
   should have been enough — so stating it on the item as well is belt and
   braces against anything that sets `align-self` further up, and against
   `.pet-img`'s `height:auto`, which applies to these same images and
   would let one grow past 26px if the cascade ever reordered.

   `display:block` because an <img> is inline by default and an inline
   image sits on the text baseline, which leaves a few pixels of descender
   space under it — enough to read as "not quite centred" even when the
   box is. */
.petstrip-mini{
  width:26px; height:26px;
  min-height:26px; max-height:26px;
  align-self:center;
  display:block;
  border-radius:999px;
  object-fit:cover;
  background:var(--paper);
  box-shadow:0 0 0 1.5px var(--sand-deep);
}
.petstrip-mini + .petstrip-mini{ margin-left:-9px }
.petstrip-btn:hover .petstrip-mini{ box-shadow:0 0 0 1.5px var(--flame) }

/* --- the grid, as an overlay -------------------------------------
   An overlay and not a screen, because the faces are reachable from
   anywhere and opening the grid must not throw away a round. Same
   behaviour as the lightbox: over the page, backdrop and Escape close it,
   nothing behind it is disturbed. */
.ptg-overlay{
  position:fixed; inset:0; z-index:60;
  display:none; align-items:flex-start; justify-content:center;
  padding:16px;
  background:rgb(var(--scrim-rgb) / .62);
  overflow-y:auto;
}
.ptg-overlay.is-open{ display:flex }
.ptg-box{
  width:100%; max-width:560px; max-height:86vh; overflow:auto;
  padding:16px;
  border-radius:20px;
  background:var(--paper);
  --fg:       var(--ink);
  --fg-soft:  var(--ink-soft);
  --fg-accent:var(--ink);
  color:var(--fg);
  /* centres it when it fits, top-aligns it when it does not */
  margin:auto;
}
.ptg-head{ display:flex; align-items:center; justify-content:space-between; gap:12px }
.ptg-title{ margin:0; font:var(--t-section); letter-spacing:var(--t-section-ls) }
.ptg-close{
  border:0; background:rgb(var(--shade-rgb) / .18);
  width:2em; height:2em; line-height:1; border-radius:999px;
  font-size:1.2rem; color:var(--fg); cursor:pointer;
}
.ptg-grid{
  display:grid;
  grid-template-columns:repeat(auto-fill, minmax(min(84px,100%), 1fr));
  gap:9px;
  margin:14px 0 0;
}
.ptg-cell{
  display:flex; flex-direction:column; align-items:center; gap:2px;
  padding:7px 5px;
  border:1px solid rgb(var(--sand-rgb) / .4);
  border-radius:var(--r-md);
  background:var(--sand-wash);
  color:var(--fg); cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.ptg-img{
  width:100%; height:auto; aspect-ratio:1/1;
  object-fit:cover; border-radius:9px;
  background:var(--paper);
}
.ptg-name{
  font:var(--t-eyebrow); letter-spacing:0; color:var(--fg);
  max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
.ptg-de{
  font:var(--t-eyebrow); letter-spacing:0; color:var(--fg-soft);
  max-width:100%; overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* Not hers yet. Greyed and still legible — a row of blanks is nothing to
   want, and the shelf is meant to be a reason to come back. */
/* So the tick above can sit in the corner. */
.ptg-cell{ position:relative }
/* The requirement, on the cell. Small and wrapped — it has to fit under a
   96px picture without pushing the grid apart. See the note in
   petstrip.js. */
.ptg-need{
  display:block;
  margin-top:3px;
  padding:0 2px;
  font:600 .62rem/1.25 var(--ff-ui);
  color:var(--fg-soft);
  text-align:center;
}
.ptg-cell.is-locked .ptg-img{ filter:grayscale(1); opacity:.42 }
.ptg-cell.is-locked .ptg-name{ opacity:.65 }
.ptg-cell.is-own{ border-color:rgb(var(--yes-rgb) / .5) }
/* WHICH PETS ARE ALREADY OUT.

   This WAS a 2px flame border and nothing else — invisible beside the
   greyscale locked cells, so the grid could say a carrier was full but
   not which three were in it. That makes "put one away first" an
   instruction she cannot act on: she does not know what to tap. Steven,
   09 Sep.

   A filled ring, a wash, and a tick in the corner. The tick matters more
   than the colour: the locked cells are already greyscaled, so a
   colour-only signal is the one thing that cannot be relied on here. */
.ptg-cell.is-picked{
  border-color:var(--flame);
  border-width:2px;
  box-shadow:0 0 0 2px rgb(var(--flame-rgb) / .40);
  background:rgb(var(--flame-rgb) / .10);
}
.ptg-cell.is-picked::after{
  content:'\2713';
  position:absolute; top:6px; right:6px;
  width:20px; height:20px;
  display:grid; place-items:center;
  border-radius:999px;
  background:var(--flame); color:var(--on-flame);
  font:700 .74rem/1 var(--ff-ui);
}

/* The word a vocab round just confirmed, and what it means.

   A right answer used to say only "Correct", so the moment she was
   certain she knew the word was the moment the app would not tell her
   what it meant — while a WRONG answer named it. Being right is exactly
   when the pairing is worth confirming.

   The hold is about 1.6 seconds, so this has to be readable at a glance:
   the German large, the gloss under it, nothing to dismiss. */
.vg-face{
  display:flex; flex-direction:column; align-items:center; gap:2px;
  text-align:center;
}
.vg-face-de{
  font:var(--t-de); letter-spacing:var(--t-de-ls);
  color:var(--fg);
}
.vg-face-gloss{ display:flex; flex-direction:column; gap:1px }
.vg-face-g{
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
}
/* A dictionary sense carries a definition, which is worth more than the
   gloss — quieter and smaller, because it is the second thing read. */
.vg-face-d{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft); opacity:.85;
}
/* On the miss panel it sits under the German that panel already shows. */
.vm-note .vg-face-gloss{ align-items:flex-start; margin:2px 0 0 }

/* ==============================================================
   WORD SEARCH
   --------------------------------------------------------------
   Both directions. German in for "what does this mean", Russian in
   for "how do I say this" — and the second is the harder, more
   useful case, the one a paper dictionary exists for.
   ============================================================== */
.ref-search{
  position:relative;
  margin:0 0 10px;
  --fg:       var(--ink);
  --fg-soft:  var(--ink-soft);
  --fg-accent:var(--ink);
}
.ref-search-in{
  width:100%;
  padding:12px 40px 12px 14px;
  border:1.5px solid rgb(var(--sand-rgb) / .5);
  border-radius:var(--r-md);
  background:var(--paper);
  color:var(--ink);
  font:var(--t-body); letter-spacing:var(--t-body-ls);
  -webkit-appearance:none;
  appearance:none;
}
.ref-search-in::placeholder{ color:var(--ink-soft); opacity:.8 }
.ref-search-in:focus{
  outline:none;
  border-color:var(--flame);
  box-shadow:0 0 0 3px rgb(var(--flame-rgb) / .18);
}
/* Safari draws its own clear button on type=search and it sits on top of
   ours. Hidden, because ours also resets the group state. */
.ref-search-in::-webkit-search-cancel-button{ -webkit-appearance:none; display:none }
.ref-search-x{
  position:absolute; top:50%; right:8px;
  transform:translateY(-50%);
  width:26px; height:26px; line-height:1;
  border:0; border-radius:999px;
  background:rgb(var(--shade-rgb) / .22);
  color:var(--ink);
  font-size:1.05rem; cursor:pointer;
}
/* Nothing found. Said plainly, with the tap hint suppressed — advice about
   enlarging pictures under an empty list is the app talking to itself. */
.ref-none{
  margin:14px 0 0; padding:16px;
  border-radius:var(--r-md);
  background:rgb(var(--shade-rgb) / .12);
  text-align:center;
}
.ref-none-t{ margin:0 0 4px; font:var(--t-card); letter-spacing:var(--t-card-ls) }

/* ==============================================================
   WER IST ES? — GUESS WHO
   --------------------------------------------------------------
   The app answers; she flips the faces. So the board has to make
   "still standing" and "ruled out" unmistakable at a glance, and
   flipping has to be obviously reversible.
   ============================================================== */
.gw-levels{ display:flex; flex-direction:column; gap:9px; margin:16px 0 0 }
.gw-level{
  display:flex; flex-direction:column; gap:2px;
  width:100%; text-align:left; padding:14px 16px;
  border:0; border-radius:var(--r-md);
  background:var(--sand-wash);
  border-left:3px solid var(--sand-deep);
  cursor:pointer;
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  -webkit-tap-highlight-color:transparent;
}
.gw-level-name{ font:var(--t-card); letter-spacing:var(--t-card-ls) }
.gw-level-sub{ font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft) }

/* The answer, large, above the board — it is what she asked for. */
.gw-answer{
  display:flex; flex-direction:column; gap:4px;
  margin:14px 0 0; padding:13px 15px;
  border-radius:var(--r-md);
  background:var(--sand-wash);
  border-left:4px solid var(--sand-deep);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
}
.gw-answer.is-yes{ border-left-color:var(--yes) }
.gw-answer.is-no{ border-left-color:var(--no) }
.gw-answer-q{
  border:0; background:none; padding:0; text-align:left;
  color:var(--fg); cursor:pointer;
  font:var(--t-de); letter-spacing:var(--t-de-ls);
}
.gw-answer-a{ font:var(--t-section); letter-spacing:var(--t-section-ls) }
.gw-answer-dead{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls); color:var(--flame);
}

/* The board. Four across on a phone at any size — three would make the
   32-face board eleven rows deep. */
.gw-board{
  display:grid;
  grid-template-columns:repeat(4, minmax(0, 1fr));
  gap:7px;
  margin:14px 0 0;
}
@media (min-width:620px){
  .gw-board{ grid-template-columns:repeat(6, minmax(0, 1fr)) }
}
.gw-cell{
  display:flex; flex-direction:column; gap:2px;
  padding:4px;
  border-radius:10px;
  background:rgb(var(--shade-rgb) / .14);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
}
.gw-pic{
  border:0; padding:0; background:none;
  border-radius:8px; overflow:hidden; cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.gw-cell-name{
  font:var(--t-eyebrow); letter-spacing:0; color:var(--fg);
  text-align:center;
  overflow:hidden; text-overflow:ellipsis; white-space:nowrap;
}
/* Ruled out. Greyed and tipped, not hidden: she has to be able to see what
   she eliminated in order to notice a mistake, and a blank square could
   equally be a picture that failed to load. */
.gw-cell.is-down{ background:rgb(var(--shade-rgb) / .06) }
.gw-cell.is-down .fp{
  filter:grayscale(1) brightness(.6);
  transform:rotate(-4deg) scale(.92);
}
.gw-cell.is-down .gw-cell-name{ opacity:.4; text-decoration:line-through }
.gw-cell .fp{ transition:filter .15s, transform .15s }
@media (prefers-reduced-motion: reduce){ .gw-cell .fp{ transition:none } }

.gw-guess{
  border:1px solid rgb(var(--sand-rgb) / .45);
  border-radius:7px;
  background:var(--paper);
  color:var(--ink);
  padding:3px 2px;
  font:var(--t-eyebrow); letter-spacing:0;
  cursor:pointer;
}
.gw-guess:hover{ border-color:var(--flame) }

/* The palette: a category, then its values. */
.gw-ask{ margin:16px 0 0 }
.gw-cat, .gw-val{
  padding:11px 14px; margin:0 6px 6px 0;
  border:1.5px solid rgb(var(--sand-rgb) / .4);
  border-radius:var(--r-md);
  background:var(--sand-wash);
  color:var(--ink);
  font:var(--t-btn); letter-spacing:var(--t-btn-ls);
  cursor:pointer;
}
.gw-cat.is-spent, .gw-val.is-spent{ opacity:.4 }
.gw-vals{ display:flex; flex-wrap:wrap; margin:8px 0 0 }
.gw-ask-back{
  border:0; background:none; padding:0; margin:0 0 4px;
  color:var(--fg-soft); cursor:pointer;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
}
.gw-left{
  margin:14px 0 0; text-align:center;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls); color:var(--fg-soft);
}

/* Guessing ends the round, so it asks first. */
.gw-confirm{ margin:18px 0 0; padding:18px; text-align:center }
.gw-confirm-pic{ width:120px; margin:0 auto }
.gw-confirm-t{ margin:12px 0 0; font:var(--t-section); letter-spacing:var(--t-section-ls) }
.gw-confirm-yes, .gw-confirm-no{ width:100%; margin:10px 0 0 }

/* Why she was wrong, which is the only part of losing that teaches. */
.gw-verdict{
  margin:14px 0 0; padding:15px; text-align:center;
  border-radius:var(--r-md);
  background:var(--sand-wash);
  border-left:4px solid var(--sand-deep);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
}
.gw-verdict.is-eliminated{ border-left-color:var(--no) }
.gw-verdict.is-right{ border-left-color:var(--yes) }
.gw-verdict-pic{ width:96px; margin:10px auto 0 }
.gw-verdict-t{ margin:0; font:var(--t-card); letter-spacing:var(--t-card-ls) }
.gw-verdict-why{ margin:10px 0 0; font:var(--t-sub); letter-spacing:var(--t-sub-ls) }
.gw-verdict-q{
  margin:8px 0 0; font:var(--t-de); letter-spacing:var(--t-de-ls);
}

/* A face cropped out of a 2x2 sheet. */
.fp{
  width:100%; aspect-ratio:3/4;
  border-radius:8px;
  background-color:var(--paper);
  background-repeat:no-repeat;
  display:flex; align-items:center; justify-content:center;
}
/* The name stands in until the sheet loads, and stays if it never does —
   a board is half-painted for days while Steven works, and an empty square
   is indistinguishable from a face already ruled out. */
.fp-name{
  font:var(--t-eyebrow); letter-spacing:0;
  color:var(--ink-soft); padding:2px; text-align:center;
}
.fp.has-art .fp-name{ display:none }

/* A word cropped out of a 3x3 sheet — sprite.js's tile()/cell(). Same
   pattern as .fp above, one column wider. */
.sp{
  width:100%; aspect-ratio:1/1;
  border-radius:8px;
  background-color:var(--paper);
  background-repeat:no-repeat;
  display:flex; align-items:center; justify-content:center;
}
.sp-word{
  font:var(--t-eyebrow); letter-spacing:0;
  color:var(--ink-soft); padding:4px; text-align:center;
}
.sp.has-art .sp-word{ display:none }
/* `n:0` / no sheet at all — a word tile that was never going to have a
   picture, not a sheet that failed to load. Same look either way; the
   difference does not matter to her. */
.sp.sp-noart{ background-color:transparent; border:1.5px dashed rgb(var(--sand-rgb) / .4) }

.gw-ask-head{ display:flex; align-items:center }

/* The board size, on the board.

   A chooser screen is a gate between her and the game. This sits above the
   faces: one tap to change, and the change is the board itself rather than
   a description of it.

   Dimmed once a question has been asked — switching starts a fresh round,
   and a control that silently throws away work is worse than no control. */
.gw-sizes{ display:flex; align-items:center; gap:6px; margin:14px 0 0; flex-wrap:wrap }
.gw-size{
  display:flex; align-items:baseline; gap:5px;
  padding:6px 12px;
  border:1.5px solid rgb(var(--sand-rgb) / .38);
  border-radius:999px;
  background:var(--sand-wash);
  color:var(--ink);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.gw-size-name{ font:var(--t-btn); letter-spacing:var(--t-btn-ls) }
.gw-size-n{ font:var(--t-eyebrow); letter-spacing:0; color:var(--ink-soft) }
.gw-size.is-on{ border-color:var(--flame); border-width:2px; padding:5px 11px }
.gw-size.is-locked{ cursor:default }
.gw-size.is-locked:not(.is-on){ opacity:.4 }
.gw-size-note{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft); margin-left:2px;
}

/* ==============================================================
   THE PURSE IN THE HEADER
   --------------------------------------------------------------
   The balance is the doorknob. Not a Store button with a number
   beside it — the number itself is what she taps, because the
   number is what makes her want to.
   ============================================================== */
.purse{
  display:inline-flex; align-items:center; gap:6px;
  padding:6px 13px 6px 11px;
  border:1.5px solid rgb(var(--sand-rgb) / .5);
  border-radius:999px;
  background:var(--sand-wash);
  color:var(--ink);
  font-family:var(--ff-ui);
  font-weight:700; font-size:.92rem; line-height:1;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
  transition:border-color .15s, transform .15s;
}
.purse:hover{ border-color:var(--flame) }
.purse:active{ transform:translateY(1px) }
.purse-mark{ color:var(--sand-deep) }
.purse-n{ font-variant-numeric:tabular-nums }



/* ---- RECORDING, ANYWHERE ------------------------------------------

   One switch at the top of a screen, then a mic beside every line while
   it is on. Built by GH.record.deck(), so the songbook, the comic and
   anything added later share these classes rather than each inventing
   their own — see the long note in js/record.js.

   OFF BY DEFAULT is the whole design: a song is thirty lines and a comic
   page a dozen, and mics on all of them unasked is a wall. */
.rec-on{
  display:inline-flex; align-items:center; gap:6px;
  padding:6px 12px;
  border-radius:999px;
  border:1.5px solid rgb(var(--mauve-rgb) / .45);
  background:transparent;
  color:var(--fg-soft);
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.rec-on.is-on{
  border-color:var(--flame);
  color:var(--flame);
  background:rgb(var(--flame-rgb) / .1);
}
.rec-on-ico{ font-size:.95rem; line-height:1 }

/* The per-line row: mic, then Computer and Me once there is a take. */
.rec-row{
  display:inline-flex; align-items:center; gap:6px;
  flex:0 0 auto;
}
.rec-mic{
  width:32px; height:32px; flex:0 0 auto;
  display:flex; align-items:center; justify-content:center;
  border-radius:50%;
  border:1.5px solid rgb(var(--mauve-rgb) / .45);
  background:var(--paper);
  font-size:.9rem; line-height:1;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
/* Live. Colour AND a ring, because colour alone is not a state on a
   screen she may be looking at from an angle in bad light. */
.rec-mic.is-rec{
  border-color:var(--no);
  box-shadow:0 0 0 3px rgb(var(--flame-rgb) / .18);
}
.rec-cmp{
  padding:4px 10px;
  border-radius:999px;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  background:rgb(var(--sand-rgb) / .55);
  color:var(--ink);
  border:none;
  cursor:pointer;
}

/* A song line and its mic share a row: the line is a <button>, so the
   mic cannot live inside it. `min-width:0` lets the line shrink instead
   of pushing the mic off the screen. */
.sg-line-wrap{
  display:flex; align-items:flex-start; gap:8px;
}
.sg-line-wrap .sg-line{ flex:1 1 auto; min-width:0 }
.sg-line-wrap .rec-row{ padding-top:4px }

/* The comic's all-text view stacks speaker, line and mic. */
.cm-all-line .rec-row{ margin-top:6px }

/* ---- THE FIVE RUNGS ------------------------------------------------

   Steven's fill-blank ladder: Choose, Copy, Options, Peek, Type. Five
   buttons where there were two, so the toggle carries ICONS and only the
   active rung shows its name underneath. Five words on one phone row was
   never going to work — "Посмотреть, затем ввести" alone is wider than a
   fifth of a 380px screen.

   `currentColor` is the whole mechanism. The icon markup declares no
   paint at all, so the SVG takes the button's colour: accent when
   active, soft ink otherwise, and every theme works with no extra art.
   This is also why the icons are inline and not <img> — an <img> cannot
   inherit currentColor. */
.mode-toggle.is-five{
  display:grid;
  grid-template-columns:repeat(5, minmax(0,1fr));
  gap:4px;
}
.fb-rung{
  display:flex; align-items:center; justify-content:center;
  padding:7px 0;
  border-radius:var(--r-sm);
  border:1.5px solid rgb(var(--mauve-rgb) / .35);
  background:transparent;
  color:var(--fg-soft);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.fb-rung.is-on{
  /* REVERSE, not a faint tint. Steven, 11 Sep: the active rung's icon was
     invisible — it inherited `color:var(--flame)` on a 10% flame background
     (flame-on-faint-flame), which washes out, and on the pale themes it
     vanished entirely. A solid flame fill with the guaranteed-contrast
     on-flame ink makes the icon read on every theme. */
  border-color:var(--flame);
  color:var(--ink-on-accent);
  background:var(--flame);
}
.fb-ico{ display:block; line-height:0 }
.fb-ico svg{ width:22px; height:22px; display:block }

/* The active rung's name, and the ? that opens the legend. */
.fb-mode-why{
  display:flex; align-items:center; justify-content:center;
  gap:8px; margin:8px 0 2px;
  /* Room on the right so a long mode name cannot run under the ? in the
     corner. Mirrored on the left so the name stays optically centred. */
  padding:0 30px;
}
.fb-mode-name{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  text-transform:uppercase; color:var(--fg);
}
/* Top right of the card, not in the mode row. See the note in
   fill-blank.js. Absolute rather than floated so it never reflows the
   toolbar, and slightly larger than before because a corner target with
   nothing beside it needs to be tappable on its own. */
.fb-mode-q{
  position:absolute; top:10px; right:10px; z-index:2;
  width:26px; height:26px; flex:0 0 auto;
  display:flex; align-items:center; justify-content:center;
  border-radius:50%;
  border:1.5px solid rgb(var(--mauve-rgb) / .5);
  background:rgb(var(--sand-rgb) / .10); color:var(--fg-soft);
  font:700 .82rem/1 var(--ff-ui);
  cursor:pointer;
}
.fb-mode-q:hover{
  background:rgb(var(--sand-rgb) / .22);
  border-color:rgb(var(--mauve-rgb) / .8);
  color:var(--fg);
}

/* GLOWING UNTIL SHE FINDS IT. Set by fill-blank.js once the legend has
   been seen but the ? has never been used — see the note by
   `legendSeen` there. A pointer, not an alert: it pulses slowly and it
   stops for good the first time she taps it.

   `--flame` here rather than a role colour, because this is the app's
   own "look at this" accent and that is exactly what it is doing. */
.fb-mode-q.is-new{
  border-color:var(--flame);
  color:var(--flame);
  background:rgb(var(--flame-rgb) / .14);
  animation:fbq-glow 2.4s ease-in-out infinite;
}
@keyframes fbq-glow{
  0%,100%{ box-shadow:0 0 0 0 rgb(var(--flame-rgb) / .55) }
  50%    { box-shadow:0 0 10px 3px rgb(var(--flame-rgb) / .30) }
}
@media (prefers-reduced-motion: reduce){
  /* The colour still marks it; only the pulse goes. */
  .fb-mode-q.is-new{ animation:none; box-shadow:0 0 8px 2px rgb(var(--flame-rgb) / .35) }
}
.fb-mode-help{
  margin:0 0 10px; text-align:center;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
}

/* COPY and PEEK show the answer. Big, because she is reading it to
   reproduce it, and centred so it sits over the input below. */
.fb-show{
  margin:4px 0 10px; text-align:center;
  font:600 1.5rem/1.2 var(--ff-display);
  color:var(--fg);
}
.fb-ready{ display:block; margin:0 auto 12px }

/* OPTIONS keeps the four words on screen as plain chips — readable, not
   tappable. A tappable one here would just be Choose again. */
.fb-ref{
  display:flex; flex-wrap:wrap; justify-content:center;
  gap:6px; margin:0 0 12px;
}
.fb-ref-w{
  padding:4px 10px;
  border-radius:var(--r-sm);
  background:rgb(var(--sand-rgb) / .5);
  color:var(--ink-soft);
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
}

/* ---- THE LEGEND, in howto.js's panel ---- */
.howto-legend{ display:flex; flex-direction:column; gap:14px; margin:4px 0 16px }
.howto-leg-row{ display:flex; align-items:flex-start; gap:12px }
.howto-leg-ico{ flex:0 0 auto; color:var(--fg-accent); line-height:0 }
.howto-leg-ico svg{ width:24px; height:24px; display:block }
.howto-leg-txt{ display:flex; flex-direction:column; gap:2px; min-width:0 }
.howto-leg-name{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  text-transform:uppercase; color:var(--fg);
}
.howto-leg-desc{
  font:var(--t-sub); letter-spacing:var(--t-sub-ls); color:var(--fg-soft);
}

/* ---- THE PURSE PANEL ------------------------------------------------

   Tapping the balance opens this instead of a screen, so it can never
   throw away a round — see the note in js/purse.js. Positioned by
   GH.nav.clampPanel, which writes `left`, so nothing here may set a
   side: a rule setting `right` would fight it and win on first paint.

   Opaque and declares its ink, per the rule. It floats over content, and
   content reading through a balance panel is unreadable. */
.purse-pop{
  position:absolute;
  top:calc(100% + 6px);
  left:0;
  right:auto;
  z-index:60;
  width:232px;
  max-width:calc(100vw - 16px);
  box-sizing:border-box;
  padding:14px 14px 12px;
  text-align:center;
  border-radius:var(--r-md);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft); --fg-accent:var(--flame);
  color:var(--fg);
  border:1.5px solid rgb(var(--mauve-rgb) / .45);
  box-shadow:0 10px 28px rgb(var(--ink-rgb) / .22);
}
.purse-pop-n{
  display:flex; align-items:baseline; justify-content:center; gap:8px;
  font:700 1.9rem/1 var(--ff-display);
  color:var(--fg);
}
.purse-pop-l{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  text-transform:uppercase; color:var(--fg-soft);
  margin:6px 0 12px;
}
.purse-pop-go{ display:block; width:100% }
/* The second way out — see the note in purse.js. Sits under the primary
   with a little air, full width so the two read as a pair rather than as
   a button and an afterthought. */
.purse-pop-shop{ display:block; width:100%; margin-top:8px }
.purse-pop-note{
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft); margin:10px 0 0;
}
/* Counting up after a payout. The lift and the ring say "this went up"
   from anywhere on the page, not only on the end screen. */
.purse.is-up{
  border-color:var(--flame);
  transform:scale(1.06);
}
/* The colour change still applies to the CHARACTER, which is what shows
   until the WebP is in place and if it ever 404s. */
.purse.is-up .purse-mark{ color:var(--flame) }
/* AND A SCALE FOR THE PICTURE, because a raster cannot take a colour.
   The mark used to flip to --flame while the balance counted up, and that
   flip was the signal she had just been paid — visible from anywhere on
   the page rather than only on the end screen. A WebP ignores `color`
   entirely, so the signal had to become something a picture can do.

   Scale and not opacity: fading a mark reads as it going away, which is
   the opposite of being paid. */
.purse.is-up .crystal-mark img{ transform:scale(1.25) }
.crystal-mark img{ transition:transform .18s ease }
@media (prefers-reduced-motion: reduce){
  .purse{ transition:none }
  .purse.is-up{ transform:none }
  .crystal-mark img{ transition:none }
  .purse.is-up .crystal-mark img{ transform:none }
}

/* ==============================================================
   THE BUTLER
   --------------------------------------------------------------
   The missing first thirty seconds. Nothing on first open told
   her what is here or where to start.
   ============================================================== */
/* THE BUTLER STANDS AT THE SIDE.

   Not a centred modal. A tour has to let her SEE and REACH the page it is
   talking about — a sheet over the top makes that impossible, and a
   highlighted button she cannot press is worse than no highlight at all.

   `is-blocking` is for the moments that really are a stop: the first offer,
   the handover. Those darken the page. A tour step never does. */
.bt-overlay{ display:none }

/* ---------- THE SCRIM: THE DIM, AND NOTHING ELSE ----------

   The dim used to be `.bt-overlay`'s own background, which made the scrim
   and the bubble one element and therefore one stacking context at z-index
   70. `.bt-lit` is 81, so ANY highlighted element painted over the bubble,
   and a tall one buried it: highlighting the Games section put the whole
   games grid on top of Waddles.

   No value of `.bt-lit` could fix that — above 70 covers the bubble, below
   70 gets dimmed, which defeats the highlight. So the dim is its own
   element underneath:

       .bt-scrim    700   the dim, paint only
       .wm-stage    750   full-screen panels a tour step runs INSIDE,
       .gr-overlay  755   raised so their contents are not dimmed and
       .ptd-wrap    758   can still be lit — a child cannot escape its
       .howto       765   ancestor's stacking context
       .lg-wrap     770
       .lb-overlay  780   the zoomed picture: the top of everything the
                          user can open, and under the bubble ONLY while
                          a tour is running — the Full Tour opens it on
                          purpose at steps 60 and 61
       .bt-lit      810   the thing he is pointing at
       .bt-overlay  900   him and his bubble, always on top
       .bt-gift     950   the crystal hand-over

   RAISED FROM 70/81/90 ON 09 Sep. At 90 the bubble still sat under the
   lightbox and the how-to panel (both 200) and the language picker
   (120), so "Waddles is behind" was reachable from several steps.
   Steven: "pull him to the top everywhere across the board." He is now
   above every z-index in this file except nothing.

   The one deliberate exception is `.wc-overlay` at 90 — the welcome
   screen, which is only ever on screen before any tour exists.

   `butler.js`'s `ensure()` creates the scrim first and appends it first,
   and `show()` toggles both together. */
.bt-scrim{ display:none }
.bt-scrim.is-open{
  display:block;
  position:fixed; inset:0; z-index:700;
  pointer-events:none;
  background:rgb(var(--scrim-rgb) / .3);
}
.bt-scrim.is-blocking{ background:rgb(var(--scrim-rgb) / .55) }

/* `pointer-events:none` on the overlay itself is what keeps the page
   underneath usable — nothing here blocks a tap. The dim lives in
   `.bt-scrim` above; this layer carries no background of its own. */
.bt-overlay.is-open{
  display:flex;
  /* TOP LEFT, NOT BOTTOM CENTRE. He sat at the bottom middle, which put
     his bubble across the widest part of whatever he was pointing at — on
     the Games step it covered three game tiles. Top left is the one corner
     of the content area that is never the subject of a step.
     96px clears the sticky jumpbar (two rows on a phone) and the back
     button, both of which he would otherwise cover now that he is at 90. */
  align-items:flex-start; justify-content:flex-start;
  position:fixed; inset:0; z-index:900;
  pointer-events:none;
}
.bt-overlay.is-blocking{
  /* The hello/handover screen is a real stop and still owns the middle of
     the screen. This selector is more specific than the rule above, so
     both axes have to be restated or the top-left alignment leaks in.
     `safe center`, not `center` — a tall panel would otherwise push its
     own top off the screen, and this is the one panel she cannot dismiss. */
  align-items:flex-start; align-items:safe center;
  justify-content:center;
  overflow-y:auto;
  pointer-events:auto;
}

/* Waddles is 642 x 1322 — a standing penguin, almost exactly 1:2. So he
   stands BESIDE the bubble rather than under it. A tall figure under a
   bubble would be 62px wide and 127 tall in the corner, and the tail would
   point at his hat rather than his face. */
.bt-stage{
  /* The stage's rectangle includes transparent flex space around Waddles and
     the bubble. Let taps through that empty space so a nearby highlighted
     target (notably Game Guide / Store) cannot be intercepted invisibly.
     The bubble opts back in below so its own controls remain clickable. */
  pointer-events:none;
  /* `flex-start`, not `flex-end`. Bottom-aligned was right when the stage
     sat on the bottom edge; at the top of the screen his head and the
     bubble's tail have to line up from the top instead. */
  display:flex; align-items:flex-start;
  gap:0;
  /* 96px down, hard against the left. See the alignment note on
     `.bt-overlay.is-open` for why 96. */
  margin:96px 12px 12px;
  /* NARROWER THAN IT WAS (460px). A narrow, taller bubble reaches across
     less of whatever he is pointing at, which is the whole reason he moved
     up here. Roughly four lines of body text instead of three wide ones. */
  max-width:min(340px, calc(100vw - 24px));
}
/* The hello/offer screen (blocking) keeps him beside the bubble too, same
   as every other step — just with more room to work with since it owns
   the whole screen, so the bubble is allowed to grow into it instead of
   staying content-width. */
/* HE STANDS AT THE BOTTOM WHEN HIS TARGET IS AT THE TOP.

   Set by `highlight()` in butler.js whenever the lit element's top edge
   is in the upper quarter of the screen — see the note there. The
   crystals popup and the Appearance panel both open downward out of the
   toolbar, and the bubble's usual 96px perch is exactly where they land.

   `margin-top:auto` rather than a fixed bottom offset, so the stage keeps
   its own height and simply sits at the far end of the flex column. */
.bt-overlay.is-low .bt-stage{
  margin-top:auto;
  margin-bottom:16px;
}

.bt-overlay.is-blocking .bt-stage{
  /* auto, not 0 — centres when it fits, top-aligns when it does not. */
  margin:auto;
  width:100%;
  max-width:min(520px, calc(100vw - 32px));
}

/* The bubble, with a tail that points down at his face.

   The tail is a rotated square rather than a border triangle, so it takes
   the bubble's own background and border and stays correct in all twelve
   themes without a per-theme colour. */
.bt-bubble{
  pointer-events:auto;
  position:relative;
  background:var(--paper);
  border:1.5px solid rgb(var(--sand-rgb) / .5);
  border-radius:var(--r-lg);
  box-shadow:0 8px 28px rgb(var(--scrim-rgb) / .32);
  padding:15px 17px;
  color:var(--ink);
  --fg:var(--on-dark);
}
/* On the blocking screen, let the bubble take whatever width the stage
   has to give rather than sizing to its own content — his speech should
   fill the space beside him, especially on a phone where width is the
   thing there's the most of. */
.bt-overlay.is-blocking .bt-bubble{ flex:1 1 auto; min-width:0 }
/* The tail points LEFT, into his head, which sits near the top of him
   because he is drawn standing. */
.bt-bubble::after{
  content:'';
  position:absolute; left:-8px; top:26px;
  width:15px; height:15px;
  background:var(--paper);
  border-left:1.5px solid rgb(var(--sand-rgb) / .5);
  border-bottom:1.5px solid rgb(var(--sand-rgb) / .5);
  transform:rotate(45deg);
  border-bottom-left-radius:3px;
}
.bt-overlay.is-blocking .bt-bubble::after{ display:none }

/* Him, to the left of the bubble. `order:-1` so he comes first visually
   while the bubble stays first in the DOM — the words are what matters to a
   screen reader, and the picture is decoration. Same left-of-the-bubble
   arrangement on the blocking screen as everywhere else — he just gets a
   bigger picture there (see .bt-face below) since the screen is his alone. */
.bt-who{
  order:-1; flex:none;
  display:flex; flex-direction:column; align-items:center; gap:2px;
  margin:0 6px 0 0;
}
.bt-overlay.is-blocking .bt-who{ margin:0 10px 0 0 }
.bt-who.has-no-face .bt-name{ padding-left:0 }
/* A CUT-OUT FIGURE, NOT A PORTRAIT.

   No circular crop, no border, no background — all three were right for a
   round face in a chip and all three are wrong for a penguin standing on
   the interface. The only thing left is a soft shadow so he does not look
   pasted onto whatever is behind him.

   Sized by HEIGHT, with the width following, because his aspect is fixed
   and the height is what has to agree with the bubble beside him. 118px is
   about 236 physical pixels on her phone, from a 1322px source, so there is
   room to grow. */
.bt-face{
  height:118px; width:auto;
  border-radius:0; border:0; background:none;
  object-fit:contain;
  filter:drop-shadow(0 4px 10px rgb(var(--scrim-rgb) / .38));
}
@media (max-width:400px){ .bt-face{ height:96px } }
/* The hello/offer screen is his introduction — bigger than a mid-tour
   aside, not smaller. */
.bt-overlay.is-blocking .bt-face{ height:148px }
@media (max-width:400px){ .bt-overlay.is-blocking .bt-face{ height:116px } }
/* "Max is returning to the store to wait for you…" — the line above a
   departing pet's goodbye. Quieter than what he says, because it is the
   app narrating rather than the pet speaking. */
.bt-head{
  margin:0 0 7px;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}

/* What she must press, when the step is one she performs. */
.bt-do{
  margin:9px 0 0;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--flame);
}
.bt-name{
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft); text-transform:uppercase;
}
.bt-line{
  margin:0 0 10px;
  font:var(--t-body); letter-spacing:var(--t-body-ls);
}
.bt-pet{ color:var(--flame) }
.bt-bye{ font-style:italic; color:var(--fg-soft) }
.bt-acts{ display:flex; flex-direction:column; gap:8px; margin:16px 0 0 }
.bt-btn{
  width:100%; padding:12px 16px;
  border:1.5px solid rgb(var(--sand-rgb) / .5);
  border-radius:var(--r-md);
  background:var(--sand-wash);
  color:var(--ink);
  font:var(--t-btn); letter-spacing:var(--t-btn-ls);
  cursor:pointer;
}
.bt-btn.is-primary{
  background:var(--flame); border-color:var(--flame); color:var(--on-flame);
}
.bt-count{
  margin:12px 0 0; text-align:center;
  font:var(--t-eyebrow); letter-spacing:var(--t-eyebrow-ls);
  color:var(--fg-soft);
}

/* ---------- THE GIFT, FRONT OF SCREEN ----------

   Built by `giftPop()` in js/butler.js when a tour step carries `gift`.

   z-index 95: above the bubble (90), because for two seconds this IS the
   thing being said. `pointer-events:none` on everything here — the same
   step asks her to tap the crystal icon in the header, and a panel that
   swallowed that tap would be worse than no panel.

   The crystal is sized by HEIGHT against `.bt-face` (118px in a tour step)
   and deliberately larger: 200px, so it reads as the subject rather than
   an illustration beside him. */
.bt-gift{
  position:fixed; inset:0; z-index:950;
  display:flex; flex-direction:column;
  align-items:center; justify-content:center;
  gap:10px;
  pointer-events:none;
  animation:bt-gift-in .28s ease-out both;
}
.bt-gift-n{
  margin:0;
  font-family:var(--ff-display, var(--ff-ui));
  font-weight:800;
  font-size:clamp(2rem, 9vw, 3.4rem);
  line-height:1;
  color:var(--flame);
  text-shadow:0 3px 18px rgb(var(--scrim-rgb) / .55);
  white-space:nowrap;
}
.bt-gift-img{
  height:200px; width:auto;
  object-fit:contain;
  filter:drop-shadow(0 10px 30px rgb(var(--scrim-rgb) / .5));
}
/* The character fallback when the file is missing. A glyph has no
   intrinsic height, so it needs a font-size instead. */
.bt-gift-img.is-char{
  height:auto;
  font-size:200px; line-height:1;
  color:var(--flame);
}
@media (max-width:400px){
  .bt-gift-img{ height:150px }
  .bt-gift-img.is-char{ font-size:150px }
}
@keyframes bt-gift-in{
  from{ opacity:0; transform:scale(.7) }
  to{ opacity:1; transform:scale(1) }
}
@media (prefers-reduced-motion: reduce){
  .bt-gift{ animation:none }
}

/* Where he goes when dismissed. Dismissing him is never permanent from her
   side — the perch stays, and he says he is there. */
/* HIM, SMALL, IN THE HEADER.

   A 34px circle was right for a bell and wrong for a standing penguin: at
   1:2 he would be letterboxed into a strip 17px wide and read as a smudge.

   So the perch is TALLER THAN IT IS WIDE and takes his aspect. It sits
   beside the theme picker and the pet faces, so it cannot grow much —
   30 x 40 is about as large as that row tolerates.

   No circle and no background: he is a cut-out figure and a disc behind
   him would put him in a badge. The border only appears while he is
   glowing, which is the one moment he needs an outline. */
/* A ROUND 40px BUTTON, LIKE EVERYTHING ELSE IN THE ROW.

   Steven, 09 Sep: "Waddles should also be in a round circle to match."

   It was 30x40 with an 8px radius and no background — a tall thin
   rounded rectangle sitting between three pill-shaped controls, which is
   why it read as the odd one out even before you noticed the heights.

   Now the same 40px box the crystal, the theme swatch and the pet pill
   use, fully round, with the same faint border and wash the pet pill
   carries so the four read as one set. */
/* 38px, THE SAME AS THE THEME COLOUR CIRCLE.

   Steven, 09 Sep: "I have no problem with Waddles being clipped inside
   his circle, I just want that circle the exact same size as the theme
   color circle."

   The theme control is a 40px button holding a 38px dot, so the CIRCLE
   she sees there is 38 — and the perch was a 40px circle, one step
   bigger than the thing next to it. Deliberately left out of the 40px
   group above for this reason; `align-self:center` from the row still
   centres it, so 38 in a 40 row sits with 1px either side.

   `overflow:hidden` and a filling image: he is clipped by the circle
   rather than shrunk to fit inside it. */
.bt-perch{
  width:38px; height:38px;
  align-self:center;
  flex:none;
  display:grid; place-items:center;
  overflow:hidden;
  padding:0;
  border:1px solid rgb(var(--sand-rgb) / .45);
  border-radius:999px;
  background:var(--sand-wash);
  font-size:1.05rem; line-height:1;
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.bt-perch:hover{ border-color:rgb(var(--sand-rgb) / .8) }
/* WHERE HE WENT.

   Scale alone was too quiet against a busy header, so it glows as well.
   Three beats over about two and a half seconds — enough to catch her eye
   once, not enough to become something blinking at her while she works. */
.bt-perch.is-new{
  border-color:var(--flame);
  animation:btPulse .85s ease-in-out 3;
}
@keyframes btPulse{
  0%,100%{ transform:scale(1); box-shadow:0 0 0 0 rgb(var(--flame-rgb) / 0) }
  50%{
    transform:scale(1.14);
    box-shadow:0 0 0 5px rgb(var(--flame-rgb) / .32),
               0 0 15px 3px rgb(var(--flame-rgb) / .5);
  }
}
/* His face on the perch, not a bell. `contain` so a tall figure is never
   cropped, and `bottom` so he stands on the row rather than floating with
   his feet cut off. */
/* ANY image in the perch, not just one carrying the class. The button is
   40px and the artwork is a full-body portrait several times that, so an
   unconstrained img is the difference between a face on a button and a
   penguin standing over the toolbar. */
/* THE SAME SIZE AS THE PET FACES, AND CENTRED.

   Steven, 09 Sep: "Waddles bubble is too big, it does not line up with
   the rest of the bar."

   It was `height:100%` with `object-position:bottom` — a full-body
   portrait stretched to the full 40px and stood on the floor of its box,
   so he read as half again the size of the 26px faces next to him and
   his feet sat on the bottom edge rather than on the row's centre line.

   26px to match `.petstrip-mini` exactly, centred in the 40px button the
   way every other control centres its contents. */
/* ZOOMED ONTO HIS FACE, AND CLIPPED BY THE CIRCLE.

   Steven, 09 Sep: "I liked Waddles face — zoom back in on him, just have
   his circle the same size as the theme circle. I never asked you to
   shrink him to an ant."

   The reason cropping alone never showed a face: `perchFace` in
   data/butler-script.js is EMPTY, so this falls back to `portrait`, which
   is the full standing figure — hat, tailcoat, feet. Cropping that to a
   38px circle gives shoulders and a body at best.

   So it scales up and takes the head. `transform-origin` at 22% down is
   roughly where his face sits in the standing portrait; `scale(2.4)`
   fills the circle with it. `overflow:hidden` on `.bt-perch` does the
   clipping.

   THE PROPER FIX IS A PICTURE, NOT CSS: set `perchFace` to a tight
   face-crop file and the scale below can go back to 1. The comment in
   butler-script.js says as much and has since it was written. Until then
   these two numbers are the dial — raise the scale to come closer, move
   the origin down to look lower on the figure. */
.bt-perch-face,
.bt-perch img{
  width:100%; height:100%;
  /* PULLED BACK. Steven, 10 Sep: he was super-zoomed in. At 1.6 the
     circle held his head alone. At 1.0 the crop does the work — `cover`
     on a standing portrait in a square frame keeps a vertical band, and
     `object-position` chooses which. 22% down lands on his face rather
     than the top edge of the canvas.

     Two numbers to nudge: raise the scale to come closer, raise the
     percentage to look further down the figure. */
  object-fit:cover; object-position:center 22%;
  /* 1.6, not 2.4. At 2.4 the circle held one eye. The head needs to fit
     INSIDE the frame, not fill it — so enough zoom to get past the
     tailcoat and the feet, and no more.

     The two numbers are the dial: raise the scale to come closer, move
     the origin percentage down to look lower on the figure. */
  /* 3px right. Steven, 10 Sep: "he's the right size but too far left."

     A TRANSFORM, NOT `object-position`. With `cover` on a portrait that
     is taller than it is wide, the image is scaled to fill the width — so
     there is no horizontal overflow to slide, and changing the X
     percentage does nothing at all. Moving the element is the only thing
     that moves him. */
  transform:translateX(3px) scale(1);
  transform-origin:center 22%;
  display:block;
}

/* What a step is pointing at. Above the scrim, so the thing he names is
   the one bright object on the page.

   THE RING AND THE LIFT ARE TWO CLASSES, and that is not tidiness.
   `z-index` needs a positioned element, but setting `position` on an
   element that is ALREADY positioned overwrites it: `.backlink` is
   `position:fixed`, this rule comes later in the file, and so
   highlighting the back button used to drop it out of its fixed corner
   into the document flow. Eighteen tour steps point at `.backlink`.

   So `butler.js` adds `bt-lit-static` only when the computed position is
   `static` — an element with nothing to lose. Everything else keeps its
   own positioning and just gets the ring. */
.bt-lit{
  z-index:810;
  /* DO NOT set position here. Tour targets can already be fixed, sticky,
     absolute, or relative — especially .backlink. Overriding that positioning
     is what made the Quick and Full tours pull Back out of its real corner.
     butler.js adds .bt-lit-static only to genuinely static targets, and that
     class supplies position:relative for the sparkle anchors without moving
     already-positioned controls. */
  /* STATIC RING + SPARKLES, not a pulsing glow. Steven, 11 Sep: on a huge
     target (the whole Games section) the expanding/contracting box-shadow
     was ugly and thrashed paint, feeding the jitter. Now the ring/glow is
     STATIC (no size animation = no paint thrash, no reflow), and the motion
     that draws the eye comes from small SPARKLES on the pseudo-elements,
     which are tiny and animate independently of the target's box. Glow
     spread trimmed (was 22px) so it bleeds less past the edges. */
  outline:4px solid var(--flame);
  outline-offset:3px;
  border-radius:var(--r-sm);
  /* Bright and STATIC. v372 trimmed this so far it was nearly invisible on
     the dark themes (Steven, 11 Sep — the Reference step looked unhighlighted).
     Back to a strong ring + glow, just without the size ANIMATION that
     caused the jitter — static keeps it calm but visible. */
  box-shadow:
    0 0 0 3px var(--paper),                  /* gap so the ring never blends in */
    0 0 0 8px rgb(var(--flame-rgb) / .40),   /* solid outer wash */
    0 0 26px 8px rgb(var(--flame-rgb) / .75);/* bright static glow */
}
/* Two sparkles that twinkle at opposite corners — the moving cue that
   replaces the pulsing halo. Positioned on the ring, animated on opacity +
   scale only (compositor-friendly, never reflows). */
.bt-lit::before,
.bt-lit::after{
  content:"";
  position:absolute;
  width:12px; height:12px;
  border-radius:50%;
  pointer-events:none;
  z-index:2;
  background:
    radial-gradient(closest-side, var(--paper) 0%, var(--flame) 45%, transparent 70%);
  filter:drop-shadow(0 0 4px rgb(var(--flame-rgb) / .9));
  opacity:0;
}
.bt-lit::before{ top:-8px; left:-8px; animation:bt-sparkle 1.6s ease-in-out infinite; }
.bt-lit::after { bottom:-8px; right:-8px; animation:bt-sparkle 1.6s ease-in-out .8s infinite; }
@keyframes bt-sparkle{
  0%,100%{ opacity:0; transform:scale(.4) rotate(0deg); }
  50%    { opacity:1; transform:scale(1) rotate(45deg); }
}
@media (prefers-reduced-motion: reduce){
  .bt-lit::before, .bt-lit::after{ animation:none; opacity:.9; }
}

/* HIGHLIGHT INSIDE THE JUMP BAR MUST NOT CHANGE LAYOUT. Steven, 11 Sep:
   the Jump Bar step jittered. Root cause — the jump bar is
   `display:flex; flex-wrap:wrap`, and `.bt-lit`'s `outline` + 3px
   `outline-offset` enlarged a button's footprint enough to reflow the
   wrap, changing the sticky bar's height, shifting the page, which
   re-triggered placement measurement → wobble. Box-shadow never causes
   reflow; outline can interact with wrap. So inside the jump bar, drop the
   outline and offset and carry the whole highlight in box-shadow. Same
   look, zero layout effect. */
.jumpbar .bt-lit{
  /* v379: the host has to rise above the tour scrim so the target can be
     bright at all, but that also makes every neighbouring jump pill bright.
     On a phone those pills are only 6px apart, so an OUTER shadow alone is
     partly covered by the neighbours and the selected pill still reads like
     the rest. Make the target unmistakable INSIDE its own pixels as well:
     the inset flame ring cannot be covered by a sibling, and outline does not
     participate in flex sizing, so this does not change the wrap geometry. */
  outline:4px solid var(--flame) !important;
  outline-offset:3px !important;
  border-color:var(--paper) !important;
  border-radius:999px;
  box-shadow:
    inset 0 0 0 3px var(--flame),
    0 0 0 3px var(--paper),
    0 0 0 7px var(--flame),
    0 0 24px 9px rgb(var(--flame-rgb) / .90) !important;
}

/* Raising the whole jumpbar is necessary to let one child escape the scrim.
   There are TWO intentional visual states:

   1. A SINGLE jump-pill target (Reference / Games / ↑): mute its siblings so
      the one Waddles says to tap is unmistakable.
   2. The JUMP-BAR OVERVIEW step: the bar itself is the target, so every pill
      is part of what he is showing her. Do NOT make the row look disabled;
      give every jump button the same bright treatment as a single target.

   The `:not(.bt-lit)` guard is the distinction: on a single-pill step the host
   has bt-lit-host only; on the overview the jumpbar itself also has bt-lit. */
.jumpbar.bt-lit-host:not(.bt-lit){
  background:linear-gradient(rgb(var(--scrim-rgb) / .58), rgb(var(--scrim-rgb) / .58));
}
.jumpbar.bt-lit-host:not(.bt-lit) .jump:not(.bt-lit){
  opacity:.38;
  filter:brightness(.55);
}

/* WHOLE JUMP BAR OVERVIEW. Steven, 11 Sep: when Waddles introduces the whole
   bar, "make them all glow like Reference." Keep every pill fully bright and
   use the exact same inner/outer cue as `.jumpbar .bt-lit`. This changes no
   dimensions, so the wrapped bar cannot reflow or jitter. */
.jumpbar.bt-lit.bt-lit-host .jump{
  opacity:1 !important;
  filter:none !important;
  position:relative;
  z-index:810;
  outline:4px solid var(--flame) !important;
  outline-offset:2px !important;
  border-color:var(--paper) !important;
  box-shadow:
    inset 0 0 0 3px var(--flame),
    inset 0 0 14px 2px rgb(var(--flame-rgb) / .42),
    0 0 0 3px var(--paper),
    0 0 0 7px var(--flame),
    0 0 24px 9px rgb(var(--flame-rgb) / .90) !important;
}

/* A highlighted child cannot escape an ancestor stacking context. The sticky
   jump bar is z-20 and the purse popup is z-60, both below the tour scrim at
   700; that is why Reference/Games/↑ and Go to Crystals stayed DIM even
   though .bt-lit itself is z-810. butler.js marks only the containing host
   for the duration of the highlight. Lift the host above the scrim but keep
   it below Waddles (900). No geometry changes, so the jump bar cannot reflow. */
.bt-lit-host{ z-index:805 !important; }


/* THE POINTER ARROW. Built by js/butler-arrow.js, positioned by butler.js
   `place()` at the bubble edge and rotated to aim at the lit target. Fixed
   so it sits in the same coordinate space as the highlight; its colour is
   the accent so it matches the ring. */
.bt-arrow{
  position:fixed;
  z-index:812;                 /* above the bubble (811) and the ring, so it is never hidden */
  width:44px; height:56px;
  color:var(--flame);          /* the arrow itself: the accent, matching the ring */
  pointer-events:none;
  animation:bt-arrow-bob 1.1s ease-in-out infinite;
}
/* THE HALO ADAPTS TO THE ARROW'S OWN LIGHTNESS. Steven, 10 Sep: a dark
   glow when the arrow is bright, a bright glow when it is dark, so it never
   disappears into a same-hued background. `--on-flame` is exactly that —
   the palette already defines it as the colour legible ON the accent, so
   it is dark against a bright flame and light against a dark one, tuned
   per theme. Two stacked drop-shadows give a ring of it all the way
   around. */
.bt-arrow-svg{
  width:100%; height:100%; display:block; transform-origin:50% 50%;
  filter:
    drop-shadow(0 0 2px var(--on-flame))
    drop-shadow(0 0 4px var(--on-flame));
}
@keyframes bt-arrow-bob{
  0%,100%{ transform:translate(0,0) }
  50%{ transform:translate(0,-5px) }
}
@media (prefers-reduced-motion: reduce){
  .bt-arrow{ animation:none }
}
.bt-lit-static{ position:relative }
@media (prefers-reduced-motion: reduce){
  .bt-perch.is-new{ animation:none }
}

/* A NEWER BUILD IS AVAILABLE.

   Bottom of the screen, not the top: the top is where the back link now
   sticks, and two things competing for that space is worse than either.

   It offers and does not reload. Reloading under someone mid-round throws
   the round away, and an app that restarts uninvited is frightening. */
.update-bar{
  position:fixed; left:12px; right:12px; bottom:12px; z-index:60;
  display:flex; align-items:center; gap:10px;
  padding:11px 12px 11px 15px;
  border-radius:var(--r-md);
  background:var(--card);
  border:1.5px solid rgb(var(--flame-rgb) / .55);
  box-shadow:0 6px 24px rgb(var(--scrim-rgb) / .3);
}
.update-msg{
  flex:1; min-width:0;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg);
}
.update-go{
  flex:none;
  padding:7px 14px; border:0; border-radius:999px;
  background:var(--flame); color:var(--on-flame);
  font:var(--t-btn); letter-spacing:var(--t-btn-ls);
  cursor:pointer;
}
.update-no{
  flex:none;
  width:28px; height:28px; line-height:1;
  border:0; border-radius:999px;
  background:rgb(var(--shade-rgb) / .25);
  color:var(--fg-soft);
  font-size:1.05rem; cursor:pointer;
}
@media (min-width:620px){
  .update-bar{ left:auto; right:16px; bottom:16px; max-width:420px }
}

/* ==============================================================
   FIRST RUN
   --------------------------------------------------------------
   Three questions before anything else. A modal here IS correct,
   unlike the tour — she has nothing to explore yet and nothing
   behind this is usable until it is answered.
   ============================================================== */
.wc-overlay{ display:none }
.wc-overlay.is-open{
  display:flex; align-items:flex-start; justify-content:center;
  overflow-y:auto;
  position:fixed; inset:0; z-index:90;
  /* Nearly solid. This is the first thing she ever sees and there is
     nothing behind it worth showing — a hub bleeding through the question
     is noise, not context. */
  background:rgb(var(--scrim-rgb) / .92);
  padding:18px;
}
/* THE SAME CARD THE REST OF THE APP USES.

   This was invented from scratch: `background:var(--card)`, which is
   semi-transparent, and no `color` at all. So the hub showed through it and
   the heading inherited the page's dark ink onto a dark surface — the exact
   contrast failure fixed in thirty-four other rules earlier.

   `.card` is opaque, declares its own ink, and is already correct in all
   twelve themes. The welcome now wears it and only overrides the size. */
.wc-card{
  width:100%; max-width:380px;
  padding:24px 20px 18px;
  text-align:center;
  color:var(--fg);
  /* centres it when it fits, top-aligns it when it does not */
  margin:auto;
}
.wc-head{
  margin:0 0 6px;
  font:var(--t-section); letter-spacing:var(--t-section-ls);
  color:var(--fg);
}
.wc-note{
  margin:0 0 14px;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg-soft);
}

/* The language buttons carry no heading, because she cannot read a heading
   yet — that is the entire point of the first screen. Each label is in its
   own language and `lang` is set so the right font is used. */
.wc-langs{ display:flex; flex-direction:column; gap:8px; margin:4px 0 0 }
/* A LIGHT SURFACE INSIDE A DARK CARD REMAPS ITS OWN INK.

   `color:var(--ink)` alone is not enough: anything inside still inherits
   `--fg-soft` from the page, which is a pale colour meant for a dark
   background and is unreadable here. The pattern used everywhere else is
   to remap BOTH and then take colour from --fg. */
.wc-lang{
  padding:13px 16px;
  border:1.5px solid rgb(var(--sand-rgb) / .45);
  border-radius:var(--r-md);
  background:var(--sand-wash);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  font:var(--t-card); letter-spacing:var(--t-card-ls);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.wc-lang:hover{ border-color:var(--flame) }
.wc-lang.is-on{ border-color:var(--flame); border-width:2.5px; padding:12px 15px }

.wc-input{
  width:100%; margin:2px 0 0;
  padding:13px 14px;
  border:1.5px solid rgb(var(--sand-rgb) / .5);
  border-radius:var(--r-md);
  background:var(--paper);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  font:var(--t-body); letter-spacing:var(--t-body-ls);
  text-align:center;
  -webkit-appearance:none; appearance:none;
}
.wc-input::placeholder{ color:var(--ink-soft); opacity:.75 }
.wc-input:focus{
  outline:none; border-color:var(--flame);
  box-shadow:0 0 0 3px rgb(var(--flame-rgb) / .18);
}
.wc-gender-q{ margin:16px 0 7px }
.wc-genders{ display:flex; gap:7px; justify-content:center }
.wc-gender{
  flex:1;
  padding:10px 6px;
  border:1.5px solid rgb(var(--sand-rgb) / .42);
  border-radius:var(--r-md);
  background:var(--sand-wash);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  font:var(--t-btn); letter-spacing:var(--t-btn-ls);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.wc-gender.is-on{ border-color:var(--flame); border-width:2.5px; padding:9px 5px }

.wc-acts{ display:flex; flex-direction:column; gap:7px; margin:18px 0 0 }
.wc-btn{
  padding:12px 16px;
  border:1.5px solid rgb(var(--sand-rgb) / .45);
  border-radius:var(--r-md);
  background:var(--sand-wash);
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  color:var(--fg);
  font:var(--t-btn); letter-spacing:var(--t-btn-ls);
  cursor:pointer;
  -webkit-tap-highlight-color:transparent;
}
.wc-btn.is-primary{
  background:var(--flame); border-color:var(--flame);
  --fg:var(--on-flame);
  color:var(--fg);
}
/* Ghost sits on the CARD, not on a light chip, so it keeps the card's ink
   rather than remapping to dark. */
.wc-btn.is-ghost{
  background:none; border-color:transparent;
  --fg:var(--on-dark); --fg-soft:var(--on-dark);
  color:var(--fg); opacity:.72;
}

/* Three dots, not a progress bar. The honest message is "this is short". */
.wc-dots{ display:flex; gap:6px; justify-content:center; margin:16px 0 0 }
.wc-dot{
  width:6px; height:6px; border-radius:999px;
  background:rgb(var(--sand-rgb) / .45);
}
.wc-dot.is-on{ background:var(--flame) }

/* The neutral first line: the same question in every interface language,
   because she cannot read any single one of them yet. */
.wc-neutral{
  margin:0 0 18px;
  font:var(--t-sub); letter-spacing:var(--t-sub-ls);
  color:var(--fg);
  line-height:1.6;
}
/* Flag then name, so the eye finds the flag first and the word confirms it. */
.wc-flag{ font-size:1.35rem; line-height:1; margin-right:10px }
.wc-lang{ display:flex; align-items:center; justify-content:center }
.wc-lang-t{ font:var(--t-card); letter-spacing:var(--t-card-ls) }

/* v415: Song view mode must remain readable when selected. Some themes
   recolor the generic selected-pill text close to the selected fill. Keep
   this local to the songbook instead of changing every mode-toggle site. */
.sg-view-toggle button[aria-pressed="true"]{
  color:#221f1b !important;
}


/* ==============================================================
   v421 — NAV RAIL CLEARANCE + GRAMMAR LEGIBILITY
   ============================================================== */

/* The brand already moves 52px right to clear the fixed navigation rail.
   The control row below it must start on the same vertical line. This moves
   the crystal purse (and the controls that travel with it) by the identical
   amount instead of leaving the purse under ↑ / Back on a phone. */
@media (max-width:979px){
  .topbar-controls{ margin-left:52px }
}
@media (min-width:980px){
  .topbar-controls{ margin-left:0 }
}

/* Grammar is information-dense and is read for long stretches. The old
   sizes were acceptable for a game HUD but too faint/small for reference
   material on a phone, especially the conjugation tables shown inside the
   dark grammar card. These rules strengthen hierarchy without changing the
   lesson/game logic or the theme palette. */
.gr-card .gr-lede{
  font-size:clamp(1.04rem,3.5vw,1.16rem);
  line-height:1.62;
  color:var(--ink-on-dark);
}
.gr-card .gr-note{
  font-size:.98rem;
  line-height:1.58;
  color:var(--soft-on-dark);
}
.gr-card .gr-group{
  font-size:clamp(1.14rem,3.9vw,1.32rem);
  line-height:1.3;
}
.gr-card .gr-preps{ font-size:clamp(1.08rem,3.8vw,1.28rem) }

/* Opaque light islands inside the dark grammar card get explicit dark ink.
   Do not let them inherit the surrounding card's pale text variables. */
.gr-card .cg-table,
.gr-card .gr-stat,
.gr-card .gr-pair,
.gr-card .gr-past-row,
.gr-card .gr-mark,
.gr-card .gr-fut-now,
.gr-card .ww-pair,
.gr-card .gr-sent:hover{
  --fg:var(--ink);
  --fg-soft:var(--ink-soft);
  --fg-accent:var(--ink);
}

/* Conjugation tables are the worst offender in the physical phone test:
   make the German forms primary, pronouns clearly secondary, and remove
   opacity-on-secondary-text (soft colour + opacity was being applied twice). */
.gr-card .cg-table{
  padding:14px 15px;
  border-color:rgb(var(--mauve-rgb) / .48);
}
.gr-card .cg-table-head{ padding-bottom:10px; margin-bottom:8px }
.gr-card .cg-table-verb{ font-size:1.18rem; font-weight:600 }
.gr-card .cg-table-kind{ font-size:.88rem }
.gr-card .cg-row{
  grid-template-columns:5.4rem 1fr auto;
  gap:10px;
  padding:7px 8px;
  line-height:1.35;
}
.gr-card .cg-pron{ font-size:1rem; color:var(--ink-soft) }
.gr-card .cg-form{ font-size:1.08rem; font-weight:600; color:var(--ink) }
.gr-card .cg-gloss{ font-size:.94rem; color:var(--ink-soft); opacity:1 }
.gr-card .cg-table-hint{ font-size:.9rem; color:var(--ink-soft); opacity:1 }

/* The other light grammar rows carry the same readable floor. */
.gr-card .gr-pair{ padding:11px 12px; gap:14px }
.gr-card .gr-pair-de{ font-size:1.06rem; line-height:1.35 }
.gr-card .gr-pair-ru{ font-size:.98rem; line-height:1.4 }
.gr-card .gr-past-inf,
.gr-card .gr-past-pt,
.gr-card .gr-past-pp,
.gr-card .gr-past-de,
.gr-card .gr-past-tr,
.gr-card .gr-mark-de,
.gr-card .gr-mark-tr,
.gr-card .gr-fut-de,
.gr-card .gr-fut-tr{ line-height:1.4 }

@media (max-width:430px){
  /* Three columns still fit, but the gloss must wrap rather than becoming
     tiny. The first column is kept stable so the paradigm scans vertically. */
  .gr-card .cg-row{ grid-template-columns:4.8rem minmax(0,1fr); }
  .gr-card .cg-gloss{ grid-column:2; text-align:left; }
}

/* ==============================================================
   v422 — DARK GRAMMAR CONSISTENCY
   ============================================================== */
/* Regular-verbs tables are the dark-theme reference: the container owns
   the themed surface and each row is transparent. Wo/Wohin used <button>
   elements for its two example halves but never cleared Safari's native
   button face, so Midnight/Ember/Pine drew pale system buttons with the
   grammar card's pale ink on top. Keep the exact Regular Verbs model:
   themed dark parent, transparent interactive rows, inherited readable ink. */
.gr-card .ww-side{
  background:transparent;
  color:inherit;
  font:inherit;
  -webkit-appearance:none;
  appearance:none;
}


/* v428 — TOUR CONTROLS ARE A ROW, ALWAYS.
   .bt-acts is also used for ordinary full-width choice buttons, so older
   mobile/theme rules legitimately stack it. Once butler.js marks the actual
   tour footer, however, X / Pause / Continue are compact controls and must
   never inherit that vertical layout. */
.bt-acts.bt-tour-actions{
  display:flex !important;
  flex-direction:row !important;
  flex-wrap:nowrap !important;
  align-items:center !important;
  justify-content:flex-start !important;
  gap:11px !important;
  width:100% !important;
}
.bt-tour-actions .bt-tour-icon{
  width:36px !important;
  height:36px !important;
  min-width:36px !important;
  max-width:36px !important;
  flex:0 0 36px !important;
  padding:0 !important;
}
.bt-tour-actions .bt-tour-next{ margin-left:auto !important; }

/* v428 — WORD MATCH BACK MUST REMAIN A PILL.
   Word Match deliberately moves its normal fixed Back control to the bottom
   with .wm-exit. During the tour the highlight is visual only: explicitly
   preserve the intrinsic pill geometry so no combination of top/bottom or
   flex sizing can stretch it into the full-height slab seen on iPhone. */
.backlink.wm-exit.bt-lit{
  top:auto !important;
  right:auto !important;
  width:auto !important;
  height:auto !important;
  min-width:0 !important;
  min-height:0 !important;
  max-width:none !important;
  max-height:none !important;
  align-self:auto !important;
}


/* === JUKEBOX v2 (filter, sort, playlists, lyrics) — theme-safe === */

/* Jukebox language filter + sort controls. Add to css/style.css. Steven, Sep. */

.jb-filter{
  display:flex; flex-wrap:wrap; gap:8px;
  margin:4px 0 12px;
}
.jb-chip{
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  padding:7px 14px;
  border-radius:999px;
  border:1.5px solid rgb(var(--sand-rgb) / .18);
  background:var(--paper);
  color:var(--fg);
  font:600 .85rem/1 var(--ff-ui);
  cursor:pointer;
  transition:background .12s, border-color .12s, color .12s;
}
.jb-chip:hover{ border-color:var(--flame); }
.jb-chip.is-on{
  background:var(--flame);
  border-color:var(--flame);
  color:var(--ink-on-accent, #fff);
}

.jb-sort{
  display:flex; gap:8px;
  margin:0 0 12px;
  align-items:center;
}
.jb-sortbtn{
  padding:6px 12px;
  border-radius:var(--r-sm);
  border:1.5px solid rgb(var(--sand-rgb) / .15);
  background:transparent;
  color:var(--fg-soft);
  font:600 .8rem/1 var(--ff-ui);
  cursor:pointer;
}
.jb-sortbtn:hover{ color:var(--fg); border-color:var(--flame); }
.jb-sortbtn.is-on{
  color:var(--flame);
  border-color:var(--flame);
  background:rgb(var(--flame-rgb) / .08);
}


/* Jukebox: playlists + full-screen lyrics. Add to css/style.css. Steven, Sep. */

/* ---- loaded playlist name + up-next on the now-playing card ---- */
.jb-from{ margin:0 0 4px; font:600 .8rem/1.2 var(--ff-ui); color:var(--flame); }
.jb-next{ margin:6px 0 0; font:400 .82rem/1.3 var(--ff-ui); color:var(--fg-soft); }
.jb-lyr-open{
  margin-top:12px; padding:8px 14px; border-radius:var(--r-sm);
  border:1.5px solid var(--flame); background:transparent; color:var(--flame);
  font:600 .85rem/1 var(--ff-ui); cursor:pointer;
}
.jb-lyr-open:hover{ background:rgb(var(--flame-rgb) / .08); }

/* ---- playlist card ---- */
.jb-pl-save{ display:flex; gap:8px; margin:4px 0 12px; }
.jb-pl-name{
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  flex:1; padding:9px 12px; border-radius:var(--r-sm);
  border:1.5px solid rgb(var(--sand-rgb) / .18); background:var(--paper);
  color:var(--fg); font:400 .9rem/1 var(--ff-ui);
}
.jb-pl-name.is-bad{ border-color:var(--flame); }
.jb-pl-savebtn{ flex:0 0 auto; }
.jb-pl-empty{ margin:4px 0; color:var(--fg-soft); font:400 .85rem/1.4 var(--ff-ui); }
.jb-pl-list{ display:flex; flex-direction:column; gap:6px; }
.jb-pl-row{
  display:flex; gap:6px; align-items:center;
  padding:4px; border-radius:var(--r-sm);
}
.jb-pl-row.is-on{ background:rgb(var(--flame-rgb) / .10); }
.jb-pl-load{
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  flex:1; text-align:left; padding:9px 12px; border-radius:var(--r-sm);
  border:1px solid rgb(var(--sand-rgb) / .12); background:var(--paper);
  color:var(--fg); font:600 .9rem/1.2 var(--ff-ui); cursor:pointer;
}
.jb-pl-row.is-on .jb-pl-load{ border-color:var(--flame); color:var(--flame); }
.jb-pl-load:hover{ border-color:var(--flame); }
.jb-pl-act{
  flex:0 0 auto; width:36px; height:36px; border-radius:var(--r-sm);
  border:1px solid rgb(var(--sand-rgb) / .12); background:transparent;
  color:var(--fg-soft); font-size:.95rem; cursor:pointer;
}
.jb-pl-act:hover{ color:var(--flame); border-color:var(--flame); }

/* ---- full-screen lyrics ----
   Fills the screen with the current song's words in the chosen language;
   the X (top-right) closes it, and so does Escape. Music keeps playing
   underneath. Steven, Sep. */
.jb-lyr-overlay{
  position:fixed; inset:0; z-index:860;
  display:flex; flex-direction:column;
  background:var(--page);
}
.jb-lyr-bar{
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  /* Layout, top to bottom: TITLE spans the full width and may wrap; the X sits
     in the top-right corner on the title's first line; the LANGUAGE chips are
     on their own row BELOW the title. The X is pinned absolute (below) and a
     66px right lane is reserved on the title so the title never runs under it.
     Steven, Sep. */
  position:relative;
  display:flex; flex-direction:column; align-items:stretch; gap:10px;
  padding:14px 18px;
  border-bottom:1px solid rgb(var(--sand-rgb) / .12);
  flex:0 0 auto;
}
.jb-lyr-title{
  margin:0; font:600 1.05rem/1.3 var(--ff-display); color:var(--fg);
  padding-right:52px;   /* keep the title clear of the X on its first line */
}
.jb-lyr-langs{
  /* Own row below the title. A single horizontal row that scrolls sideways if
     there are many languages, so the bar stays compact at any count. */
  display:flex; gap:6px;
  overflow-x:auto; -webkit-overflow-scrolling:touch;
  max-width:100%; scrollbar-width:thin;
}
.jb-lyr-lang{ flex:0 0 auto; }
.jb-lyr-lang{
  padding:6px 12px; border-radius:999px;
  border:1.5px solid rgb(var(--sand-rgb) / .18); background:var(--paper);
  color:var(--fg); font:600 .8rem/1 var(--ff-ui); cursor:pointer;
}
.jb-lyr-lang.is-on{ background:var(--flame); border-color:var(--flame); color:var(--ink-on-accent, #fff); }
.jb-lyr-x{
  /* Pinned to the bar's top-right corner, immovable, above everything. */
  position:absolute; top:12px; right:14px; z-index:5;
  width:42px; height:42px; border-radius:50%;
  border:1px solid rgb(var(--sand-rgb) / .30);
  background:var(--paper);
  color:var(--fg); font-size:1.15rem; line-height:1; cursor:pointer;
  display:flex; align-items:center; justify-content:center;
}
.jb-lyr-x:hover{ background:rgb(var(--sand-rgb) / .12); }
.jb-lyr-body{
  --fg:var(--ink); --fg-soft:var(--ink-soft);
  flex:1; overflow-y:auto; padding:30px 22px 60px;
  text-align:center;
}
.jb-lyr-line{ margin:0 0 6px; font:400 1.3rem/1.55 var(--ff-display); color:var(--fg); }
.jb-lyr-gap{ height:22px; }


/* === FILTER (Topics) + TOC buttons restyle === */

/* Filter (Topics) + Table of Contents buttons — distinct from the jump-bar
   pills. Steven, Sep: TOC gets a ~20% accent wash; the filter button gets a
   different, dimmed theme colour; both nowrap so the pair fits one line. */

.filter-row{ flex-wrap:nowrap; }               /* keep the two on one line */
.filter-toggle{ white-space:nowrap; }

/* FILTER (Topics \u25be) — a clearly SOLID mauve wash so it reads as its own
   button: not the pale paper jump-pill, and not the flame-accent TOC. Steven,
   Sep: the earlier 14% wash was too faint and looked like the jump buttons.
   Bumped to a solid mauve tint with a full-mauve border, and the selector
   uses the .filter-row parent for enough weight to beat the base rule. */
.filter-row .filter-toggle:not(.toc-toggle){
  /* Lighter fill + a THICK mauve border. A heavy 35% fill made Topics read
     inconsistently because --mauve ranges from soft mauve to strong blue to
     brown across themes; a light wash with a bold border shows the theme
     accent as an outline, which reads well on every theme. Steven, Sep. */
  background:rgb(var(--mauve-rgb) / .16);
  border:2.5px solid var(--mauve);
  color:var(--ink);
}
.filter-row .filter-toggle:not(.toc-toggle):hover{
  background:rgb(var(--mauve-rgb) / .26);
  border-color:var(--mauve);
}
/* Topics selected/open — fill in with the accent so the pressed state is
   obvious and still distinct from the flame TOC. */
.filter-row .filter-toggle:not(.toc-toggle).has,
.filter-row .filter-toggle:not(.toc-toggle)[aria-expanded="true"]{
  background:var(--mauve);
  border:2.5px solid var(--mauve);
  color:var(--on-dark, #fff);
}

/* TABLE OF CONTENTS button (in the filter row) — ~20% accent wash. */
.toc-toggle{
  background:rgb(var(--flame-rgb) / .20);
  border:2.5px solid rgb(var(--flame-rgb) / .55);
  color:var(--fg);
}
.toc-toggle:hover{
  background:rgb(var(--flame-rgb) / .30);
  border-color:var(--flame);
}

/* TOC pill on the JUMP BAR (.jump.is-toc) — same ~20% accent so the two TOC
   entry points read as related. */
.jump.is-toc{
  background:rgb(var(--flame-rgb) / .20);
  border-color:rgb(var(--flame-rgb) / .45);
  color:var(--fg);
}
.jump.is-toc:hover{
  background:rgb(var(--flame-rgb) / .30);
  border-color:var(--flame);
}

/* Jukebox transport SVG icons */
.jb-ico{ display:inline-flex; align-items:center; justify-content:center; }
.jb-ico svg{ width:1.6em; height:1.6em; display:block; }
.jb-play.jb-ico svg{ width:1.9em; height:1.9em; }

/* Jukebox native audio bar (play counter + scrubber), below the SVG transport.
   Full width, matching the Songbook's native controls. Steven, Sep. */
.jb-native-wrap{ margin:12px 0 4px; }
.jb-native{ width:100%; height:40px; }

/* Lyrics-mode transport + skip row (under the title, above the languages). */
.jb-lyr-ctl{
  --fg:var(--ink);
  display:flex; align-items:center; gap:8px; flex-wrap:wrap;
}
.jb-lyr-btn{
  --fg:var(--ink);
  min-width:44px; height:44px; padding:0 10px;
  border-radius:999px;
  border:1.5px solid rgb(var(--sand-rgb) / .30);
  background:var(--paper); color:var(--fg);
  font:700 .8rem/1 var(--ff-ui); cursor:pointer;
  display:inline-flex; align-items:center; justify-content:center;
}
.jb-lyr-btn:hover{ border-color:var(--flame); }
.jb-lyr-btn.jb-ico svg{ width:1.5em; height:1.5em; }
.jb-lyr-play{ background:var(--flame); border-color:var(--flame); color:var(--ink-on-accent, #fff); }
.jb-lyr-play svg{ fill:var(--ink-on-accent, #fff); }
.jb-lyr-seek{ font-size:.85rem; }
