/* =====================================================================
   gridora-kit-ext.css — Gridora extension of the UI Kit (EXTENSION.md).
   D13 (owner): kit layout, markup and sizes are reused 1:1; Gridora changes only styles (theme variables in
   css/theme-gridora.css). This file may contain ONLY classes for NEW elements that the kit does not have
   (Canvas board host, no-space strip, objective glyphs, …), coloured with kit variables. It must not
   restyle or resize existing kit classes (.modal*, .btn*, .toggle, .listrow, .topbar, .tile, .toast …).
   D15 (owner, variant B): it also holds LOCAL re-points of theme variables scoped to one kit element (contrast
   F1–F5; the ribbon backing V-3 lives in the theme since D44) and the toast host wrapper `.gr-toast-host` (V-1). Global theme tokens in
   theme-gridora.css are not changed; sizes, positions and fonts of kit classes are not changed.
   base.css, templates.js and upstream markup are never changed.
   D60 (owner 08.10.2026, the UI review docs/UI_UX_REVIEW_2026-10-08.md "do the whole plan"): the ONE named exception to
   "sizes and fonts of kit classes are not changed" - the readability rules marked D60 below (secondary text 14 px, small
   buttons 44 px, the off switch, the input box, the Journey door label, the level-map digits). Every other kit rule stays.
   D91 (owner 14.10.2026, the UX review docs/ux/UX_REVIEW_2026-10-14.md "all but U9"): the second named exception - the
   window entrance (U10) and the pause drawn under its confirm or Settings (GDD 05 UI-86), both marked D91 below.
   ===================================================================== */

/* ===== [shared] owner: integrator — edit only inside this section ===== */
/* Reduced motion (UI-101): data-reduced-motion на <html> */
html[data-reduced-motion="true"] *, html[data-reduced-motion="true"] *::before, html[data-reduced-motion="true"] *::after { animation: none !important; transition-duration: 0.1s !important; }

/* hidden-атрибут сильнее display у Gridora-классов (ромб при D<2, пилюли при best=0) */
.gr-hud [hidden], .gr-home [hidden], .gr-play [hidden] { display: none !important; }


/* GDD 05 UI-28 / U13 (canon 05 G05-11): the screen background is painted by the PAGE, so on a tablet or a desktop
   window the bands beside the play column (kit --app-max-w 900px, D13) show the same background with overscan
   instead of a flat colour; the shell is transparent over it. A var(--bg-image) url would resolve against the file
   that uses it, so the base image is a direct url() here; a skin's gradient (applySkinBackground, no url) takes over
   through data-skin-bg. The boot `<style>` in index.html and the upstream `html, body { background }` paint flat -
   :root outranks them, body is transparent. */
html:root { background: var(--c-bg) url("../assets/gridora/bg.svg") center / cover no-repeat; }
html:root[data-skin-bg] { background-image: var(--bg-image); }
html body { background: transparent; }
html .ai-shell:has([data-kit-app]) { background: transparent; }
/* 06.10 (owner: "no empty block at the top of any screen"): the SDK shell pads ALL its sides with the safe area, and every
   kit screen pads its own top again (.topbar, the sheet head, modal--full: calc(var(--sat) + ...)) - the status-bar
   height was reserved twice, an empty band over every screen. While a kit screen is mounted the shell drops its top pad;
   the screens keep theirs, so their backgrounds run under the status bar and their content stays below it. */
html .ai-shell:has([data-kit-app]) { padding-top: 0; }
/* UI-28: a modal's dim covers the WHOLE viewport, not the play column. Kit flow hosts are `display: contents`
   (gameplay.css), so the kit .modal (absolute, inset 0) is laid out in the shell's overlay slot - 900 px wide. While
   that slot holds a modal it is fixed to the viewport: .modal and .modal__scrim span the viewport, no kit class is
   restyled, the window stays centred and sized as in the kit. */
html .ai-shell:has([data-kit-app]) > [data-slot="overlay"]:has(.modal) { position: fixed; inset: 0; }

/* GDD 05 UI-104 (canon 05 G05-12): a visible focus ring on every focusable control. The kit rings .btn; switches and
   the focusable rows (role button / radio) get the same ring - selected by role, not by restyling a kit class.
   gate:a11y measures --c-focus against every surface the ring sits on (>= 3:1). */
[role="switch"]:focus-visible, [role="button"]:not(button):focus-visible, [role="radio"]:focus-visible,
.ai-shell .ui-popup button:focus-visible { outline: 3px solid var(--c-focus); outline-offset: 2px; }

/* GDD 05 UI-105: the HUD live region - read by screen readers, never painted (no negative margin: it would stick
   1 px out of the screen and the layout smoke counts that as overflow). */
.gr-sr { position: absolute; left: 0; top: 0; width: 1px; height: 1px; margin: 0; padding: 0; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; border: 0; }

/* U13 rotate hint (src/modules/shared/a11y.js): a coarse pointer in landscape - covers the viewport, takes every
   input, the game is paused underneath. */
.gr-rotate { position: fixed; inset: 0; z-index: 2147483000; display: flex; flex-direction: column; align-items: center; justify-content: center;
  gap: 1rem; padding: 1.5rem calc(1.5rem + env(safe-area-inset-right, 0px)) 1.5rem calc(1.5rem + env(safe-area-inset-left, 0px));
  background: var(--c-bg); color: var(--c-text); text-align: center; pointer-events: auto; touch-action: none; }
.gr-rotate[hidden] { display: none; }
.gr-rotate .ico { width: 4.5rem; height: 4.5rem; color: var(--c-accent-text); }
.gr-rotate__text { margin: 0; max-width: 24rem; font-family: var(--font-display); font-size: 1.375rem; line-height: 1.25; }
/* GDD 09 MON-06 (05 owns the layout, 09 the number): on the gameplay screens the banner reserve of THIS session
   (--gr-banner-reserve, set once at session start by game.monetization: 58 dp on Android with ads, 0 otherwise) keeps
   the tray and every control above the banner, whether or not a banner has loaded - the board never jumps. */
html .ai-shell:has([data-kit-hud]) { padding-bottom: var(--gr-banner-reserve, 0px); box-sizing: border-box; }

/* D60 U3 (readability, GDD 05 UI-100 floor 12 px): the secondary text role - kit .small, the field captions, the popup
   subtitle, the tile name - is 14 px at the 390 reference (0.875rem of the 16 px root), not 12. One rule for every screen;
   the colour is the theme --c-muted, one step lighter since D60. tests: readability smoke (text >= 13 px). */
/* A9 (TZ check 14.10, M-402: 12.9 px and 41 px on 360x640): the kit root is fluid (clamp(13.5px, 4.1vw, 21px) - 14.8 px at
   360 wide), so a rem size shrinks under the D60 floor on a narrow phone; every D60 size carries its px floor. */
.small, .field__label, .field__hint, .divider, .modal__subtitle, .tile__name { font-size: max(14px, .875rem); }
/* D60 U4: the kit small button is a 44 px target with 14 px text (it was 36 px / 12 px next to 64 px buttons). */
.btn--s { min-height: max(44px, 2.75rem); font-size: max(14px, .875rem); }
/* A9 (gate:readability at 360 x 640): the kit row title keeps its 390-reference size (.85rem = 13.6 px) as a floor, and on a
   short screen the Home column shrinks the logo, not the controls (the Daily card and the goal pills were squeezed to 33 px). */
.listrow__title { font-size: max(13.6px, .85rem); }
[data-kit-screen="home"] .stage__center > :not(.tile) { flex-shrink: 0; }
/* D60 U5: an OFF switch reads as a switch - a lighter track with a rim (4.1:1 to the panel, WCAG 1.4.11) and a light knob;
   ON stays the kit primary track. The 38 px switch gets an invisible 4 px band above and below (::before; the knob is
   ::after), so the target is 46 px tall. */
