/* ============================================================================================
   GNOME GARDEN — the whole player-facing surface.

   What this file is for: every pixel that is NOT the canvas. The board, the bed, the gnome and
   the particles are drawn by render.js into one <canvas>; everything a person reads, presses or
   navigates is DOM, and this sheet dresses it.

   The rule it upholds: THIS SHEET HOLDS NO RULES. There is no board size here, no score, no
   move count, no unlock gate — those are sim numbers and ui.js reads them from sim.js
   (CLAUDE.md, "every number the UI shows is read from the sim"). What lives here is the
   *picture*: four kingdom palettes, a vertical budget, and the typography.

   Phone-first: designed at 360x780 portrait. The vertical budget is
   HUD 88 · board 336 · garden 168 · dock 64 (DESIGN §10), declared as custom properties in §1
   so the short-screen media queries in §18 can shrink it in ONE place. Desktop is the same
   screens, wider, and the extra height goes to the garden bed.

   ONE PLACE PER COMPONENT. Every layout rule for a component lives in its own numbered section
   and nowhere else. QUAL-244 was a CSS rule that HALF-won — equal specificity, earlier source
   order, `overflow-y` applied and `justify-content` silently dropped — and it was photographed
   twice, before and after the "fix", while the assertion suite stayed green throughout. A rule
   that must come later than another is commented with why.
   ============================================================================================ */


/* ============================================================================ §1  TOKENS
   Kingdom-independent. Everything here is a colour, a radius, a shadow or a size — never a
   game number. Nothing in this sheet uses a raw colour outside §1 and §2. */

:root {
  /* --- the cream world the kingdoms sit inside ------------------------------------------ */
  --cream:        #fff8ea;
  --cream-2:      #fdf0d9;
  --paper:        #fffdf6;   /* cards and panels: a half-step brighter than the page */
  /* THE INK RAMP, WITH ITS CONTRAST MEASURED, not eyeballed. Each ratio below is against
     --cream (#fff8ea), the palest ground any of them sits on; --paper is a shade higher still.
     4.5:1 is the floor for 14 px bold, which is this sheet's smallest type (--fs-min).
     --ink-faint began life at #a4937c and measured 2.82:1 — and it was carrying the HUD's
     "Moves"/"Score" labels, a locked path node's number and the empty-companion slot. All three
     are text somebody has to read. Faintness is now carried by WEIGHT and SIZE; the colour
     stays legible. */
  --ink:          #3d3226;   /* 11.81:1 — warm dark brown; pure black is cold, this garden is not */
  --ink-soft:     #6b5a45;   /*  6.26:1 — secondary text */
  --ink-faint:    #7d6a53;   /*  4.90:1 — tertiary text, and the floor */
  --line:         #e8d9be;
  --line-strong:  #d4bf9c;

  /* GOLD SPLITS IN TWO, and the split is a rule rather than a judgement call at each site:
     gold as a FILL or a BORDER is --gold / --gold-deep; gold as TEXT is always --gold-ink.
     --gold itself measures 1.97:1 on cream and --gold-deep 3.43:1 — fine behind ink, not fine
     as ink. The one that bit hardest: the ready Kingdom Charm drew a --gold-deep glyph on a
     --gold button, which is 1.74:1, i.e. an icon you cannot see on the one control the game
     most wants you to press. */
  --gold:         #e6a92b;   /* fill: the accent on anything active or earned */
  --gold-lite:    #ffd977;   /* fill: the light stop of every gold gradient */
  --gold-deep:    #b07d13;   /* border only */
  --gold-ink:     #8a6008;   /*  5.29:1 — gold that is text */

  /* --- the six charms, DESIGN §2.1. Used by HUD chips and objective pips. The canvas draws
         its own charms; these exist so a DOM chip and a drawn charm are the same colour. --- */
  --c-sun:   #ffcc4d;
  --c-dew:   #4fc3f7;
  --c-leaf:  #7cc46a;
  --c-bloom: #f78fb3;
  --c-berry: #a97bd6;
  --c-cap:   #ff7a5c;

  /* --- the dock's function tints, and the HUD's card surface -----------------------------
     THE HUES ARE LIFTED FROM THE CHARMS ABOVE RATHER THAN CHOSEN. Four identical cream cards
     make a player read four labels every time; four hues make the row learnable, and the thumb
     finds Garden by colour long before it finds it by the word. Taking those hues from the
     --c-* set keeps the dock inside the colour language the board already draws with instead of
     introducing a fifth palette, and it means each button is tinted by the thing it DOES:
     Shuffle takes leaf, Hint takes sun, Garden takes bloom.

     Every one is a pale wash carrying --ink (#3d3226) at 9:1 or better — far above the 4.5:1
     floor — so no label had to change size or weight to sit on one.

     The Kingdom Charm is deliberately absent. Gold in this row MEANS READY, and a button that is
     already gold cannot announce that it has become gold. It keeps the neutral card and earns
     its colour. --- */
  --tint-leaf:  #eaf6e3;      /* Shuffle  — --ink 9.9:1 */
  --tint-sun:   #fdf3d5;      /* Hint     — --ink 10.4:1 */
  --tint-bloom: #f6e9f0;      /* Garden   — --ink 10.0:1 */

  /* --- shape ---------------------------------------------------------------------------- */
  --r-sm: 10px;
  --r:    16px;
  --r-lg: 22px;
  --r-xl: 28px;
  --r-pill: 999px;

  /* 44px is the floor for anything a thumb aims at, everywhere, no exceptions. */
  --tap: 44px;

  /* Soft and warm: two stacked shadows, a tight contact shadow and a wide diffuse one. A single
     hard shadow reads as a sticker; this reads as an object resting on paper. */
  --shadow-sm: 0 1px 2px rgba(96, 74, 46, .16);
  --shadow:    0 2px 3px rgba(96, 74, 46, .14), 0 8px 20px rgba(96, 74, 46, .14);
  --shadow-lg: 0 4px 6px rgba(96, 74, 46, .16), 0 18px 44px rgba(96, 74, 46, .22);
  --inset-lite: inset 0 1px 0 rgba(255, 255, 255, .75);

  /* --- type. System fonts only: DESIGN §13 forbids a third-party font at runtime, and a
         webfont is also the slowest thing a phone game can do to itself. `ui-rounded` is real
         on Apple platforms; the rest are named because they cost nothing when absent and are
         lovely when present. --- */
  --font: ui-rounded, "SF Pro Rounded", "Nunito", "Quicksand", "Varela Round",
          "Trebuchet MS", "Segoe UI Variable Display", "Segoe UI", system-ui,
          -apple-system, sans-serif;
  --lh: 1.55;                 /* generous: this is a storybook, not a dashboard */
  --fs-min: 14px;             /* THE FLOOR. Nothing in this sheet is smaller, including the
                                 metric labels and the dock captions — a caption a thumb is
                                 aimed at is text somebody has to read at arm's length on a bus.
                                 Where 14px stopped fitting, the LAYOUT gave way (see §19), not
                                 the type. */

  --focus: #1c5fa8;           /* a cold blue on purpose — it must never read as decoration */

  /* --- the vertical budget, DESIGN §10 --------------------------------------------------- */
  --h-hud:    88px;
  --h-board: 336px;
  --h-garden:168px;
  --h-dock:   64px;
  --app-max: 720px;

  /* THE VIEWPORT HEIGHT, and it has to be dvh. `100vh` and `height:100%` both resolve to the
     LARGE viewport on a phone — the height with the URL bar hidden — so a layout sized by them
     runs underneath the browser chrome and its bottom rows are unreachable while the page
     insists everything fits. That is exactly how a bottom action bar goes missing on a real
     handset. `dvh` tracks the viewport as the chrome comes and goes; the vh line below stays as
     the fallback for anything that lacks it. */
  --vh: 100vh;
}

@supports (height: 100dvh) {
  :root { --vh: 100dvh; }
}


/* ======================================================================= §2  KINGDOMS
   Four palettes, DESIGN §1, as custom-property sets keyed on `[data-kingdom]`. ui.js sets that
   one attribute on #app and the whole surface re-dresses.

   The selector is the BARE ATTRIBUTE and not `#app[data-kingdom]`, which it was first, because
   the kingdom chooser has to show all four palettes AT ONCE: each card carries its own
   `data-kingdom` and paints in that kingdom's colours while the app around it stays in the
   player's. Scoping the sets to #app made every card render in the active palette and the
   chooser show four identical cards — a screen whose entire job is comparison.

   Each kingdom declares its four canonical colours under stable ROLE names, so a component can
   ask for "the deep one" without knowing which kingdom it is in:

     --k-deep   the anchoring colour   --k-mid   the saturated one
     --k-lite   the light tint         --k-air   the palest / the accent

   Plus six DERIVED tones that are not in DESIGN §1 and must not pretend to be. They exist
   because a palette is a mood and CONTRAST IS A FLOOR. Every one is a tint or a shade of the
   kingdom's own four, and every ratio below was measured, not judged:

     --k-ink    the kingdom colour AS TEXT. Ten rules in this sheet colour a word or a glyph
                with the kingdom — the logo, the grove name, every sheet title, the moves
                readout, the dock icons, the fieldset legends. Pointed at --k-deep they measured
                4.75 (mosswick), 2.01 (sunmeadow), 2.27 (frostpetal) and 6.82 (emberbell) on
                cream: two of the four kingdoms rendered their own headings unreadable, and it
                was invisible until somebody put a number on it. --k-ink lands all four in a
                5.4–8.4 band.
     --k-bar-1/2 + --k-bar-ink   the top bar and the text that sits on it (5.0–7.2:1). --k-bar-2
                is `var(--k-deep)` rather than a repeated hex, because it IS the kingdom colour
                and two copies of one value drift.
     --k-sky-1/2  the page gradient stops.   --k-glow  the halo, never text. */

[data-kingdom="mosswick"] {
  --k-deep: #2f7d55;  /* emerald */
  --k-mid:  #3fb59a;  /* teal    */
  --k-lite: #7cc46a;  /* moss    */
  --k-air:  #f4efe0;  /* cream   */
  --k-ink: #28684a;             /* 6.27:1 on cream — the kingdom as TEXT */
  --k-bar-1: #3d9268;
  --k-bar-2: var(--k-deep);
  --k-bar-ink: #ffffff;
  --k-sky-1: #e3f2e2;
  --k-sky-2: #f7f2e4;
  --k-glow: rgba(47, 125, 85, .30);
}

[data-kingdom="sunmeadow"] {
  --k-deep: #f2a03d;  /* amber */
  --k-mid:  #e8b64c;  /* honey */
  --k-lite: #ffd98a;  /* wheat */
  --k-air:  #8fd3f4;  /* sky   */
  --k-ink: #94590c;             /* 5.38:1 on cream — the kingdom as TEXT */
  --k-bar-1: #ffbe6b;
  --k-bar-2: var(--k-deep);
  --k-bar-ink: #57340a;      /* 6.1:1 on the amber — white would be 2.2:1 and unreadable */
  --k-sky-1: #d6eefb;
  --k-sky-2: #fff2d6;
  --k-glow: rgba(232, 182, 76, .34);
}

[data-kingdom="frostpetal"] {
  --k-deep: #8fa8d8;  /* periwinkle */
  --k-mid:  #bfe6f7;  /* ice        */
  --k-lite: #dfe7ee;  /* silver     */
  --k-air:  #ffffff;  /* white      */
  --k-ink: #41547f;             /* 7.10:1 on cream — the kingdom as TEXT */
  --k-bar-1: #a6bce6;
  --k-bar-2: var(--k-deep);
  --k-bar-ink: #1f2c47;      /* 7.0:1 on the periwinkle */
  --k-sky-1: #eaf5fd;
  --k-sky-2: #fbfcfe;
  --k-glow: rgba(143, 168, 216, .34);
}

[data-kingdom="emberbell"] {
  --k-deep: #6b4b7a;  /* plum  */
  --k-mid:  #ff8f6b;  /* coral */
  --k-lite: #f4a6c0;  /* rose  */
  --k-air:  #ffb347;  /* ember */
  --k-ink: #5c3f69;             /* 8.38:1 on cream — the kingdom as TEXT */
  --k-bar-1: #7f5a91;
  --k-bar-2: var(--k-deep);
  --k-bar-ink: #ffffff;
  --k-sky-1: #fbe4ec;
  --k-sky-2: #fff1de;
  --k-glow: rgba(107, 75, 122, .30);
}

/* A charm chip anywhere can carry data-charm and get its colour. */
[data-charm="sun"]   { --charm: var(--c-sun); }
[data-charm="dew"]   { --charm: var(--c-dew); }
[data-charm="leaf"]  { --charm: var(--c-leaf); }
[data-charm="bloom"] { --charm: var(--c-bloom); }
[data-charm="berry"] { --charm: var(--c-berry); }
[data-charm="cap"]   { --charm: var(--c-cap); }
[data-charm="any"]   { --charm: var(--gold); }


/* ==================================================================== §3  DOCUMENT RESET */

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

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

body {
  /* NEVER a horizontal scrollbar. The companion half of this promise is `min-width: 0` on
     every flex child that holds text (see .min0 in §16) — without it a long species name blows
     its track out and the page slides sideways, which on a phone reads as the game being
     broken. */
  overflow-x: hidden;
  overflow-y: hidden;
  overscroll-behavior: none;
  background: var(--cream);
  color: var(--ink);
  font-family: var(--font);
  font-size: 16px;
  line-height: var(--lh);
  -webkit-text-size-adjust: 100%;
  -webkit-tap-highlight-color: transparent;
}

/* THE ONE `!important` IN THIS SHEET, and it is deliberate.
   `hidden` is the source of truth for which screen is up. It is a *browser* rule
   (`[hidden] { display: none }`) and any author rule with a `display` beats it on origin, not
   on specificity — which is how a screen marked hidden stays on top of the one you wanted.
   Every display rule below is written as `:not([hidden])` for that reason; this line is the
   backstop for anything added later. */
[hidden] { display: none !important; }

img, canvas, svg { max-width: 100%; }

::selection { background: var(--gold-lite); color: var(--ink); }


/* ===================================================================== §4  TYPOGRAPHY */

h1, h2, h3 {
  margin: 0;
  font-weight: 800;
  letter-spacing: .2px;
  line-height: 1.2;
}
h2 { font-size: 20px; }
h3 { font-size: 17px; }
p  { margin: 0; }

.lede {
  margin: 2px 0 12px;
  color: var(--ink-soft);
  font-size: 15px;
}
.hint {
  margin: 4px 0 2px;
  color: var(--ink-soft);
  font-size: 14px;       /* the floor. Nothing in this sheet goes under 14px */
  line-height: 1.5;
}

/* Screen-reader-only, but still focusable and still measured — `display:none` would take it
   out of the accessibility tree entirely, which defeats the point. */
.sr {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

.noscript {
  max-width: 34em;
  margin: 24px auto;
  padding: 0 24px;
  font-family: var(--font);
  font-size: 16px;
  color: var(--ink);
}


/* ================================================================ §5  APP + SCREENS
   The ONE place a screen's display is decided. Nothing else in this sheet sets `display` on
   `.screen`. */

.app {
  position: relative;
  width: 100%;
  height: var(--vh);
  max-width: var(--app-max);
  margin: 0 auto;
  overflow: hidden;
  isolation: isolate;
  /* The sky. A gentle vertical gradient, per kingdom, drawn in CSS so the game has its
     atmosphere before a single byte of generated art has landed (the G5 contract, seen from
     the shell's side). */
  background:
    radial-gradient(140% 70% at 50% -10%, rgba(255, 255, 255, .85) 0%, rgba(255, 255, 255, 0) 60%),
    linear-gradient(180deg, var(--k-sky-1) 0%, var(--k-sky-2) 62%, var(--cream) 100%);
  box-shadow: 0 0 60px rgba(96, 74, 46, .18);
}

.screen {
  position: absolute;
  inset: 0;
  display: none;
  flex-direction: column;
  min-width: 0;
  padding-top: env(safe-area-inset-top, 0px);
}
.screen:not([hidden]) { display: flex; }

/* A drifting light wash behind the title. Decoration only; it holds no information, so §17
   settles it without taking anything away. */
.sky {
  position: absolute;
  inset: -20% -20% auto -20%;
  height: 70%;
  background:
    radial-gradient(46% 46% at 22% 30%, rgba(255, 255, 255, .9), rgba(255, 255, 255, 0) 70%),
    radial-gradient(38% 38% at 78% 18%, var(--k-glow), rgba(255, 255, 255, 0) 72%);
  animation: drift 26s ease-in-out infinite alternate;
  pointer-events: none;
}


/* ============================================================ §6  BARS, PANES, CHIPS */

.topbar {
  display: flex;
  align-items: center;
  gap: 10px;
  flex: 0 0 auto;
  min-height: 56px;
  padding: 8px 12px;
  color: var(--k-bar-ink);
  background: linear-gradient(180deg, var(--k-bar-1) 0%, var(--k-bar-2) 100%);
  box-shadow: 0 2px 0 rgba(96, 74, 46, .12), var(--shadow-sm);
}
.topbar h2 {
  flex: 1 1 auto;
  min-width: 0;              /* §3: the text child that would otherwise widen the row */
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  text-shadow: 0 1px 0 rgba(0, 0, 0, .14);
}
.topbar-chip {
  flex: 0 0 auto;
  padding: 5px 12px;
  font-size: 14px;
  font-weight: 800;
  color: var(--ink);
  background: rgba(255, 255, 255, .82);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-sm);
}