.toggle:not(.is-on) { background: #343F78; box-shadow: inset 0 0 0 2px #7F8BC2; }
.toggle::after { background: #EEF2FF; }
.toggle::before { content: ""; position: absolute; inset: -4px -2px; }
/* D60 U10: the text field fills its 52 px frame - a tap anywhere on the frame lands in the input (it was 22 px tall). */
.input > input { align-self: stretch; min-height: max(44px, 2.75rem); }
/* D60 U2: the level numbers are 20 px bold (large text, WCAG >= 3:1); on a completed tile the digit takes the ink of its hue
   (--gr-ink-N in the theme: dark on the five light hues, white on ember and violet) - white on rose / leaf was 2.0-2.5:1. */
[data-kit-screen="journey-select"] .tile .display { font-size: 1.25rem; }
/* D60 (gate:readability): a coloured button whose label is NOT large text (default and small sizes, 14-16 px) takes the
   deeper pair of its colour - white on the light top of the kit green (#1FA86A) is 3.1:1 and of the kit blue 3.2:1. The large
   buttons (btn--l / btn--xl, 20 px+ bold = large text, 3:1) keep the brand colours. */
.btn--primary:not(.btn--l):not(.btn--xl) { --btn-primary-bg: linear-gradient(180deg, #14834F, #0F7445); }
.btn--accent:not(.btn--l):not(.btn--xl), .btn--iap:not(.btn--l):not(.btn--xl) { --btn-accent-bg: linear-gradient(180deg, #2F6BDB, #2A5BC2); --btn-iap-bg: linear-gradient(180deg, #2F6BDB, #2A5BC2); }
/* D91 U10 (GDD 05 UI-89): every window comes in with an even fade. The kit `pop` curve overshoots: the window is half
   visible after ~60 ms of its 220 (frames, 14.10), so it "jumped"; here half way is half the time (~105 of 180 ms). Backdrop and panel share --gr-modal-in-ms
   (interface.hudTimings.popupFadeMs, set on <html> by game.platform); the panel grows from --gr-modal-from
   (popupScaleFromPct). Reduced motion: an opacity fade of --gr-modal-in-reduced-ms, no scale (the global rule above stops
   the panel). The rules hold only under html[data-gr-modal-in] (set with the variables): a record without the keys keeps
   the kit's own entrance - no fallback numbers here. The closing fade and the sheet stay the kit's. */
html[data-gr-modal-in] .ai-shell .modal:not(.is-closing) { animation: gr-modal-in var(--gr-modal-in-ms) ease-in-out both; }
html[data-gr-modal-in] .ai-shell .modal--popup:not(.is-closing) > .modal__win { animation: gr-modal-win-in var(--gr-modal-in-ms) ease-out both; }
html[data-gr-modal-in][data-reduced-motion="true"] .ai-shell .modal:not(.is-closing) { animation: gr-modal-in var(--gr-modal-in-reduced-ms) linear both !important; }
@keyframes gr-modal-in { from { opacity: 0; } to { opacity: 1; } }
@keyframes gr-modal-win-in { from { opacity: 0; transform: scale(var(--gr-modal-from)); } to { opacity: 1; transform: none; } }
/* D93 (owner 15.10.2026, accepted sketches, general rules): the window close is neutral - red is for the irreversible, a
   close loses nothing (it was a red round button on every window); the toggle tiles of sound / music / vibration. */
.btn--icon.btn--round.btn--danger.modal__close { --_bg: #2A3570; --_fg: #FFFFFF; --_sh: inset 0 0 0 2px #9AA6DA, 0 .3rem .8rem rgba(3,6,20,.45); --_shp: inset 0 0 0 2px #9AA6DA; }
.gr-chips { width: 100%; gap: .5rem; align-items: stretch; } /* one height for the three tiles - a two-line name (ja) grows them all */
.btn.gr-chip { flex: 1 1 0; min-width: 0; min-height: 5.25rem; flex-direction: column; gap: .15rem; padding: .5rem .25rem; --_bg: #1E2A62; --_fg: #C7CEF0; --_sh: inset 0 0 0 2px #5A6AAE; --_shp: inset 0 0 0 2px #5A6AAE; font-family: var(--font-body); }
.btn.gr-chip.is-on { --_fg: #FFFFFF; --_sh: inset 0 0 0 2px var(--c-primary), 0 0 0 1px var(--c-primary); --_shp: inset 0 0 0 2px var(--c-primary); }
.gr-chip b { font-family: var(--font-display); font-size: max(16px, 1rem); line-height: 1; }
.gr-chip .gr-chip__on { color: var(--c-primary-text); display: none; } .gr-chip.is-on .gr-chip__on { display: block; } .gr-chip.is-on .gr-chip__off { display: none; }
.gr-chip__name { font-size: max(13px, .8rem); line-height: 1.1; white-space: normal; overflow-wrap: anywhere; text-align: center; }
/* D93 (accepted sketch §1): the profile number tiles - rounded inset panels, the number in the reward colour */
.gr-ptile { border-radius: var(--r-l); padding: .55rem .25rem; min-width: 0; }
.gr-ptile b { display: block; color: var(--c-reward); font-size: max(17px, 1.1rem); }
/* ===== [/shared] ===== */

/* ===== [home] owner: game.home (src/ui/gridora/home.js) — edit only inside this section ===== */
/* scr.home = upstream lobby markup verbatim (D13): no new elements. The lobby zones inside the screen host are a
   layout-only bridge rule in src/ui-kit/styles/gameplay.css ([home]).
   D15 (variant B): local variable re-points in the scope of one kit element only — the global theme token is unchanged. */
/* V-14 / F5: muted Journey progress node (inline background:var(--c-locked-dk), home.js nodeRow) on --c-bg —
   #10173A was 1.05:1, #5A66A2 → 3.35:1 (graphic ≥ 3:1). */
[data-kit-screen="home"] .node--muted { --c-locked-dk: #5A66A2; }
/* D93 (owner 15.10.2026, Home variant A "like Color Block"; replaces the 52 px Journey door of WP8.4 T4 / D41 / D60 U1 - their
   rules carry over: a long label wraps between words, never inside one; white or dark text keeps 4.5:1 on its colour).
   The centre column is a head row (the logo centred, the Daily door at its right), the numbers column, Journey and Classic
   as two equal xl buttons; the column keeps a side gutter only. A short screen (<= 720 px) gets the compact sizes. */
[data-kit-screen="home"] .gr-home-a .stage__center { padding: 0 1rem; gap: .75rem; }
.gr-home-head { width: min(100%, 22rem); display: grid; grid-template-columns: 1fr auto 1fr; align-items: start; gap: .5rem; }
[data-kit-screen="home"] .gr-home-logo { width: min(30vw, 8rem); }
.gr-home-daily { justify-self: end; display: flex; flex-direction: column; align-items: center; gap: .35rem; max-width: 6.5rem; min-width: 0; }
@media (max-height: 720px) {
  [data-kit-screen="home"] .gr-home-a .stage__center { gap: .5rem; }
  [data-kit-screen="home"] .gr-home-logo { width: min(22vw, 5rem); }
  .gr-home-stats { gap: .35rem; } .gr-home-stats .listrow { min-height: max(44px, 2.75rem); }
  [data-kit-screen="home"] .btn.gr-mode { min-height: 4rem; font-size: 1.5rem; }
}
.gr-home-stats { width: min(100%, 22rem); display: flex; flex-direction: column; gap: .5rem; }
.gr-home-stats .listrow { min-height: max(48px, 3rem); padding-block: .25rem; border: 0; font: inherit; color: inherit; text-align: left; width: 100%; }
.gr-home-stats button.listrow { cursor: pointer; }
.gr-home-stats button.listrow:active { transform: translateY(1px); }
.gr-home-stats .listrow__ico { width: 2.25rem; height: 2.25rem; background: var(--c-reward-soft); color: var(--c-reward); }
.gr-home-stats .listrow__title { font-size: max(16px, 1rem); }
.gr-stat__v { font-family: var(--font-display); font-size: max(20px, 1.25rem); color: var(--c-reward); padding-right: .5rem; white-space: nowrap; }
.gr-stat__of { color: var(--c-muted); font-size: .8em; }
/* the two modes: one width, the label wraps between words (de/fr/uk), the icon stays */
[data-kit-screen="home"] .btn.gr-mode { width: min(100%, 22rem); min-width: 0; padding: 0 1rem; gap: .6rem; }
[data-kit-screen="home"] .btn.gr-mode .t { white-space: normal; overflow-wrap: normal; word-break: keep-all; line-height: 1.25; } /* diacritics (uk, pl, vi, ar) stay inside the line box */
/* Journey = the kit reward button re-pointed to orange in this button only (dark ink #3A1D00 on #FF9F2E: 7.6:1) */
[data-kit-screen="home"] .btn.gr-mode--journey { --btn-reward-bg: linear-gradient(180deg, #FFAE4A, #FF9124); --btn-reward-fg: #3A1D00;
  --btn-reward-shadow: inset 0 -4px 0 #C2620A, 0 0.5rem 1.25rem rgba(3,6,20,.45); --btn-reward-shadow-pressed: inset 0 -1px 0 #C2620A; position: relative; overflow: visible; }
[data-kit-screen="home"] .btn.gr-mode--journey .badge { top: -.6rem; right: -.4rem; height: 1.5rem; padding: 0 .55rem; font-size: max(13px, .8rem);
  background: #FFFFFF; color: #B3243F; text-transform: uppercase; }
[data-gr="journey-locked"] { margin-top: -.4rem; }
/* the Daily door: a calendar face (today's number, a check when done, a lock while closed) over its title */
.btn--icon.btn--l.gr-daily-door { width: 4.5rem; height: auto; min-height: 5.25rem; flex-direction: column; gap: .2rem; padding: .45rem .25rem .4rem; overflow: visible;
  --_bg: #2E3B74; --_fg: #FFFFFF; position: relative; }
.gr-daily-door .gr-cal { width: 2.75rem; height: 2.75rem; border-radius: .7rem; background: #FFFFFF; color: #1B2450; display: grid; place-items: center;
  font-family: var(--font-display); font-size: 1.35rem; line-height: 1; box-shadow: inset 0 .45rem 0 #FF5C7A; padding-top: .3rem; }
.gr-daily-door .gr-cal .ico { width: 1.4rem; height: 1.4rem; color: var(--c-primary); }
.gr-daily-door__t { font-size: max(13px, .8rem); line-height: 1; white-space: nowrap; }
.btn.gr-daily-door.is-locked .gr-cal { background: #3A4A8C; color: #DDE3FF; box-shadow: none; padding: 0; }
.btn.gr-daily-door.is-locked .gr-cal .ico { color: #DDE3FF; }
.btn.gr-daily-door.is-locked { --_bg: #232E5C; --_fg: #C7CEF0; }
.btn.gr-daily-door.is-locked::before { background: transparent; } /* the face carries the lock; no veil over a readable title */
.gr-daily-cap { display: flex; flex-direction: column; align-items: center; gap: .25rem; max-width: 100%; text-align: center; line-height: 1.15; }
.gr-daily-cap .small { color: var(--c-text); }
.gr-pips { display: flex; gap: .2rem; } .gr-pips i { width: .9rem; height: .35rem; border-radius: 1rem; background: #3A4A8C; }
.gr-pips i.on { background: var(--c-reward); }
/* D60 U13: the top-bar icons were grey on grey - a lighter face and a white glyph (the PRO door keeps its gold). */
[data-kit-screen="home"] .topbar .btn--icon:not(.gr-home-noads) { --_bg: #2E3B74; --_fg: #FFFFFF; }
[data-kit-screen="home"] .topbar .avatar { box-shadow: inset 0 0 0 2px #7F8BC2; }
/* D60 U13 (the Journey progress dots carry their caption) -> D93: the progress is the "Levels N / 96" row of the numbers column. */
/* D70 (retention plan B2): Home goal lines - the streak and the nearest skin are buttons in the kit pill shape */
button.pill.gr-goal { border: 0; font: inherit; cursor: pointer; padding: 0 .9rem; gap: .4rem; max-width: 100%; height: max(44px, 2.75rem); } /* 44 px tap target (gate:readability read.taps) */
button.pill.gr-goal:active { transform: translateY(1px); }
/* ===== [/home] ===== */

/* ===== [hud] owner: game.play HUD + board (src/ui/gridora/hud.js) — edit only inside this section ===== */
/* D13: the HUD is the kit `endless-run` (Classic) / `hud-progress` (Journey) markup verbatim — no kit class is
   restyled or resized here; only states of the bound data and the new Canvas board host. */

/* Hidden content, box kept (Best pill / % row at best = 0, combo pill below comboMinD) — nothing reflows (UI-54). */
.gr-vis-off { visibility: hidden; }

/* Combo pill (kit "×2 multiplier" / "Combo ×3" pill): the tier colour (GDD 06 ART-45) re-points the variable of the
   kit inline `color:var(--c-danger-text)`, so text and icon take the tier colour. */
.gr-combo--blue { --c-danger-text: var(--c-accent); }
.gr-combo--teal { --c-danger-text: var(--c-info); }
.gr-combo--gold { --c-danger-text: var(--c-reward); }
/* Streak diamond on the combo pill (GDD 07 5.4 / 5.3 / 5.6; UI-57): transform + opacity only (VFX-07); game.play adds the
   classes only when motion is allowed. Appear 0 -> 1.15 -> 1 (260 ms), pulse per clear 1 -> 1.12 -> 1 (180 ms), icon breath
   +-3 % on 2.4 s while the streak lives (the one continuous effect, VFX-83), flare 1.35 on a new tier, lost = shrink + fade. */
[data-bind="combo"] { transition: color .22s ease-out; }
.gr-combo-in { animation: gr-combo-in .26s cubic-bezier(.34,1.56,.64,1) both; }
@keyframes gr-combo-in { 0% { transform: scale(0); opacity: 0; } 60% { transform: scale(1.15); opacity: 1; } 100% { transform: scale(1); opacity: 1; } }
.gr-pop { animation: gr-pop .18s ease-out both; }
@keyframes gr-pop { 50% { transform: scale(1.12); } 100% { transform: scale(1); } }
.gr-combo-live .ico { animation: gr-combo-breath 2.4s ease-in-out infinite; }
@keyframes gr-combo-breath { 50% { transform: scale(1.03); } }
.gr-combo-live.gr-flare .ico { animation: gr-combo-flare .36s ease-out both; }
@keyframes gr-combo-flare { 44% { transform: scale(1.35); } 100% { transform: scale(1); } }
.gr-combo-lost { --c-danger-text: var(--c-muted); animation: gr-combo-lost .36s cubic-bezier(.55,0,1,.45) .12s both; }
@keyframes gr-combo-lost { to { transform: scale(0); opacity: 0; } }
[data-reduced-motion="true"] .gr-combo-live .ico { animation: none; }
/* GDD 07 VFX-74: colour is never the only carrier - the diamond's decoration names the tier in greyscale too:
   blue plain, teal ringed, gold ringed + starred */
[data-bind="combo"] .ico { border-radius: 50%; }
.gr-combo--teal .ico, .gr-combo--gold .ico { box-shadow: 0 0 0 2px currentColor; }
.gr-combo--gold { position: relative; }
.gr-combo--gold::after { content: "\2605"; position: absolute; top: -0.45rem; left: 0.35rem; font-size: 0.7rem; line-height: 1; color: currentColor; }
/* GDD 07 13.2: under reduced motion the diamond appears and disappears by a fade */
[data-reduced-motion="true"] [data-bind="combo"] { transition: opacity .2s ease-out, color .22s ease-out; }
/* Journey objective counters (GDD 07 10.2 / 10.5): pop on a decrement, a rising "-N", a flash + check at zero, a warn
   pulse on a failed level; transform + opacity only (VFX-07) */
.gr-cp-pop { animation: gr-cp-pop .22s cubic-bezier(.34,1.56,.64,1); }
@keyframes gr-cp-pop { 50% { transform: scale(1.28); } }
.gr-cp-done { animation: gr-cp-done .24s cubic-bezier(.34,1.56,.64,1); }
@keyframes gr-cp-done { 0% { transform: scale(.4); opacity: .4; } 100% { transform: scale(1); opacity: 1; } }
.gr-cp-minus { position: absolute; left: 50%; top: -0.2rem; transform: translateX(-50%); font-size: .7rem; font-weight: 700; color: var(--c-text); pointer-events: none; animation: gr-cp-minus .4s ease-out forwards; }
@keyframes gr-cp-minus { to { transform: translate(-50%, -18px); opacity: 0; } }
.gr-cp-warn { animation: gr-cp-warn .26s cubic-bezier(.34,1.3,.64,1); box-shadow: 0 0 0 3px var(--c-danger) !important; }
@keyframes gr-cp-warn { 50% { transform: scale(1.08); } }

/* % of best (kit «Fever» progress-row, UI-50/51): neutral / warm / record re-point the theme colours used by the
   kit fill gradient and end node (accent → info → reward, the Stage-1 pill states). */
.gr-pct { --c-reward: var(--c-accent-dk); --c-danger: var(--c-accent); --c-danger-soft: var(--c-accent-soft); --c-danger-dk: var(--c-accent-text); }
.gr-pct--warm { --c-reward: var(--c-info-dk); --c-danger: var(--c-info); --c-danger-soft: var(--c-info-soft); --c-danger-dk: var(--c-info-text); }
.gr-pct--record { --c-reward: var(--c-reward-dk); --c-danger: var(--progress-fill-reward); --c-danger-soft: var(--c-reward-soft); --c-danger-dk: var(--progress-fill-reward); }

/* Objective glyphs (Journey progress-row): remaining count in the target kind colour; gem glyph (also used by the
   Journey level cards). */
.gr-kind { color: var(--gr-kind, currentColor); }
.objective-chip__ico { width: 1.25rem; height: 1.25rem; display: grid; place-items: center; }
.objective-chip__ico--gem { background: url("../assets/gridora/gem.svg") center / contain no-repeat; }

/* --- Canvas board host (BoardHost): the stage below the HUD block, then the kit bottom spacer --- */
.gr-board { width: 100%; height: 100%; display: grid; grid-template-rows: minmax(0, 1fr) auto; }
.gr-play { position: relative; width: 100%; height: 100%; min-height: 0; touch-action: none; }
.gr-play canvas { position: absolute; left: 0; top: 0; touch-action: none; -webkit-user-select: none; user-select: none; }
/* D70 (retention plan E1): a soft glow under the board in the skin's colour (--gr-board-glow, render/theme/skins.js
   boardGlowOf) - the dark board on the dark page read as a hole. A static gradient behind the canvas that only breathes
   its opacity (compositor-only, 9 s), still with reduced motion. */
.gr-play::before { content: ''; position: absolute; inset: 0; pointer-events: none;
  background: radial-gradient(ellipse 72% 44% at 50% 48%, var(--gr-board-glow, rgba(134,143,176,.34)) 0%, transparent 70%);
  animation: gr-glow-breathe 9s ease-in-out infinite alternate; }
@keyframes gr-glow-breathe { from { opacity: .7; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .gr-play::before { animation: none; } }
/* R4.1 skin rotation on all-clear (06 ART-58): the new board fades in over the old background (380 ms), off with reduced
   motion; the chip with Undo (10 ECO-16) sits centred under the score, never over the board */
.gr-play[data-skin-swap] canvas { animation: gr-skin-in 380ms ease-out; }
@keyframes gr-skin-in { from { opacity: 0; } to { opacity: 1; } }
@media (prefers-reduced-motion: reduce) { .gr-play[data-skin-swap] canvas { animation: none; } }
/* D72 (owner 11.10 "very small board", Color Block CB2): the chip shows for rotationChipMs after an all-clear - it no longer
   RESERVES a 40 px row under the score for the whole game; it floats over the bottom row of the HUD (the goal / % row),
   still under the score and never over the board */
[data-kit-hud] main.stage { position: relative; }
.gr-skin-chip { display: flex; justify-content: center; pointer-events: auto; position: absolute; left: 0; right: 0; bottom: 0; z-index: 3; }
/* D70 (retention plan E2): a newly unlocked skin on the result - the card pops in, its board thumbnail shines once */
.gr-unlock { animation: gr-unlock-pop 640ms cubic-bezier(.2, 1.4, .4, 1) both; }
.gr-unlock__thumb { position: relative; box-shadow: 0 0 0 2px var(--c-reward), 0 0 14px var(--c-reward); }
.gr-unlock__thumb::after { content: ''; position: absolute; inset: 0; pointer-events: none;
  background: linear-gradient(115deg, transparent 30%, rgba(255,255,255,.55) 50%, transparent 70%); transform: translateX(-120%);
  animation: gr-unlock-shine 900ms 380ms ease-out both; }
@keyframes gr-unlock-pop { 0% { opacity: 0; transform: scale(.9); } 60% { opacity: 1; transform: scale(1.03); } 100% { opacity: 1; transform: scale(1); } }
@keyframes gr-unlock-shine { to { transform: translateX(120%); } }
@media (prefers-reduced-motion: reduce) { .gr-unlock, .gr-unlock__thumb::after { animation: none; } .gr-unlock__thumb::after { display: none; } }
.gr-skin-chip.gr-vis-off { pointer-events: none; }
/* R4.3 Daily milestone bar (10 ECO-32): five equal segments lit one per milestone; a first-time fill plays once */
.gr-daily { display: flex; flex-direction: column; align-items: center; gap: .3rem; }
/* D70: the newcomer goal bar and the % row share ONE slot (grid cell) - the board keeps its size */
.gr-goalslot { display: grid; }
.gr-goalslot > * { grid-area: 1 / 1; align-self: center; min-width: 0; }
/* U4 (phone 15.10: the canvas line above the board was cut on a tall screen): the tutorial step line takes the hidden
   header's place - large, bold, white, at the bottom of the header next to the board; the one header element visible
   over the steps (.gr-vis-off hides the rest). */
.gr-tutline { visibility: visible; margin-top: auto; padding: 0 var(--gutter) .75rem; text-align: center; color: #FFFFFF;
  font-family: var(--font-display); font-weight: 700; font-size: max(22px, 1.5rem); line-height: 1.2; }
.gr-tutline[hidden] { display: none; }
/* D91 U5 (UX review 14.10): under the result window the HUD goals strip is hidden - its numbers (1,000 2,500 5,000) stood
   beside the result ribbon through the dim. */
.ai-shell:has([data-kit-flow^="result-"]) .gr-goalslot { visibility: hidden; }
.gr-segs { display: grid; grid-template-columns: repeat(5, 1fr); gap: .3rem; width: min(100%, 18rem); }
/* D93 (accepted sketch §9): a step not reached yet is a visible empty slot (a light rim on the panel colour), the reached one
   is gold - the dark-on-dark empty steps did not read as steps */
.gr-seg { height: .7rem; border-radius: 999px; background: #1E2A62; box-shadow: inset 0 0 0 1px #6B79B8; }
.gr-seg.is-on { background: var(--c-reward); box-shadow: 0 0 0 1px var(--c-reward-dk) inset; }
/* D60 U12: the Daily bar reads as a reward scale - the next target is body size, every segment has its threshold under it. */
.gr-daily [data-bind="dailyNext"] { font-size: 1rem; color: var(--c-text); font-family: var(--font-display); }
.gr-seg-cap { display: grid; grid-template-columns: repeat(5, 1fr); gap: .3rem; width: min(100%, 18rem); text-align: center; font-size: max(14px, .875rem); color: var(--c-muted); font-variant-numeric: tabular-nums; }
.gr-seg-cap > .is-on { color: var(--c-reward); }
.gr-seg.is-new { animation: gr-seg-fill 520ms ease-out; }
@keyframes gr-seg-fill { 0% { transform: scaleX(.2); filter: brightness(1.6); } 100% { transform: scaleX(1); filter: none; } }
@media (prefers-reduced-motion: reduce) { .gr-seg.is-new { animation: none; } }
/* R4.3 pop.dailyBest calendar (10 ECO-34): a day cell carries its number and a glyph (UI-99: never colour alone) */
.gr-cal { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: .05rem; min-height: 2.25rem; border-radius: var(--r-s, .5rem); background: var(--c-panel-2, rgba(255,255,255,.06)); }
.gr-cal--perfect, .gr-cal--complete { background: var(--c-reward-soft); color: var(--c-reward-dk); }
.gr-cal--played { background: var(--c-accent-soft, rgba(255,255,255,.1)); }
.gr-cal--today { box-shadow: 0 0 0 2px var(--c-primary) inset; }
.gr-cal--future { opacity: .45; }
/* No-space strip (UI-68) over the tray */
.gr-nospace { position: absolute; left: var(--gutter); right: var(--gutter); height: 3rem; border-radius: var(--r-l); display: flex; align-items: center; justify-content: center;
  background: rgba(5,8,22,.78); box-shadow: inset 0 0 0 1px var(--c-line); font-family: var(--font-display); font-size: 1.125rem; pointer-events: none; }
/* D60 U8: under the result / revive window the strip only repeats what the window says, dimmed to grey - hidden there. */
html:has([data-kit-flow^="result-"], [data-kit-flow="revive"]) .gr-nospace { visibility: hidden; }

/* D15 (variant B): contrast fixes as local variable re-points scoped by the D13 markers — no new markup classes,
   global tokens in theme-gridora.css unchanged (docs/qa/wp8-d13/contrast.md F1–F4). */
/* V-10 / F1: Classic mode tag (.tag--new → background:var(--c-danger)), #FFF on #CC3D58 → 4.80:1. */
[data-kit-hud="classic"] .topbar .tag--new, [data-kit-hud="daily"] .topbar .tag--new { --c-danger: #CC3D58; } /* D60: Daily too (4.25 -> 4.80) */
/* D91 U3 (UX review 14.10: the red "CLASSIC" tag was the brightest thing on top - the eye went there before the score): the
   mode tag is a quiet capsule - the dark scrim backing, a hairline and the muted text (#B2BCE6 on it ~8:1); the kit tag and
   its size stay. */
[data-kit-hud="classic"] .topbar .tag--new, [data-kit-hud="daily"] .topbar .tag--new { background: rgba(5, 8, 22, .55); color: var(--c-muted); box-shadow: inset 0 0 0 1px var(--c-line); font-size: max(14px, .85rem) !important; } /* A9: the inline .85rem was 12.5 px at 360 */
/* V-11 / F2: Journey objective bar (.progress__fill → background:var(--progress-fill)), label #EEF2FF on #117A4B → 4.80:1. */
[data-kit-hud="journey"] .progress:has([data-bind="objLabel"]) { --progress-fill: #117A4B; }
/* V-12 / F3: checkpoint counts (inline color:var(--c-reward-dk)) on --c-panel, #C8922A → 5.37:1. */
[data-kit-hud="journey"] [data-cp] { --c-reward-dk: #C8922A; }
/* V-13 / F4: objective end-node glyph (inline color:var(--c-danger-dk)) on --c-panel → --c-danger-text #FF8C9C, 6.70:1.
   Only the node right after the objective bar in the Journey row (Classic .gr-pct re-points its own). */
[data-kit-hud="journey"] .progress-row > .progress + .node { --c-danger-dk: var(--c-danger-text); }
/* D60 U3: the objective count "0 / 730" on the bar is 14 px (kit .progress__label 12 px). */
[data-kit-hud] .progress__label { font-size: .875rem; }
/* ===== [/hud] ===== */

/* ===== [journey-select] owner: game.journey (src/ui/gridora/journeySelect.js) — edit only inside this section ===== */
/* --- Journey select (scr.journeySelect) ---
   D13: built only from kit parts (upstream sheet shell `.modal--sheet`, `P.tile` in `.grid--4`, `.progress`,
   `.pill`, `.btn--primary.btn--l`); the objective glyph is an icon in the kit `.tile__body` slot. No new
   classes are needed, so this section is intentionally empty (Stage-1 `.gr-select*`, `.tile--level*`,
   `.level-card__obj`, `@keyframes gr-ring` removed). */
/* D41 (03 LVL-02/36 Product): the chapter mosaic - a completed tile shows its picture hue (the base skin's seven tile
   hues, --gr-hue-1..7 in the theme), the kit check stays; a hard level carries a gold flame in the kit corner slot. */
.gr-mosaic { background: var(--gr-mosaic); }
.gr-mosaic .display { color: var(--gr-mosaic-ink, #fff); text-shadow: none; }
.gr-flame { color: var(--c-reward); line-height: 0; }
.gr-flame .ico { width: 1.25rem; height: 1.25rem; filter: drop-shadow(0 1px 1px rgba(0, 0, 0, .5)); }
/* D93 (accepted sketch §5): the current level is the bright tile (orange, dark ink 7.6:1) - the one thing to press; closed
   tiles keep the kit locked look without a lock on each */
[data-kit-screen="journey-select"] .tile.tile--active { background: linear-gradient(180deg, #FFAE4A, #FF9124); box-shadow: 0 0 0 3px #FFD8A8, var(--tile-shadow-active); }
[data-kit-screen="journey-select"] .tile.tile--active .display, [data-kit-screen="journey-select"] .tile.tile--active .ico { color: #3A1D00; }
.gr-chapter .progress { --progress-fill: var(--c-primary); }
/* D93 path (owner 15.10.2026 «дорожка на 96 уровней — делай», accepted sketch §5): the kit tiles stand on a winding road -
   a holder per level at its point (journeySelect.js pathPoint), the tile inside is a round node (the current one larger);
   the line under them is an SVG stretched to the box with a non-scaling stroke, green up to the current level */
.gr-path { position: relative; width: 100%; height: calc(var(--gr-path-n, 1) * 4.75rem); margin-top: .25rem; }
.gr-path__line { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; pointer-events: none; }
.gr-path__line polyline { fill: none; stroke-width: 10px; stroke-linecap: round; stroke-linejoin: round; vector-effect: non-scaling-stroke; }
.gr-path__rest { stroke: rgba(255, 255, 255, .14); }
.gr-path__done { stroke: var(--c-primary); }
.gr-node { position: absolute; transform: translate(-50%, -50%); }
[data-kit-screen="journey-select"] .gr-node > .tile { --tile-h: 3.75rem; width: 3.75rem; height: 3.75rem; border-radius: 50%; padding: 0; gap: 0; justify-content: center; }
[data-kit-screen="journey-select"] .gr-node > .tile.tile--active { --tile-h: 4.75rem; width: 4.75rem; height: 4.75rem; }
[data-kit-screen="journey-select"] .gr-node > .tile .tile__body { flex: 0 0 auto; width: auto; }
[data-kit-screen="journey-select"] .gr-node > .tile .tile__body .ico { width: 1rem; height: 1rem; }
[data-kit-screen="journey-select"] .gr-node > .tile.tile--active .tile__body .ico { width: 1.25rem; height: 1.25rem; }
/* ===== [/journey-select] ===== */

/* ===== [popups] owner: game.play popups + game.feedback toast (src/ui/gridora/popups.js) — edit only inside this section ===== */
/* D13: pause / confirm / revive / result / help / toast use the kit markup and sizes verbatim (no classes here).
   Revive urgent state (GDD 07 §9.2: ring colour accent → warn in the last seconds): colour only — the kit ring's own
   colour variable switches to danger; size, stroke and layout of `.ring` are untouched. */
.ring.is-urgent { --c-info: var(--c-danger); }

/* The Framework overlay host (.ui-custom spans the shell, pointer-events:auto) must not swallow input under the
   non-interactive kit toast (UI-95). Host interaction only; `.toast` itself is not restyled. The :has([data-kit-app])
   form out-ranks the gameplay.css host rule (same specificity + loaded later made the Stage-1 rule a no-op). */
.ai-shell [data-slot="overlay"] > .ui-custom:has(.toast),
.ai-shell:has([data-kit-app]) [data-slot="overlay"] > .ui-custom:has(.toast) { pointer-events: none; }

/* V-1 (D15): toast host wrapper `<div class="gr-toast-host"><div class="toast t-1" …>` — centres the kit toast
   (upstream `pop` keyframe ends at transform:none and drops translateX(-50%)), 16 px side fields, text wraps.
   Same top / z-index as the kit .toast. */
.gr-toast-host { position: fixed; left: max(16px, var(--gutter)); right: max(16px, var(--gutter)); top: calc(var(--sat) + 5rem); z-index: 200;
  display: flex; justify-content: center; pointer-events: none; }
/* Inside the host only the positioning and wrapping of .toast are neutralised; background, radius, shadow, font,
   colour and the pop animation stay the kit's. */
.gr-toast-host > .toast { position: static; left: auto; top: auto; transform: none; max-width: 100%; white-space: normal; overflow-wrap: anywhere; text-align: center; }

/* V-3 ribbon backing: moved to the theme (--ribbon-bg in theme-gridora.css) by D44 - every popup, not a list. */
/* Owner 2026-09-24 (xx-LONG, docs/qa/wp84/xx-long/LIST.md): long translations wrap instead of being cut.
   O-2: result footer buttons ("Play again" …) — the kit .btn does not wrap its label. */
[data-kit-flow^="result-"] .modal__foot .btn { white-space: normal; overflow-wrap: anywhere; min-width: 0; }
/* O-3: help — the objective description shows in full instead of the kit two-line clamp of .t-2. */
[data-kit-flow="help"] .t-2.grow { -webkit-line-clamp: unset; line-clamp: unset; display: block; overflow: visible; }
/* D91, GDD 05 UI-86 (A5, TZ check 14.10: the pause vanished under them): the pause stays drawn under its confirm or
   Settings - inert and a further 0.25 darker (game.play sets `inert` + data-gr-under). The layer over it adds no scrim of
   its own: the one backdrop is the pause's, so the pause reads dimmed, not buried under a second 0.64. */
[data-kit-flow="pause"][data-gr-under]::after { content: ""; position: absolute; inset: 0; background: rgba(0, 0, 0, .25); pointer-events: none; }
.ai-shell:has([data-kit-flow="pause"][data-gr-under]) [data-kit-flow]:not([data-kit-flow="pause"]) > .modal__scrim { background: transparent; }
/* D93 (owner 15.10.2026, result variant A): the secondary actions are kit icon buttons with a LIGHT rim on a transparent face
   (the grey-on-grey fills read as one shape - UX review 15.10); the run's numbers are one line under the score. */
.btn.gr-ring { --_bg: transparent; --_fg: var(--c-text); --_sh: inset 0 0 0 2px #9AA6DA; --_shp: inset 0 0 0 2px #9AA6DA; }
.btn.gr-ring:disabled, .btn.gr-ring.is-disabled { --_bg: transparent; --_fg: #6D78A8; --_sh: inset 0 0 0 2px #46527F; --_shp: inset 0 0 0 2px #46527F; }
.gr-res-icons { gap: .9rem; justify-content: center; width: 100%; }
.gr-res-stats { text-align: center; }
/* ===== [/popups] ===== */

/* ===== [cards] owner: game.play Journey cards (src/ui/gridora/journeyCards.js) + the Daily intro (src/ui/gridora/daily.js) — edit only inside this section ===== */
/* D41 (03 LVL-03/32/35/36): the Journey cards - the level intro band, the milestone card, the chapter-complete card and
   the Consecutive Victories badge of the win result. Kit popup shell and kit parts; only these few rules are ours. */
.gr-hard { font-family: var(--font-display); color: var(--c-reward); letter-spacing: .06em; text-transform: uppercase; }
.gr-intro__goal { display: flex; align-items: center; justify-content: center; gap: .5rem; overflow-wrap: anywhere; }
.gr-intro__goal .ico { flex: 0 0 auto; color: var(--c-accent-text); }
.gr-peak { position: relative; width: 9rem; height: 7rem; margin: 0 auto; color: var(--c-accent-text); }
.gr-peak > .ico { width: 100%; height: 100%; }
.gr-peak__trophy { position: absolute; left: 50%; top: -.4rem; transform: translateX(-50%); color: var(--c-reward); }
.gr-peak__you { position: absolute; left: 22%; bottom: 18%; padding: .1rem .45rem; border-radius: 999px; background: var(--c-accent); color: #fff; font-size: .75rem; }
.gr-beat { font-family: var(--font-display); font-size: 1.375rem; text-align: center; overflow-wrap: anywhere; }
.gr-mosaic-grid { display: grid; grid-template-columns: repeat(8, 1fr); gap: 3px; width: min(100%, 15rem); margin: 0 auto; }
.gr-mosaic-grid > i { aspect-ratio: 1; border-radius: 3px; background: var(--gr-mosaic, var(--c-locked-dk)); animation: gr-mosaic-in .4s both; animation-delay: calc(var(--i) * 12ms); }
.gr-mini-board { width: min(60%, 9rem); gap: 2px; }
.gr-mini-board > i { border-radius: 2px; animation: none; }
@keyframes gr-mosaic-in { from { opacity: 0; transform: scale(.4); } to { opacity: 1; transform: none; } }
/* D41 layout audit: a result card with buttons (Journey unlocked, keep your progress, new skin, remove ads) puts its
   buttons UNDER its text when the window is narrow - side by side they squeezed the title to 50 px and cut it (de "Reise
   freigeschaltet", "Fortschritt sichern", ru skin rule). The kit listrow stays; only its line may wrap. */
[data-gr="journey-card"], [data-gr="signin-card"], [data-gr="skin-card"], [data-gr="paywall-card"] { flex-wrap: wrap; row-gap: .6rem; }
[data-gr="journey-card"] > .listrow__main, [data-gr="signin-card"] > .listrow__main, [data-gr="skin-card"] > .listrow__main, [data-gr="paywall-card"] > .listrow__main { flex: 1 1 9rem; min-width: 0; }
[data-gr="journey-card"] > .btn, [data-gr="signin-card"] > .btn, [data-gr="skin-card"] > .btn, [data-gr="paywall-card"] > .btn { flex: 1 1 auto; }
.gr-streak { display: inline-flex; align-items: center; gap: .35rem; max-width: 100%; white-space: normal; overflow-wrap: anywhere; }
/* ===== [/cards] ===== */

/* ===== [settings] owner: game.settings (src/ui/gridora/settings.js) — edit only inside this section ===== */
/* win.settings uses the verbatim upstream `settings` markup (D13) — no classes of its own here.
   Vibration toggle disabled when navigator.vibrate is unavailable (D2): the kit has no disabled .toggle state.
   Also used by the pause toggles (shared.js toggleRow). */
.toggle[aria-disabled="true"] { opacity: .45; }

/* Legacy Stage-1 classes, no longer used by win.settings. Kept ONLY because files outside game.settings still use them:
   .listrow--setting / .toggle--sm — src/ui/gridora/popups.js (pause rows); .gr-hit — home.js, hud.js, journeySelect.js,
   modules/debug, theme preview. Delete once those owners migrate. */
.listrow--setting { min-height: 3.5rem; padding: 0.375rem 0.5rem 0.375rem 0.625rem; background: transparent; box-shadow: none; }
.listrow--setting + .listrow--setting { box-shadow: inset 0 1px 0 color-mix(in srgb, var(--c-line) 60%, transparent); border-radius: 0; }
.listrow--setting .listrow__ico { background: transparent; color: var(--c-muted); }
.listrow--setting .listrow__title { font-size: 0.9375rem; font-weight: 500; }
.toggle--sm { width: 3.25rem; height: 2rem; } .toggle--sm::after { width: 1.625rem; height: 1.625rem; }
.toggle--sm.is-on::after { transform: translateX(1.25rem); }
.gr-hit { min-width: 3rem; min-height: 3rem; display: grid; place-items: center; }

.listrow--setting { text-align: left; }
.toggle--sm::after { background: #EEF2FF; }
/* O-1 (owner 2026-09-24, xx-LONG): the profile caption wraps instead of the kit ellipsis of .t-1. */
[data-kit-flow="settings"] .small.muted.t-1 { white-space: normal; overflow: visible; text-overflow: clip; overflow-wrap: anywhere; }
/* D41 layout audit: the Language row - the row name keeps its width, the long value ("Match system · Français") wraps
   at the end side instead of squeezing the name ("Langue" was cut by 3 px in fr). */
[data-kit-flow="settings"] [data-flow-action="language"] > .grow { flex: 0 0 auto; }
[data-kit-flow="settings"] [data-flow-action="language"] > .small { flex: 1 1 0; min-width: 0; text-align: end; overflow-wrap: anywhere; }
/* ===== [/settings] ===== */

/* ===== [debug] owner: game.debug — edit only inside this section ===== */
/* --- Debug-панель (scr.debug, только BUILD_DEBUG) --- */
.gr-debug { width: 100%; height: 100%; overflow-y: auto; padding: calc(var(--sat) + 0.5rem) var(--gutter) calc(var(--sab) + 1rem); display: flex; flex-direction: column; gap: 0.75rem; font-size: 0.75rem; }
.gr-debug .panel { padding: 0.75rem; display: flex; flex-direction: column; gap: 0.5rem; }
.gr-debug select, .gr-debug input[type="text"] { font: inherit; color: var(--c-text); background: var(--input-bg); border: 0; border-radius: var(--r-s); padding: 0.5rem; box-shadow: var(--input-shadow); min-height: 2.5rem; }
/* VFX debug HUD (GDD 07 VFX-86), BUILD_DEBUG only: one line over the board, no input */
.gr-vfxhud { position: absolute; top: 0; left: 0; right: 0; z-index: 5; pointer-events: none; padding: 0.125rem 0.25rem; font: 0.625rem/1.3 ui-monospace, Menlo, monospace; color: #FFFFFF; background: rgba(0, 0, 0, 0.6); white-space: pre-wrap; }
.gr-debug__log { font-family: ui-monospace, Menlo, monospace; font-size: 0.6875rem; white-space: pre-wrap; max-height: 12rem; overflow: auto; color: var(--c-muted); }

.gr-debug label { display: flex; flex-direction: column; gap: 0.25rem; }
/* ===== [/debug] ===== */

/* ---------------------------------------------------------------- GDD 05 §12.5 per-locale metrics (UI-123)
   <html data-locale-group> is set at boot before the first frame (index.html) and on every language change. The
   bundled Fredoka subset is Latin-1 only: Cyrillic, Polish, Turkish (group latinx), Vietnamese, CJK, Thai and Arabic
   use system faces as a whole (never letter by letter), with the line height their marks need. Same stacks on the canvas: src/ui/localization/locales.js metrics(). */
:root[data-locale-group="cjk"] {
  --font-display: 'Fredoka', 'Noto Sans CJK JP', 'Noto Sans CJK KR', 'Noto Sans CJK SC', 'Hiragino Sans', 'Apple SD Gothic Neo', 'Microsoft YaHei', 'PingFang SC', sans-serif;
  --font-body: var(--font-display);
}
:root[data-locale-group="thai"] { --font-display: 'Fredoka', 'Noto Sans Thai', 'Noto Sans Thai UI', Tahoma, sans-serif; --font-body: var(--font-display); }
:root[data-locale-group="arabic"] { --font-display: 'Noto Naskh Arabic', 'Noto Sans Arabic', 'Segoe UI', Tahoma, sans-serif; --font-body: var(--font-display); }
:root[data-locale-group="latinx"], :root[data-locale-group="vi"] { --font-display: system-ui, -apple-system, 'Segoe UI', Roboto, 'Noto Sans', sans-serif; --font-body: var(--font-display); }
/* the kit sets line-height 1 on display text; stacked Vietnamese, Thai and Arabic marks need room (UI-123) */
:root[data-locale-group="vi"] :is(.btn, .modal__title, .modal__ribbon, .listrow__title, .h1, .h2, .h3, .tag, .tabs__tab),
:root[data-locale-group="arabic"] :is(.btn, .modal__title, .modal__ribbon, .listrow__title, .h1, .h2, .h3, .tag, .tabs__tab) { line-height: 1.3; }
:root[data-locale-group="thai"] :is(.btn, .modal__title, .modal__ribbon, .listrow__title, .h1, .h2, .h3, .tag, .tabs__tab) { line-height: 1.45; }
:root[data-locale-group="cjk"] :is(.listrow__title, .h3) { line-height: 1.25; }
/* UI-121: a glyph that points along the reading direction (chevron, back) mirrors in RTL; the board never does */
[dir="rtl"] .ico--dir { transform: scaleX(-1); }
/* GDD 05 UI-115 language picker: the chosen row is outlined in the primary colour (never struck through) */
.listrow.gr-choice.is-chosen { box-shadow: inset 0 0 0 2px var(--c-primary); }
.listrow.gr-choice .listrow__aside { width: 2.5rem; height: 2.5rem; border-radius: 50%; }
.listrow.gr-choice:focus-visible { outline: 2px solid var(--c-accent); outline-offset: 2px; }

/* ===== [leaderboard] owner: game.leaderboard (src/ui/gridora/leaderboard.js) — edit only inside this section ===== */
/* GDD 05 §8.6: the player's own row is visually distinct - the kit has no "current row" state on .listrow, so the mark is
   a Gridora data attribute painted with kit variables (an accent rim, not a new colour). */
.listrow[data-gr-me] { box-shadow: inset 0 0 0 2px var(--c-accent); background: var(--c-accent-soft); }
/* D93 (accepted sketch §3): the podium (2 - 1 - 3, the winner's step the tallest), the own row pinned bright, the first three
   list rows kept for screen readers only (the podium is their picture) */
.gr-podium { display: flex; align-items: flex-end; justify-content: center; gap: .5rem; width: 100%; margin: .25rem 0 .75rem; }
.gr-pod { position: relative; flex: 1 1 0; max-width: 7rem; min-height: var(--h); display: flex; flex-direction: column; align-items: center; justify-content: flex-start; gap: .2rem; padding: .5rem .25rem .6rem; border-radius: var(--r-l) var(--r-l) .4rem .4rem; background: #2A3570; text-align: center; }
.gr-pod[data-gr="lb-podium-1"] { background: #3A4A8C; box-shadow: inset 0 0 0 2px var(--c-reward); }
.gr-pod__name { font-size: max(13px, .8rem); line-height: 1.1; max-width: 100%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.gr-pod__score { font-family: var(--font-display); color: var(--c-reward); font-size: max(15px, .95rem); }
.gr-pod__rank { position: absolute; top: -.6rem; left: 50%; transform: translateX(-50%); min-width: 1.5rem; height: 1.5rem; border-radius: 999px; background: var(--c-reward); color: var(--c-reward-text); font-family: var(--font-display); display: grid; place-items: center; font-size: .8rem; }
.listrow.gr-sr-row { position: absolute; width: 1px; height: 1px; min-height: 0; padding: 0; overflow: hidden; clip-path: inset(50%); }
.listrow.gr-lb-pinned { position: sticky; bottom: 0; background: #2F6BDB; box-shadow: 0 -.5rem 1rem rgba(3,6,20,.5); }
/* ===== [/leaderboard] ===== */

/* ===== [skins] owner: game.skins (src/ui/gridora/skins.js) — edit only inside this section ===== */
/* Owner 05.10 (phone check: the unlock rule lay on the coloured cells of its preview) -> D93: the rule is no longer over
   the preview at all - it sits under the name, so the 05.10 backing rule is gone with the overlay.
   D93 (accepted sketch §4): the locked board is seen in colour - the kit locked veil is lifted on the skin cards, the picture
   only dimmed; the lock is a corner badge; the rule and its bar sit under the name */
[data-gr="skin"].tile--locked { opacity: 1; filter: none; }
[data-gr="skin"].tile--locked .tile__body { filter: saturate(.8) brightness(.8); }
[data-gr="skin"] .gr-skin-lock { position: absolute; top: .5rem; right: .5rem; width: 1.75rem; height: 1.75rem; border-radius: 50%; background: var(--modal-scrim); display: grid; place-items: center; color: #DDE3FF; }
[data-gr="skin"] .gr-skin-rule { width: 100%; display: flex; flex-direction: column; gap: .25rem; text-align: center; color: var(--c-text); }
[data-gr="skin"] .gr-skin-rule .progress { --progress-fill: var(--c-reward); }
[data-gr="skin"] .gr-skin-state { color: var(--c-primary-text); }
/* ===== [/skins] ===== */

/* [paywall] D48 (05.10.2026, concept B "Clean board"): hero picture + live tiles, animated CTA, lifted plan, trial badge,
   confetti on a confirmed purchase. Honesty floor (MON-02/55): nothing delays the close control or a price; no timers.
   Every animation here is killed by the global reduced-motion rule above (minimal motion implies it); the live tiles,
   the flash and the confetti rest at opacity 0, so a stopped animation shows a calm picture, never a tile in mid-air. */
[data-kit-screen="paywall"] .gr-pw-hero { position: relative; width: calc(100% + 2rem); margin: -.5rem -1rem 0; aspect-ratio: 1080 / 605; overflow: hidden; border-radius: var(--r-l) var(--r-l) 0 0; }
[data-kit-screen="paywall"] .gr-pw-hero__pic { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
  -webkit-mask-image: linear-gradient(#000 62%, transparent 100%), linear-gradient(90deg, transparent 0, #000 8%, #000 92%, transparent 100%);
  -webkit-mask-composite: source-in; mask-image: linear-gradient(#000 62%, transparent 100%), linear-gradient(90deg, transparent 0, #000 8%, #000 92%, transparent 100%); mask-composite: intersect;
  animation: gr-pw-hero-in .5s cubic-bezier(.2,.8,.2,1) both; }
[data-kit-screen="paywall"][data-entered] .gr-pw-hero__pic { animation: none; }
@keyframes gr-pw-hero-in { from { opacity: 0; transform: scale(.96); } to { opacity: 1; transform: none; } }
/* the live layer: three of the game's own tiles drop into the empty row, then the row clears with a flash (5 s loop) */
[data-kit-screen="paywall"] .gr-pw-drop { position: absolute; aspect-ratio: 1; transform: translate(-50%, -50%); opacity: 0; pointer-events: none;
  animation: gr-pw-drop 5s cubic-bezier(.3,.9,.4,1) infinite; animation-delay: calc(1s + var(--d, 0s)); }
[data-kit-screen="paywall"] .gr-pw-drop img { display: block; width: 100%; height: 100%; }
@keyframes gr-pw-drop {
  0% { opacity: 0; transform: translate(-50%, -360%); }
  6% { opacity: 1; }
  16% { transform: translate(-50%, -50%) scale(1.12, .86); }
  21% { transform: translate(-50%, -50%) scale(1); }
  62% { opacity: 1; transform: translate(-50%, -50%) scale(1); filter: none; }
  70% { opacity: 0; transform: translate(-50%, -50%) scale(1.35); filter: brightness(2.2); }
  100% { opacity: 0; transform: translate(-50%, -360%); } }
[data-kit-screen="paywall"] .gr-pw-flash { position: absolute; height: 4.2%; transform: translate(-50%, -50%); border-radius: 999px; opacity: 0; pointer-events: none;
  background: radial-gradient(closest-side, #fff, rgba(255,243,214,.85) 45%, rgba(255,216,41,0) 100%); animation: gr-pw-flash 5s ease-out infinite; animation-delay: 1.44s; }
@keyframes gr-pw-flash { 0%, 56% { opacity: 0; transform: translate(-50%, -50%) scaleX(.3); } 62% { opacity: .95; transform: translate(-50%, -50%) scaleX(1.15); } 74%, 100% { opacity: 0; transform: translate(-50%, -50%) scaleX(1.4); } }
/* benefits arrive one by one, the check draws in (once per visit) */
[data-kit-screen="paywall"] .gr-pw-benefits > .listrow { animation: gr-pw-rise .38s cubic-bezier(.2,.8,.2,1) both; }
[data-kit-screen="paywall"] .gr-pw-benefits > .listrow:nth-child(2) { animation-delay: .08s; }
[data-kit-screen="paywall"] .gr-pw-benefits > .listrow:nth-child(3) { animation-delay: .16s; }
[data-kit-screen="paywall"][data-entered] .gr-pw-benefits > .listrow { animation: none; }
@keyframes gr-pw-rise { from { opacity: 0; transform: translateY(.6rem); } to { opacity: 1; transform: none; } }
/* plans: the chosen one lifts and glows in the reward colour; the trial is a large badge */
[data-kit-screen="paywall"] .gr-pw-plan { transition: transform .18s ease-out, box-shadow .18s ease-out; }
/* D93 (accepted sketch §10): prices in the text colour (the kit price is the danger pink - "expensive" / "error"); the CTA names
   the free trial while a plan with one is chosen */
[data-kit-screen="paywall"] .gr-pw-plan .price { color: var(--c-text); }
[data-kit-screen="paywall"] .gr-pw-cta__trial { display: none; }
[data-kit-screen="paywall"]:has(.gr-pw-plan.is-chosen [data-gr="trial"]) .gr-pw-cta__trial { display: inline; }
[data-kit-screen="paywall"]:has(.gr-pw-plan.is-chosen [data-gr="trial"]) .gr-pw-cta__plain { display: none; }
[data-kit-screen="paywall"] .gr-pw-plan.is-chosen { transform: translateY(-2px); box-shadow: inset 0 0 0 2px var(--c-reward), 0 .4rem 1.2rem rgba(255,194,61,.22); }
[data-kit-screen="paywall"] .gr-pw-trial { display: block; width: max-content; max-width: 100%; margin: .25rem 0 .1rem; padding: .2rem .65rem; border-radius: 999px; font-family: var(--font-display); font-weight: var(--fw-display);
  font-size: 1rem; background: var(--c-reward); color: var(--c-reward-text); box-shadow: 0 .15rem 0 var(--c-reward-dk); animation: gr-pw-pop .45s cubic-bezier(.3,1.6,.5,1) .35s both; }
[data-kit-screen="paywall"][data-plans-entered] .gr-pw-trial { animation: none; }
@keyframes gr-pw-pop { from { opacity: 0; transform: scale(.4); } to { opacity: 1; transform: none; } }
/* CTA: a shine runs across every 3 s, a gentle breath; pressing squashes it like clay */
[data-kit-screen="paywall"] .gr-pw-cta { position: relative; overflow: hidden; animation: gr-pw-breath 2.4s ease-in-out infinite; }
[data-kit-screen="paywall"] .gr-pw-cta::after { content: ""; position: absolute; top: 0; bottom: 0; left: -40%; width: 30%; transform: skewX(-20deg); pointer-events: none;
  background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,.55), rgba(255,255,255,0)); animation: gr-pw-shine 3s ease-in-out 1.2s infinite; }
[data-kit-screen="paywall"] .gr-pw-cta:active { animation: none; transform: scale(.96, .92); }
[data-kit-screen="paywall"] .gr-pw-cta[disabled] { animation: none; }
[data-kit-screen="paywall"] .gr-pw-cta[disabled]::after { display: none; }
@keyframes gr-pw-breath { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.03); } }
@keyframes gr-pw-shine { 0% { left: -40%; } 40%, 100% { left: 130%; } }
/* thanks: confetti of the game's tiles (only after the server confirmed the purchase) */
[data-kit-screen="paywall"] .gr-pw-confetti { position: fixed; inset: 0; pointer-events: none; overflow: hidden; z-index: 5; }
[data-kit-screen="paywall"] .gr-pw-confetti > span { position: absolute; top: -8%; width: calc(1.6rem * var(--s, 1)); aspect-ratio: 1; opacity: 0;
  animation: gr-pw-fall 1.6s cubic-bezier(.25,.6,.5,1) var(--d, 0s) both; }
[data-kit-screen="paywall"] .gr-pw-confetti img { display: block; width: 100%; height: 100%; }
@keyframes gr-pw-fall { 0% { opacity: 1; transform: translateY(0) rotate(0); } 85% { opacity: 1; } 100% { opacity: 0; transform: translateY(115vh) rotate(var(--r, 180deg)); } }
/* 06.10 (owner: "highlight the offer to open the paywall"): the Result card stands out - gold rim and glow (the colour of
   the chosen plan on the paywall), the crown, the CTA in the purchase style with the same shine. Calm under reduced motion
   (the global rule stops the shine). */
[data-gr="paywall-card"].gr-pw-offer { box-shadow: inset 0 0 0 2px var(--c-reward), 0 .4rem 1.2rem rgba(255,194,61,.22); }
[data-gr="paywall-card"] .gr-pw-offer__cta { position: relative; overflow: hidden; }
[data-gr="paywall-card"] .gr-pw-offer__cta:not([disabled])::after { content: ""; position: absolute; top: 0; bottom: 0; left: -40%; width: 30%; transform: skewX(-20deg); pointer-events: none;
  background: linear-gradient(90deg, rgba(255,255,255,0), rgba(255,255,255,.55), rgba(255,255,255,0)); animation: gr-pw-shine 3s ease-in-out .6s infinite; }

/* 07.10 (owner): the PRO door on Home must be seen - it was a grey icon among grey icons. Reward gold in soft clay
   anatomy, a slow breath (scale + a fading gold halo) and a shine passing across the face (a background layer, so it
   never leaves the button); reduced motion keeps it still (the global data-reduced-motion rule and the media query). */
[data-kit-screen="home"] .btn--icon.gr-home-noads { position: relative; overflow: visible; isolation: isolate; color: var(--c-reward-text);
  background-image: linear-gradient(105deg, rgba(255,255,255,0) 40%, rgba(255,255,255,.65) 50%, rgba(255,255,255,0) 60%), linear-gradient(180deg, #FFD978, var(--c-reward) 55%, #E9A22A);
  background-size: 300% 100%, 100% 100%; background-position: 150% 0, 0 0; background-repeat: no-repeat;
  box-shadow: inset 0 -3px 0 var(--c-reward-dk), inset 0 2px 0 rgba(255,255,255,.5), 0 .35rem 1rem rgba(255,194,61,.35);
  animation: gr-pro-breath 2.6s ease-in-out infinite, gr-pro-shine 3.2s ease-in-out .9s infinite; }
[data-kit-screen="home"] .btn--icon.gr-home-noads .ico { color: var(--c-reward-text); }
[data-kit-screen="home"] .btn--icon.gr-home-noads::before { content: ""; position: absolute; inset: -3px; border-radius: inherit; z-index: -1;
  animation: gr-pro-halo 2.6s ease-out infinite; pointer-events: none; }
@keyframes gr-pro-breath { 0%, 100% { transform: scale(1); } 50% { transform: scale(1.07); } }
@keyframes gr-pro-shine { 0% { background-position: 150% 0, 0 0; } 45%, 100% { background-position: -50% 0, 0 0; } }
@keyframes gr-pro-halo { 0% { box-shadow: 0 0 0 0 rgba(255,194,61,.6); } 70%, 100% { box-shadow: 0 0 0 .7rem rgba(255,194,61,0); } }
@media (prefers-reduced-motion: reduce) { [data-kit-screen="home"] .btn--icon.gr-home-noads, [data-kit-screen="home"] .btn--icon.gr-home-noads::before { animation: none; } }