.pane {
  flex: 1 1 auto;
  min-height: 0;             /* the flex-child rule that lets an inner scroller actually scroll */
  padding: 14px 14px 18px;
}
.pane-scroll {
  overflow-y: auto;
  overflow-x: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: thin;
  padding-bottom: calc(22px + env(safe-area-inset-bottom, 0px));
}

.bottombar {
  flex: 0 0 auto;
  padding: 10px 14px calc(10px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(180deg, rgba(255, 248, 234, 0) 0%, var(--cream) 40%);
}
.bottombar-split {
  display: flex;
  gap: 10px;
}
.bottombar-split .btn { flex: 1 1 0; min-width: 0; }

/* A row of filter chips: families, wardrobe categories, groves. Scrolls horizontally INSIDE
   itself — that is the one place sideways movement is allowed, and the page never joins in. */
.chips {
  display: flex;
  gap: 8px;
  flex: 0 0 auto;
  padding: 10px 14px;
  overflow-x: auto;
  overflow-y: hidden;
  scrollbar-width: none;
}
.chips::-webkit-scrollbar { display: none; }
.chip {
  flex: 0 0 auto;
  /* The 44 px floor applies to a filter chip exactly as it does to a big button: a chip is a
     target a thumb aims at while scrolling the row sideways, which is the hardest kind to hit.
     It was 38 px until a viewport audit measured it. */
  min-height: var(--tap);
  padding: 9px 15px;
  font: inherit;
  font-size: 14px;
  font-weight: 800;
  color: var(--ink-soft);
  background: var(--paper);
  border: 2px solid var(--line);
  border-radius: var(--r-pill);
  cursor: pointer;
  touch-action: manipulation;
  transition: color .14s ease, border-color .14s ease, background-color .14s ease;
}
.chip[aria-selected="true"], .chip[aria-pressed="true"] {
  color: var(--ink);
  background: linear-gradient(180deg, var(--gold-lite), var(--gold));
  border-color: var(--gold-deep);
}


/* ==================================================================== §7  CONTROLS */

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  min-height: var(--tap);
  min-width: var(--tap);
  padding: 10px 18px;
  font: inherit;
  font-size: 16px;
  font-weight: 800;
  color: var(--ink);
  background: linear-gradient(180deg, #ffffff 0%, var(--cream-2) 100%);
  border: 2px solid var(--line-strong);
  border-radius: var(--r);
  box-shadow: var(--shadow), var(--inset-lite);
  cursor: pointer;
  touch-action: manipulation;
  transition: transform .09s ease, box-shadow .09s ease, filter .14s ease;
}
.btn:hover:not(:disabled) { filter: brightness(1.03); }
.btn:active:not(:disabled) { transform: translateY(2px); box-shadow: var(--shadow-sm); }
.btn:disabled { opacity: .5; cursor: default; box-shadow: none; }

.btn-big {
  width: 100%;
  padding: 14px 18px;
  font-size: 19px;
  border-radius: var(--r-lg);
}
.btn-primary {
  color: var(--ink);
  background: linear-gradient(180deg, var(--gold-lite) 0%, var(--gold) 100%);
  border-color: var(--gold-deep);
}
.btn-ghost {
  font-weight: 700;
  background: rgba(255, 255, 255, .62);
  border-color: var(--line);
  box-shadow: var(--shadow-sm);
}
.btn-quiet {
  font-weight: 700;
  color: var(--ink-soft);
  background: transparent;
  border-color: transparent;
  box-shadow: none;
}
.btn-back {
  flex: 0 0 auto;
  padding: 0 14px;
  font-size: 26px;
  line-height: 1;
  background: rgba(255, 255, 255, .9);
  border-color: rgba(255, 255, 255, .5);
}

/* One focus ring for the whole game, and it is loud. A focus indicator that blends in is a
   focus indicator nobody can follow. */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
  outline: 3px solid var(--focus);
  outline-offset: 2px;
  border-radius: var(--r-sm);
}


/* ======================================================================= §8  TITLE */

/* `overflow: hidden` because .sky (§5) is deliberately drawn 20% wider than the screen so its
   wash has somewhere to drift from. Without this the bleed inflates #app's scrollWidth by ~86 px
   — clipped by #app and therefore invisible, but it is a horizontal overflow sitting one
   `overflow: visible` away from becoming a real sideways scroll. Contained where it is made. */
.screen-title { justify-content: center; overflow: hidden; }

.title-inner {
  position: relative;
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: 14px;
  padding: 20px 22px calc(20px + env(safe-area-inset-bottom, 0px));
}

.logo {
  display: flex;
  flex-direction: column;
  align-items: center;
  font-size: 46px;
  font-weight: 900;
  line-height: .96;
  letter-spacing: -.5px;
  color: var(--k-ink);
  text-shadow: 0 2px 0 rgba(255, 255, 255, .9), 0 8px 22px var(--k-glow);
}
.logo-line-1 {
  font-size: 30px;
  letter-spacing: 7px;
  text-indent: 7px;            /* letter-spacing pads the RIGHT of the last glyph; this re-centres it */
  text-transform: uppercase;
  opacity: .94;
}
.logo-line-2 {
  color: var(--gold-ink);
  font-size: 54px;
  letter-spacing: 1px;
}
.tagline {
  margin: -4px 0 6px;
  text-align: center;
  font-size: 15px;
  color: var(--ink-soft);
}

.menu { display: flex; flex-direction: column; gap: 10px; }
.menu-row { display: flex; gap: 8px; }
.menu-row .btn { flex: 1 1 0; min-width: 0; padding-inline: 8px; font-size: 15px; }

.title-foot {
  display: flex;
  flex-direction: column;
  gap: 10px;
  margin-top: 6px;
}
.title-foot-btns { display: flex; gap: 8px; }
.title-foot-btns .btn { flex: 1 1 0; min-width: 0; }

.thumb-chip {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  padding: 8px 14px;
  background: rgba(255, 255, 255, .78);
  border: 2px solid var(--line);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-sm);
}
.thumb-leaf { color: var(--k-ink); font-size: 18px; }
.thumb-bar {
  flex: 1 1 auto;
  min-width: 0;
  height: 10px;
  background: var(--cream-2);
  border-radius: var(--r-pill);
  overflow: hidden;
  box-shadow: inset 0 1px 2px rgba(96, 74, 46, .2);
}
.thumb-fill {
  display: block;
  width: 0;
  height: 100%;
  background: linear-gradient(90deg, var(--k-mid), var(--k-lite));
  border-radius: var(--r-pill);
  /* Meters overshoot by 6% and settle back (DESIGN §9.3); the spring lives in this easing
     curve, so ui.js only ever writes the true width. */
  transition: width .5s cubic-bezier(.28, 1.5, .5, 1);
}
.thumb-text { flex: 0 0 auto; font-size: 14px; font-weight: 800; color: var(--ink-soft); }


/* ============================================================== §9  KINGDOM SELECT */

.kingdom-list { display: flex; flex-direction: column; gap: 12px; }

.kingdom-card {
  display: grid;
  grid-template-columns: 56px 1fr;
  gap: 4px 14px;
  align-items: center;
  min-height: var(--tap);
  padding: 14px;
  background: var(--paper);
  border: 2px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  transition: border-color .16s ease, box-shadow .16s ease, transform .1s ease;
}
.kingdom-card:active { transform: translateY(1px); }
/* The radio itself is the state; it is placed off-screen rather than removed so keyboard focus
   and screen-reader semantics stay exactly as the browser built them. */
.kingdom-card input {
  position: absolute;
  width: 1px; height: 1px;
  opacity: 0;
}
.kingdom-card:focus-within { outline: 3px solid var(--focus); outline-offset: 2px; }
/* Gold on the chosen kingdom. Later than the base rule on purpose — it is a state override and
   must win over the resting border. */
.kingdom-card:has(input:checked) {
  border-color: var(--gold-deep);
  box-shadow: 0 0 0 3px var(--gold-lite), var(--shadow);
}
.kingdom-card[data-locked="true"] { opacity: .62; }

/* Each card carries its own data-kingdom (§2), so the crest paints in the kingdom it advertises
   rather than the one the player is currently in. The ground is the bar gradient because that is
   the pair `--k-bar-ink` was chosen against; `--k-air` — the fourth canonical colour, which is
   white for Frostpetal and sky for Sunmeadow — rings it, where being pale cannot cost contrast. */
.kingdom-crest {
  grid-row: 1 / span 3;
  width: 56px; height: 56px;
  display: grid;
  place-items: center;
  font-size: 26px;
  color: var(--k-bar-ink);
  background: linear-gradient(180deg, var(--k-bar-1), var(--k-bar-2));
  border-radius: 50%;
  box-shadow: 0 0 0 3px var(--k-air), var(--shadow-sm);
}
.kingdom-name { grid-column: 2; min-width: 0; font-size: 18px; font-weight: 900; }
.kingdom-trait,
.kingdom-charm { grid-column: 2; min-width: 0; font-size: 14px; color: var(--ink-soft); }
.kingdom-charm strong { color: var(--ink); }


/* ==================================================================== §10  PATH MAP */

/* The grove strip IS a .chips row (§6) with one extra rule; it does not restate the row. */
.grove-strip { border-bottom: 2px solid rgba(255, 255, 255, .55); }

/* THE CHIP'S ICON. `ui.js` derives which one from the grove's own levels and sets the image; this
   rule owns the box only, so a missing key is a chip with just its name — which is what every
   chip was before this existed.

   `inline-flex` on the chip and a fixed 18 px box rather than a background on the chip itself:
   the label must still centre against the icon, and a background-position would drift as soon as
   two grove names differed in length. */
.grove-strip .chip { display: inline-flex; align-items: center; gap: 6px; }
.chip-emblem {
  flex: 0 0 auto;
  width: 18px;
  height: 18px;
  background: center / contain no-repeat;
}
/* A chip for a grove that is not open yet keeps its icon, dimmed to match the text beside it —
   the same reasoning as the locked path tile: what is coming is the reason to keep going. */
.grove-strip .chip:disabled .chip-emblem { opacity: .4; filter: grayscale(.5); }

.grove-name { margin-bottom: 2px; color: var(--k-ink); }
.grove-note { margin-bottom: 12px; color: var(--ink-soft); font-size: 14px; }

.path-nodes {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
  gap: 10px;
  margin: 0;
  padding: 0;
  list-style: none;
}
.path-node {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 2px;
  width: 100%;
  min-height: 78px;
  padding: 8px 6px;
  font: inherit;
  font-weight: 900;
  color: var(--ink);
  background: linear-gradient(180deg, var(--paper), var(--cream-2));
  border: 2px solid var(--line-strong);
  border-radius: var(--r);
  box-shadow: var(--shadow-sm), var(--inset-lite);
  cursor: pointer;
  touch-action: manipulation;
}

/* THE TILE IS THREE THINGS STACKED, and the order is the order the eye wants them: which level,
   what it asks for, how it went. The mock put the number top-left and the stars along the
   bottom, which leaves the middle for the picture — so the tile is a small position:relative
   frame and the three take corners of it rather than sharing a flex column.

   The number moves to a CORNER BADGE rather than the middle. Centred, it competed with the
   emblem for the same 40 px and neither one read; in the corner it is a label on a picture,
   which is what it always was. */
.path-node { position: relative; justify-content: flex-end; min-height: 96px; padding: 8px 6px 7px; }

.path-node .n {
  position: absolute;
  top: 5px;
  left: 7px;
  font-size: 15px;
  line-height: 1;
  color: var(--ink-soft);
  font-variant-numeric: tabular-nums;
}

/* THE OBJECTIVE EMBLEM. `ui.js` sets the background-image from `path_<op>`; this rule owns
   nothing but the box, so a missing key is a tile with no emblem rather than a broken one.

   `contain`, never `cover`: these are cut-out emblems with their own transparent margin, and
   cropping one to fill a square eats the ring of leaves that makes it read as a path tile. */
.node-emblem {
  display: block;
  width: 100%;
  height: 46px;
  margin: 10px 0 2px;
  background: center / contain no-repeat;
}

.path-node .pips { font-size: var(--fs-min); color: var(--gold-ink); letter-spacing: 1px; }

/* LOCKED KEEPS ITS EMBLEM, AT HALF. Hiding it would make the unplayed half of a grove a grid of
   blank cards, and knowing what is coming is the reason to keep going — but it must not read as
   available, so the whole tile desaturates rather than the picture disappearing. */
.path-node[data-state="locked"] { color: var(--ink-faint); background: var(--cream-2); cursor: default; }
.path-node[data-state="locked"] .node-emblem { opacity: .38; filter: grayscale(.55); }

.path-node[data-state="next"] {
  border-color: var(--gold-deep);
  box-shadow: 0 0 0 3px var(--gold-lite), var(--shadow);
}

/* THE GROVE NAME, WITH THE MOCK'S SPRIGS. Two rules and no new asset: a pair of flexible gold
   hairlines either side of the heading, which is what the painted leaf sprigs were doing
   compositionally. Drawn from --gold-deep at low alpha so it frames the name without competing
   with the gold that MEANS something everywhere else in this file. */
.grove-name {
  display: flex;
  align-items: center;
  gap: 10px;
  margin-bottom: 2px;
  color: var(--k-ink);
}
.grove-name::before,
.grove-name::after {
  content: "";
  flex: 1;
  height: 2px;
  border-radius: 2px;
  background: linear-gradient(90deg, transparent, rgba(176, 125, 19, .38), transparent);
}
.grove-note { text-align: center; }



/* ======================================================================= §11  LEVEL
   The vertical budget, and the only place it is spent. HUD and dock are fixed; the field takes
   what is left. §18 re-sizes the two custom properties on short screens rather than adding a
   second copy of this layout. */

/* `.screen-level` gets its `display: flex` and its column direction from §5 and from nowhere
   else. Restating them here would be two equal-specificity declarations of one layout, which is
   precisely the shape QUAL-244 took. The one thing it adds is below. */

/* The one screen that must never scroll. The canvas sizes itself to its box, so a scrollbar
   here would mean the renderer is happily drawing a board at a size the player cannot see all
   of — worse than clipping, because it looks like it fits. §19 shrinks the budget on short
   viewports instead, which is the fix; this is the guard that says so out loud. */
.screen-level { overflow: hidden; }

.hud {
  flex: 0 0 auto;
  min-height: var(--h-hud);
  display: flex;
  flex-direction: column;
  gap: 4px;
  /* 6px of bottom padding, not 8px, and the two pixels are not cosmetic — they pay for the
     objective chip's progress track above.
     MEASURED AT THREE VIEWPORTS, because 360×780 has ~107 px of headroom and says yes to
     anything. 360×640 does not: there `.field-wrap` sits exactly ON its `min-height`
     (--h-board + --h-garden = 468 px), so the HUD has no slack at all and the track's first
     draft overflowed the viewport by 1.4 px. `.screen-level` is `overflow: hidden`, so that
     does not scroll — it quietly clips the dock. Giving the track 2 px and taking 2 px back
     here makes the whole row cost nothing on the viewport where nothing is free. */
  padding: 6px 10px 6px;
  background: linear-gradient(180deg, rgba(255, 255, 255, .82) 0%, rgba(255, 255, 255, .38) 100%);
  border-bottom: 2px solid rgba(255, 255, 255, .6);
  box-shadow: 0 4px 14px rgba(96, 74, 46, .1);
}
.hud-row { display: flex; align-items: center; gap: 6px; min-width: 0; }
.hud-row-2 { gap: 10px; }

.hud-title { flex: 1 1 auto; }
.hud-level {
  font-size: 16px;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.hud-intro {
  font-size: var(--fs-min);
  color: var(--ink-soft);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* THE STAT CARDS, and the one measurement that made them free.
 *
 * The mockups put Moves and Score on raised cards, which reads far better than two bare stacks
 * of text floating against the HUD gradient — a number with an edge around it is a number the
 * eye can find without reading it first.
 *
 * The catch is DESIGN §10's vertical budget: `--h-hud` is 88 px, `.screen-level` is
 * `overflow: hidden`, so a HUD that grows does not scroll, it CLIPS the garden underneath.
 * A card is only free if it fits inside the row's existing height.
 *
 * It does, and `.btn-back` is why: it is a `.btn`, so it carries `min-height: var(--tap)` — 44 px
 * of tap target — and it sits in this same row. Row one is therefore already 44 px tall no matter
 * what these stats do. Label (14 px) + value (20 px) at line-height 1.1 is ~37 px; 2 px of padding
 * and 1.5 px of border each side brings the card to ~42 px, which fits inside the 44 px the back
 * button was already spending. The HUD does not grow by a single pixel.
 *
 * Which is why the padding is 2px/10px and not the 8px/14px that would look better in isolation:
 * the horizontal number is free and the vertical one is not. */
.hud-stat {
  flex: 0 0 auto;
  display: flex;
  flex-direction: column;
  align-items: center;
  line-height: 1.1;
  /* 4px, not the 10px this looked best at in isolation. THE HORIZONTAL NUMBER IS NOT FREE EITHER,
   * and the sheet already said so twenty lines up: the `.hud-label` note records that 40 px of
   * width is "the difference between the level name reading as 'Dandelion Meadow 1' and reading
   * as 'Dandelion Meado…'". Two cards at 10px of padding plus their borders spend exactly that
   * 40 px, and the first screenshot at 360×780 duly came back reading "Dandelion Mea…".
   *
   * So the card is drawn with the thinnest padding that still reads as a card. The lesson is the
   * comment's, not mine — I simply had to be shown it in a picture first. */
  padding: 2px 4px;
  background: linear-gradient(180deg, var(--paper), var(--cream-2));
  border: 1px solid var(--line);
  border-radius: var(--r-sm);
  box-shadow: var(--shadow-sm), var(--inset-lite);
}
/* Sentence case, not small caps. At the 14 px floor "MOVES" and "SCORE" are ~20 px wider each
   than "Moves" and "Score", and that 40 px is the difference between the level name reading as
   "Dandelion Meadow 1" and reading as "Dandelion Meado…". The warmer casing was the cheaper
   half of the trade. */
.hud-label {
  font-size: var(--fs-min);
  font-weight: 800;
  letter-spacing: .1px;
  color: var(--ink-faint);
}
.hud-value { font-size: 20px; font-weight: 900; font-variant-numeric: tabular-nums; }
.hud-moves .hud-value { color: var(--k-ink); }
.hud-score-wrap .hud-value { color: var(--gold-ink); }

/* ── THE LIVE COUNTERS ────────────────────────────────────────────────────────────────────────
 *
 * A cascade is the most exciting thing that happens in this game and the HUD used to sit through
 * it completely still, because the numbers were only repainted once the whole resolution had
 * settled. The player saw eight groups go off and one silent jump from 1,240 to 4,910 afterwards
 * — which reads as the game doing arithmetic at you rather than as something you caused.
 *
 * So the counters now tick per event and pulse as they do. `--pulse` is the escalation: the shell
 * sets it from the cascade depth, so beat one is a nudge and beat eight is a shout, using the same
 * rule the audio pitch ladder uses. `transform` and `filter` only — both composited, neither
 * triggers layout, and a HUD that reflows on every score event during a forty-charm cascade is a
 * dropped frame on every phone we care about.
 *
 * The value is an ANIMATION, not a source of truth: `paintHud()` writes the sim's number at
 * settle and reconciles any drift. Nothing here is ever allowed to be the thing the player is
 * scored on. */
.hud-value, .objective .n { transform-origin: center; }

@keyframes hud-tick {
  0%   { transform: scale(1);                                  filter: none; }
  22%  { transform: scale(calc(1 + .13 * var(--pulse, 1)));
         filter: brightness(calc(1 + .55 * var(--pulse, 1)))
                 drop-shadow(0 0 calc(5px * var(--pulse, 1)) var(--pulse-tint, var(--gold))); }
  100% { transform: scale(1);                                  filter: none; }
}

.hud-value[data-tick="1"], .objective .n[data-tick="1"] {
  animation: hud-tick 380ms cubic-bezier(.22, 1.3, .36, 1);
}

/* At depth five and up the counter keeps a standing glow between ticks, so a long cascade reads
   as sustained rather than as a string of unrelated flickers. */
.hud-stat[data-hot="1"] .hud-value {
  text-shadow: 0 0 calc(6px * var(--pulse, 1)) var(--pulse-tint, var(--gold));
}

@media (prefers-reduced-motion: reduce) {
  .hud-value[data-tick="1"], .objective .n[data-tick="1"] { animation: none; }
}

.objectives {
  flex: 1 1 auto;
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  min-width: 0;
  margin: 0;
  padding: 0;
  list-style: none;
}
/* THE OBJECTIVE IS THE PROGRESS BAR, rather than sitting next to one.
 *
 * The mockups give the sprout meter a wide rounded track with its count underneath. This chip
 * already carries that count, read from `objectiveProgress()` — so the cheaper and truer move is
 * to put the track INSIDE the chip it belongs to, not to add a second element that repeats it.
 * One thing saying one thing.
 *
 * MEASURED BEFORE IT WAS WRITTEN, because the guess was wrong. At 360×780 the HUD is 98 px, not
 * the 88 px `--h-hud` names — that token is a `min-height` in a flex column, so `.field-wrap`
 * absorbs the remainder and is 611 px against a 504 px minimum. There is ~107 px of headroom, and
 * the board does not shrink either: its cell solves to `min(43.25, 49.3)`, so it is WIDTH-bound
 * until the canvas falls below ~538 px. The 5 px this costs is genuinely free.
 *
 * `--fill` is a percentage written by `paintObjectives` from the sim's own have/need. Nothing here
 * computes progress; it only draws the number the chip is already showing. */
.objective {
  position: relative;
  display: inline-flex;
  align-items: center;
  gap: 6px;
  min-width: 0;
  padding: 4px 10px 8px 5px;
  font-size: var(--fs-min);
  font-weight: 800;
  background: var(--paper);
  border: 2px solid var(--line);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-sm);
}
.objective .pip {
  width: 18px; height: 18px;
  flex: 0 0 auto;
  border-radius: 50%;
  background: var(--charm, var(--gold));
  box-shadow: inset 0 -2px 0 rgba(0, 0, 0, .14), 0 0 0 2px rgba(255, 255, 255, .8);
}
/* ::before, not ::after — `[data-met="true"]::after` below is the tick, and this must not take
 * the slot that says the objective is done. */
.objective::before {
  content: '';
  position: absolute;
  left: 7px; right: 10px; bottom: 2px;
  /* 5px and a darker empty track, because 3px of near-transparent grey is a bar you cannot see
     in the state you MEET it in. A level opens at 0/229,750 — the fill is zero, so the empty
     track IS the whole thing on screen, and at .18 alpha it read as nothing at all. A meter is
     only doing its job if its empty state announces that there is something to fill. */
  height: 5px;
  border-radius: var(--r-pill);
  background: linear-gradient(90deg,
    var(--k-lite) 0 var(--fill, 0%), rgba(122, 98, 66, .30) var(--fill, 0%) 100%);
  box-shadow: inset 0 1px 1px rgba(96, 74, 46, .16);
}
/* A met objective reads as full whatever the arithmetic says. An op with no target number counts
 * DOWN ("6 left"), so its fraction is not have/need and the tick is the honest signal. */
.objective[data-met="true"]::before { background: var(--k-lite); }

.objective .n { font-variant-numeric: tabular-nums; }
.objective[data-met="true"] {
  color: var(--ink);
  background: linear-gradient(180deg, #f3ffe9, #dcf3cd);
  border-color: var(--c-leaf);
}
.objective[data-met="true"]::after { content: " ✓"; color: var(--k-ink); }

/* The Whistle Meter. A ring rather than a bar so it can sit in the corner of a busy HUD. */
.whistle {
  position: relative;
  flex: 0 0 auto;
  width: 34px; height: 34px;
  display: grid;
  place-items: center;
  background: var(--cream-2);
  border: 2px solid var(--line);
  border-radius: 50%;
  overflow: hidden;
  box-shadow: inset 0 1px 3px rgba(96, 74, 46, .2);
}
.whistle-fill {
  position: absolute;
  left: 0; right: 0; bottom: 0;
  height: 0%;
  background: linear-gradient(180deg, var(--k-mid), var(--k-lite));
  transition: height .45s cubic-bezier(.28, 1.5, .5, 1);
}
.whistle-note { position: relative; font-size: 16px; color: var(--k-ink); }
.whistle[data-full="true"] { animation: hum 1.1s ease-in-out infinite; border-color: var(--gold-deep); }

/* --- the field -------------------------------------------------------------------------

   `canvas.field`, not `.field`. The first draft styled the bare class, and `.field` was also
   the Settings form row — so every row in Settings inherited `position: absolute; inset: 0`,
   collapsed its fieldset to an empty box and painted four labels on top of each other. It was
   invisible to `node --check`, to the HTML validator and to every assertion; it took a
   screenshot. Two components had quietly agreed to share one generic word.

   The repair is in both directions: the form rows are `.set-*` (§13), and this rule is scoped
   to the element type it can possibly mean, so the collision cannot come back by name alone. */

.field-wrap {
  position: relative;
  flex: 1 1 auto;
  min-height: calc(var(--h-board) + var(--h-garden));
  min-width: 0;
}
/* AND THEN THE REPAIR ITSELF MISSED. The rename above swept this selector too, so the rule that
   sizes the play field read `canvas.set-row` — a selector nothing in the document matches, since
   no canvas ever carries `set-row`. The canvas therefore fell back to the intrinsic 300×150
   default, `relayout()` measured that box honestly, and the board rendered at an 11 px cell in
   the top fifth of the screen with two thirds of the phone left empty.
   Every assertion stayed green: `smoke.mjs` never touches CSS, the canvas existed, the overflow
   probe measured 0 px clipped because nothing overflowed — it under-flowed. It took a screenshot,
   which is the same lesson as the comment above and the second time this one rule has taught it. */
canvas.field {
  position: absolute;
  inset: 0;
  display: block;
  width: 100%;
  height: 100%;
  /* Swipe-to-swap needs the pointer stream, so the canvas takes it. This is scoped to the
     canvas ALONE: the document keeps its pinch-zoom (see the viewport meta in index.html),
     which is the accessibility escape hatch and is never worth a game's input model. */
  touch-action: none;
  outline-offset: -3px;
}

/* --- the dock -------------------------------------------------------------------------- */

.dock {
  flex: 0 0 auto;
  display: flex;
  gap: 8px;
  min-height: var(--h-dock);
  padding: 7px 10px;
  /* The safe-area inset, and the reason the dock exists at the bottom at all: this row is the
     thumb's row, and on a notched phone the home indicator sits exactly here. */
  padding-bottom: calc(7px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(180deg, rgba(255, 248, 234, .6) 0%, var(--cream) 45%);
  box-shadow: 0 -4px 16px rgba(96, 74, 46, .1);
}
.dock-btn {
  flex: 1 1 0;
  min-width: 0;
  min-height: var(--tap);
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 1px;
  padding: 4px 2px;
  font: inherit;
  color: var(--ink);
  background: linear-gradient(180deg, #ffffff, var(--cream-2));
  border: 2px solid var(--line-strong);
  /* --r-lg rather than --r: the mockups' dock is noticeably softer-cornered than the rest of the
     chrome, and this is the one row where that reads as friendly rather than as inconsistent —
     it is the row a thumb lives in. Taken from the existing scale; no new radius invented. */
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm), var(--inset-lite);
  cursor: pointer;
  touch-action: manipulation;
  transition: transform .09s ease, box-shadow .09s ease;
}
.dock-btn:active:not(:disabled) { transform: translateY(2px); box-shadow: none; }
.dock-btn:disabled { opacity: .45; cursor: default; }

/* THE TINTS. Set as an attribute in the markup rather than by id, because which button is which
 * is a fact about the DOCK's composition and belongs where the buttons are written down — an id
 * selector here would also outrank `.dock-charm[data-ready]` below, which is the one rule in this
 * row that must always win.
 *
 * The tint replaces only the fill. Border, radius, shadow, type size and weight are untouched, so
 * a tinted button and the neutral Charm beside it are the same object in different colours rather
 * than two different controls. */
.dock-btn[data-tint="leaf"]  { --tint: var(--tint-leaf); }
.dock-btn[data-tint="sun"]   { --tint: var(--tint-sun); }
.dock-btn[data-tint="bloom"] { --tint: var(--tint-bloom); }
.dock-btn[data-tint] { background: linear-gradient(180deg, #ffffff, var(--tint)); }

.dock-icon { font-size: 22px; line-height: 1; color: var(--k-ink); }
.dock-label {
  font-size: var(--fs-min);
  font-weight: 800;
  max-width: 100%;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The charm button carries the longest label in the row — the kingdom charm's own name, and
   "Frostbloom" is ten characters against "Hint"'s four. It gets the wider track so the type can
   stay at the floor instead of shrinking to fit. */
.dock-charm { flex: 1.45 1 0; }

/* The Kingdom Charm is the one button in the dock that earns gold, and only when it is ready.
   Later than .dock-btn on purpose: it is a state, not a variant. */
.dock-charm[data-ready="true"] {
  background: linear-gradient(180deg, var(--gold-lite), var(--gold));
  border-color: var(--gold-deep);
  animation: readyGlow 2s ease-in-out infinite;
}
.dock-charm[data-ready="true"] .dock-icon { color: var(--ink); }


/* ================================================== §12  GRIDS: CODEX · WARDROBE · CRITTERS */

.grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(96px, 1fr));
  gap: 10px;
}
.card {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 4px;
  min-width: 0;
  min-height: var(--tap);
  padding: 10px 8px;
  font: inherit;
  text-align: center;
  color: var(--ink);
  background: var(--paper);
  border: 2px solid var(--line);
  border-radius: var(--r);
  box-shadow: var(--shadow-sm);
  cursor: pointer;
  touch-action: manipulation;
  transition: transform .1s ease, border-color .16s ease, box-shadow .16s ease;
}
.card:active { transform: translateY(1px); }
.card-art {
  width: 100%;
  aspect-ratio: 1 / 1;
  display: grid;
  place-items: center;
  font-size: 30px;
  border-radius: var(--r-sm);
  background: linear-gradient(180deg, var(--k-sky-1), var(--cream-2));
}
.card-name {
  min-width: 0;
  max-width: 100%;
  font-size: var(--fs-min);
  font-weight: 800;
  overflow-wrap: anywhere;
}
.card-sub { font-size: var(--fs-min); color: var(--ink-soft); }
.card[data-owned="false"] { opacity: .55; }
.card[data-owned="false"] .card-art { filter: grayscale(1); }
.card[aria-pressed="true"], .card[data-equipped="true"] {
  border-color: var(--gold-deep);
  box-shadow: 0 0 0 3px var(--gold-lite), var(--shadow-sm);
}

.wardrobe-stage {
  flex: 0 0 auto;
  height: 168px;
  margin: 12px 14px 0;
  border-radius: var(--r-lg);
  background:
    radial-gradient(60% 46% at 50% 92%, var(--k-glow), rgba(0, 0, 0, 0) 70%),
    linear-gradient(180deg, var(--k-sky-1), var(--cream-2));
  box-shadow: inset 0 2px 10px rgba(96, 74, 46, .12);
}

.slots {
  display: flex;
  gap: 10px;
  margin-bottom: 14px;
}
.slot {
  flex: 1 1 0;
  min-width: 0;
  min-height: 72px;
  display: grid;
  place-items: center;
  font-size: var(--fs-min);
  color: var(--ink-faint);
  background: rgba(255, 255, 255, .5);
  border: 2px dashed var(--line-strong);
  border-radius: var(--r);
}
.slot[data-filled="true"] {
  color: var(--ink);
  background: var(--paper);
  border-style: solid;
  border-color: var(--gold-deep);
}


/* ==================================================================== §13  SETTINGS */

.fset {
  margin: 0 0 16px;
  padding: 12px 14px 14px;
  background: var(--paper);
  border: 2px solid var(--line);
  border-radius: var(--r-lg);
  box-shadow: var(--shadow-sm);
}
.fset legend {
  padding: 0 8px;
  font-size: 14px;
  font-weight: 900;
  letter-spacing: .4px;
  text-transform: uppercase;
  color: var(--k-ink);
}
.field {
  display: flex;
  align-items: center;
  gap: 12px;
  min-height: var(--tap);
  min-width: 0;
}
.set-row + .set-row { margin-top: 6px; }
.set-label { flex: 0 0 auto; min-width: 108px; font-size: 15px; font-weight: 800; }
.set-out {
  flex: 0 0 auto;
  min-width: 3ch;
  text-align: right;
  font-variant-numeric: tabular-nums;
  font-weight: 800;
  color: var(--ink-soft);
}
.set-row-check { gap: 10px; }
/* The box is 28 px because a bigger native checkbox looks like a mistake — so the LABEL carries
   the target instead. It is bound by `for=`, so the whole 44 px row toggles the setting, and the
   thing a thumb aims at is the width of the panel rather than a 28 px square. */
.set-row-check .set-label {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  min-height: var(--tap);
  cursor: pointer;
}

.slider {
  flex: 1 1 auto;
  min-width: 0;
  height: var(--tap);            /* the TRACK is thin; the TARGET is 44px. Both matter */
  accent-color: var(--gold);
  cursor: pointer;
}
.check {
  width: 28px; height: 28px;
  flex: 0 0 auto;
  accent-color: var(--k-ink);
  cursor: pointer;
}
.select {
  flex: 1 1 auto;
  min-width: 0;
  min-height: var(--tap);
  padding: 8px 12px;
  font: inherit;
  font-weight: 800;
  color: var(--ink);
  background: linear-gradient(180deg, #ffffff, var(--cream-2));
  border: 2px solid var(--line-strong);
  border-radius: var(--r-sm);
  cursor: pointer;
}
.account { margin-top: 10px; }


/* ================================================================ §14  BOTTOM SHEETS
   DESIGN §10: every modal is a bottom sheet, never a centre dialog — a centre dialog on a phone
   is a thumb-stretch. <dialog> gives us the top layer, focus containment and Escape for free; all
   this section does is move it to the bottom and dress it.

   THE SHEETS LIVE INSIDE #app, and that is not cosmetic: the four kingdom palettes are declared
   on `#app[data-kingdom]` (§2) and custom properties inherit down the DOM tree, not down the
   paint order. A sheet parked next to #app renders with every `--k-*` unset — a cream box with
   no kingdom in it. `#app` also carries `overflow: hidden`, which does NOT clip these, because
   an open modal <dialog> is promoted to the top layer and the top layer is outside the ancestor
   clip. Both facts have to hold for this to work, so both are written down. */

.sheet {
  position: fixed;
  inset: auto 0 0 0;
  width: 100%;
  max-width: var(--app-max);
  max-height: 88dvh;
  margin: 0 auto;
  padding: 0;
  color: var(--ink);
  background: transparent;
  border: 0;
  overflow: visible;
}
.sheet::backdrop {
  background: rgba(58, 44, 30, .48);
  backdrop-filter: blur(2px);
}
.sheet-inner {
  max-height: 88dvh;
  overflow-y: auto;
  overflow-x: hidden;
  padding: 10px 18px calc(18px + env(safe-area-inset-bottom, 0px));
  background: linear-gradient(180deg, var(--cream) 0%, var(--paper) 30%);
  border: 2px solid var(--line);
  border-bottom: 0;
  border-radius: var(--r-xl) var(--r-xl) 0 0;
  box-shadow: var(--shadow-lg);
  animation: sheetUp .34s cubic-bezier(.2, .9, .3, 1);
}
.sheet-grip {
  width: 46px; height: 5px;
  margin: 2px auto 12px;
  background: var(--line-strong);
  border-radius: var(--r-pill);
}
.sheet-title {
  margin-bottom: 6px;
  text-align: center;
  font-size: 23px;
  color: var(--k-ink);
}
.sheet-line {
  margin-bottom: 12px;
  text-align: center;
  font-size: 15px;
  color: var(--ink-soft);
}

/* The rating: pips for the glance, words for everyone else. Both are always present. */
.stars {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 2px;
  margin-bottom: 8px;
}
.pips {
  font-size: 34px;
  line-height: 1.1;
  letter-spacing: 4px;
  color: var(--gold-ink);
  text-shadow: 0 2px 0 rgba(255, 255, 255, .9), 0 4px 12px var(--k-glow);
}
.stars-text { font-size: 14px; font-weight: 800; color: var(--ink-soft); }

.tally { margin: 0 0 14px; }
.tally-row {
  display: flex;
  align-items: baseline;
  gap: 10px;
  padding: 7px 2px;
  border-bottom: 1px dashed var(--line);
}
.tally-row:last-child { border-bottom: 0; }
.tally dt { flex: 1 1 auto; min-width: 0; font-size: 15px; color: var(--ink-soft); }
.tally dd {
  flex: 0 0 auto;
  margin: 0;
  font-size: 18px;
  font-weight: 900;
  font-variant-numeric: tabular-nums;
}

.rewards, .gift {
  display: flex;
  flex-wrap: wrap;
  gap: 8px;
  justify-content: center;
  margin-bottom: 14px;
}
.reward {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  min-width: 0;
  padding: 8px 14px;
  font-size: 14px;
  font-weight: 800;
  background: linear-gradient(180deg, #ffffff, var(--k-sky-1));
  border: 2px solid var(--line);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow-sm);
  animation: popIn .3s ease-out backwards;
}

.sheet-actions { display: flex; flex-direction: column; gap: 8px; }

.pouch {
  display: grid;
  place-items: center;
  min-height: 132px;
  margin-bottom: 12px;
  border-radius: var(--r-lg);
  background:
    radial-gradient(60% 60% at 50% 40%, var(--gold-lite), rgba(0, 0, 0, 0) 70%),
    linear-gradient(180deg, var(--k-sky-1), var(--cream-2));
}
.pouch-tear {
  min-height: var(--tap);
  padding: 12px 22px;
  font: inherit;
  font-size: 17px;
  font-weight: 900;
  color: var(--ink);
  background: linear-gradient(180deg, var(--gold-lite), var(--gold));
  border: 2px solid var(--gold-deep);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow);
  cursor: pointer;
  touch-action: manipulation;
  animation: hum 1.6s ease-in-out infinite;
}
.pouch-items {
  display: flex;
  flex-direction: column;
  gap: 8px;
  margin: 0 0 14px;
  padding: 0;
  list-style: none;
}
.pouch-item {
  display: flex;
  align-items: center;
  gap: 10px;
  min-width: 0;
  padding: 10px 14px;
  background: var(--paper);
  border: 2px solid var(--line);
  border-radius: var(--r);
}
.pouch-item .name { flex: 1 1 auto; min-width: 0; font-weight: 800; overflow-wrap: anywhere; }
.pouch-item[data-new="true"] { border-color: var(--gold-deep); box-shadow: 0 0 0 3px var(--gold-lite); }


/* ================================================== §15  OVERLAYS: FX LAYER + TOAST */

.fx-layer {
  position: fixed;
  inset: 0;
  z-index: 40;
  overflow: hidden;
  pointer-events: none;
}

.toast {
  position: fixed;
  left: 50%;
  bottom: calc(84px + env(safe-area-inset-bottom, 0px));
  z-index: 50;
  max-width: min(88vw, 420px);
  padding: 11px 18px;
  font-size: 15px;
  font-weight: 800;
  text-align: center;
  color: var(--ink);
  background: var(--paper);
  border: 2px solid var(--line-strong);
  border-radius: var(--r-pill);
  box-shadow: var(--shadow);
  transform: translate(-50%, 12px);
  opacity: 0;
  pointer-events: none;
  transition: opacity .2s ease, transform .2s ease;
}
.toast[data-on="true"] { opacity: 1; transform: translate(-50%, 0); }


/* ==================================================================== §16  UTILITY */

/* The companion to `overflow-x: hidden` in §3. A flex or grid child defaults to
   `min-width: auto`, which is "as wide as my longest word" — so one long species name pushes a
   row past the viewport and the whole page slides sideways. Any track that holds text of
   unknown length carries this. */
.min0 { min-width: 0; }


/* =================================================================== §17  KEYFRAMES */

@keyframes drift {
  from { transform: translate3d(-2%, 0, 0) scale(1); }
  to   { transform: translate3d(2%, 1.5%, 0) scale(1.06); }
}
@keyframes sheetUp {
  from { transform: translateY(100%); }
  to   { transform: translateY(0); }
}
@keyframes popIn {
  from { transform: scale(.72); opacity: 0; }
  60%  { transform: scale(1.06); opacity: 1; }
  to   { transform: scale(1); opacity: 1; }
}
@keyframes hum {
  0%, 100% { transform: translateY(0) rotate(0deg); }
  50%      { transform: translateY(-2px) rotate(-1.5deg); }
}
@keyframes readyGlow {
  0%, 100% { box-shadow: var(--shadow-sm), 0 0 0 0 rgba(230, 169, 43, .55); }
  50%      { box-shadow: var(--shadow), 0 0 0 7px rgba(230, 169, 43, 0); }
}


/* ============================================================== §18  REDUCED MOTION
   Every animation and every transition in this sheet goes quiet, from the OS preference or from
   the Settings toggle — the two are written as one rule list so they cannot drift apart.

   Movement goes; information stays. Nothing above encodes a fact in motion alone: the meters
   still fill, the sheets still appear, the ready charm still turns gold. That is the reason
   this block can be as blunt as it is. */

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

#app[data-motion="reduce"],
#app[data-motion="reduce"] *,
#app[data-motion="reduce"] *::before,
#app[data-motion="reduce"] *::after {
  animation-duration: .001ms !important;
  animation-iteration-count: 1 !important;
  transition-duration: .001ms !important;
}


/* ============================================================== §19  SCREEN SIZES
   The budget from §1, re-spent. Every query here changes CUSTOM PROPERTIES rather than
   re-declaring a layout, so §11 stays the only description of how a level is arranged. */

/* --- desktop and large tablets: the same screens, wider, and the extra height goes to the
       garden bed (DESIGN §10) rather than to a taller board. --- */
@media (min-width: 760px) and (min-height: 820px) {
  :root {
    --h-board: 400px;
    --h-garden: 300px;
    --app-max: 760px;
  }
  .logo { font-size: 58px; }
  .logo-line-2 { font-size: 68px; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); }
  .path-nodes { grid-template-columns: repeat(auto-fill, minmax(104px, 1fr)); }
}

/* --- short: most handsets in portrait once the URL bar is showing. The board keeps its size
       for as long as possible because it is the thing being played; the garden gives first. --- */
@media (max-height: 720px) {
  :root { --h-garden: 132px; --h-hud: 80px; }
  .hud-intro { display: none; }
}

/* --- very short: small handsets, and any phone turned sideways. --- */
@media (max-height: 620px) {
  :root { --h-board: 280px; --h-garden: 108px; --h-hud: 72px; --h-dock: 58px; }
  /* The budget shrinks; the TYPE does not. Padding and gaps are what give way — a 10px dock
     caption on a small phone is the one thing here nobody could read. */
  .dock { gap: 6px; padding-inline: 8px; }
  .dock-btn { padding: 2px; }
  .hud-value { font-size: 18px; }
  .sheet-title { font-size: 20px; }
  .pips { font-size: 28px; }
}

/* --- landscape: vertical space is the scarce resource and horizontal is plentiful. --- */
@media (orientation: landscape) and (max-height: 540px) {
  :root { --h-board: 236px; --h-garden: 84px; }
  .screen-title { justify-content: flex-start; }
  .title-inner { padding-top: 10px; gap: 8px; }
  .logo { font-size: 34px; }
  .logo-line-2 { font-size: 40px; }
  .tagline { display: none; }
}

/* --- narrow: 360 is the design width and 320 still exists. Four dock buttons must fit without
       a sideways scroll, so the labels give way before the targets do. --- */
@media (max-width: 359px) {
  .dock { gap: 5px; padding-inline: 6px; }
  .dock-btn { padding-inline: 1px; }
  .menu-row .btn { font-size: var(--fs-min); padding-inline: 6px; }
  .grid { grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); }
}

/* --- a person who has asked for more contrast gets firmer edges, not a different game. --- */
@media (prefers-contrast: more) {
  :root {
    --ink-soft: #5b4b38;
    --ink-faint: #6f5c45;
    --line: #c9b18a;
    --line-strong: #a98d63;
  }
  .btn, .card, .chip, .dock-btn, .path-node { border-width: 3px; }
}

/* ── painted art inside DOM cards (art.js's index; the glyph is the fallback) ───────────────
 *
 * `object-fit: contain` and not `cover`: these are CUT-OUT sprites on transparency, so a crop
 * takes a petal off rather than trimming a background. The card's own colour shows through, which
 * is what keeps a Codex shelf reading as one set even though the plants are 48 different shapes.
 */
.card-art .card-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}
.kingdom-crest .crest-img {
  width: 100%;
  height: 100%;
  object-fit: contain;
  display: block;
}

/* ── the title screen's rotating painted scenes (ui.js `startTitleScenes`) ──────────────────
 *
 * `.sky` keeps its gradient and its drift for the no-art case. When the scenes are available it
 * gains `.sky-scenes`, which stretches it to the full screen and drops the animation — a painted
 * plate does not want a radial-gradient bloom sliding across it.
 *
 * The two `.scene` layers are the crossfade: both are always present, only one carries `.on`, and
 * the transition is on OPACITY ALONE so the compositor can do it without a layout pass.
 *
 * The scrim is the load-bearing part. These plates are gorgeous and the wordmark has to sit on
 * them legibly, so a vertical wash of the page colour lifts the top (behind the logo) and the
 * bottom (behind the buttons) while leaving the middle — where the gnome is — untouched.
 */
.sky.sky-scenes {
  inset: 0;
  height: 100%;
  background: none;
  animation: none;
  overflow: hidden;
}
.sky-scenes .scene {
  position: absolute;
  inset: 0;
  background-position: center;
  background-size: cover;
  background-repeat: no-repeat;
  opacity: 0;
  transition: opacity 2.4s ease-in-out;
  will-change: opacity;
}
.sky-scenes .scene.on { opacity: 1; }
.sky-scenes::after {
  content: '';
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* `--cream` is the PAGE's ground (`body { background: var(--cream) }`), not `--paper`, which is
     the card colour a half-step brighter. Scrimming to the wrong one leaves a visible seam where
     the scene meets the screen edge — and on the title screen that edge is the whole silhouette
     of the layout. The rgba() stops are that same #fff8ea, written out because a CSS variable
     cannot be given an alpha inside a gradient stop. */
  background: linear-gradient(
    to bottom,
    var(--cream) 0%,
    rgba(255, 248, 234, .74) 14%,
    rgba(255, 248, 234, .10) 38%,
    rgba(255, 248, 234, .10) 58%,
    rgba(255, 248, 234, .84) 84%,
    var(--cream) 100%);
}
@media (prefers-reduced-motion: reduce) {
  .sky-scenes .scene { transition: none; }
}
[data-motion="reduce"] .sky-scenes .scene { transition: none; }

/* ── painted icons inline with text (ui.js `chip` / `starPips`) ─────────────────────────────
 *
 * Sized in `em`, not px, so a player who scales their type up scales the icon with it — the whole
 * reason a glyph was the right call for the back arrow and the close cross, borrowed here for the
 * icons that DO have a painted version. `vertical-align` keeps them on the text baseline rather
 * than sitting proud of it, which is the give-away that an image has been dropped into a sentence.
 */
.reward-ic .ic-img {
  width: 1.35em;
  height: 1.35em;
  object-fit: contain;
  display: inline-block;
  vertical-align: -0.28em;
}
.pips { display: inline-flex; gap: .12em; align-items: center; }
.pip { line-height: 1; }
.pip .pip-img {
  width: 1.1em;
  height: 1.1em;
  object-fit: contain;
  display: block;
}

/* ======================================================================= §21  THE BARD FROG
   A frog on a toadstool in the bottom-left corner who holds the music: two volume sliders and
   his whole repertoire. Every other control in this game is a garden object, and a settings cog
   floating over a cottage garden is the one piece of furniture that would admit the whole thing
   is software.

   HE SITS ABOVE THE THUMB ROW, NOT IN IT. `--h-dock` is the level screen's control row and the
   path screen's bottom bar is about the same height, so one offset clears both — and the
   safe-area inset is added because on a notched phone the home indicator is exactly here. The
   fallback in the var() matters: this file is also read on screens that never set --h-dock. */
.bard {
  position: fixed;
  left: max(10px, env(safe-area-inset-left, 0px));
  bottom: calc(var(--h-dock, 56px) + 10px + env(safe-area-inset-bottom, 0px));
  z-index: 40;
  display: flex;
  flex-direction: column-reverse;   /* panel opens UPWARD, out of the corner */
  align-items: flex-start;
  gap: 8px;
  pointer-events: none;             /* the corner is not a wall — only its children take taps */
}
.bard > * { pointer-events: auto; }

.bard-btn {
  width: 62px;
  height: 62px;
  padding: 0;
  border: none;
  background: none;
  cursor: pointer;
  touch-action: manipulation;
  border-radius: 50%;
  /* He breathes. Two seconds, two pixels — enough that the corner is alive, small enough that
     nobody watching the board ever notices it moving. */
  animation: bard-breathe 4.2s ease-in-out infinite;
}
.bard-btn:active { transform: translateY(2px); }
@keyframes bard-breathe {
  0%, 100% { transform: translateY(0) }
  50%      { transform: translateY(-2px) }
}

/* THE SPRITE, OR THE TOADSTOOL THIS FILE DRAWS ITSELF.
   `ui.js` sets a background-image when `ui_bard` is in the art set. With no art the two
   gradients below are what a player sees: a red cap with pale spots over a cream stalk. It is a
   toadstool rather than a frog because a frog in two gradients is a green blob, and a toadstool
   in two gradients is unmistakably a toadstool — the G5 rule is that the fallback must read as
   SOMETHING, not as a smaller version of the thing that is missing. */
.bard-skin {
  display: block;
  width: 100%;
  height: 100%;
  background: center / contain no-repeat,
    radial-gradient(circle at 50% 78%, var(--cream) 0 22%, transparent 22.5%),
    radial-gradient(circle at 32% 34%, rgba(255, 255, 255, .92) 0 7%, transparent 7.5%),
    radial-gradient(circle at 66% 30%, rgba(255, 255, 255, .92) 0 6%, transparent 6.5%),
    radial-gradient(circle at 50% 46%, rgba(255, 255, 255, .92) 0 5%, transparent 5.5%),
    radial-gradient(ellipse 54% 40% at 50% 38%, #d1533f 0 99%, transparent 100%);
  filter: drop-shadow(0 3px 5px rgba(96, 74, 46, .35));
}

.bard-panel {
  width: min(72vw, 260px);
  max-height: min(56vh, 380px);
  overflow-y: auto;
  padding: 12px 14px;
  background: var(--paper);
  border: 2px solid var(--line-strong);
  border-radius: var(--r);
  box-shadow: var(--shadow);
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.bard-panel[hidden] { display: none; }
.bard-title { margin: 0; font-weight: 900; color: var(--k-ink); }
.bard-sub {
  margin: 4px 0 0;
  font-size: var(--fs-min);
  letter-spacing: .06em;
  text-transform: uppercase;
  color: var(--ink-faint);
}
.bard-hint { margin: 0 0 2px; }
.bard-tracks {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: 4px;
}
.bard-track {
  width: 100%;
  padding: 7px 9px;
  font: inherit;
  text-align: left;
  color: var(--ink);
  background: var(--cream);
  border: 2px solid var(--line);
  border-radius: var(--r);
  cursor: pointer;
}
/* The one that is playing is GOLD, which everywhere else in this file means "active or earned".
   Border and background only — the label stays --ink, because --gold as text is 1.97:1. */
.bard-track.is-playing {
  border-color: var(--gold-deep);
  background: var(--gold-lite);
  font-weight: 900;
}

@media (prefers-reduced-motion: reduce) { .bard-btn { animation: none; } }
