/* ==========================================================================
   GameOnTime — the customer-side design system
   ==========================================================================

   /admin and /pro look like one product because Filament gives them a design
   system. The customer side was 2,172 lines of hand-written CSS with nothing
   governing it, so every page was a fresh set of decisions and read as a
   different company. This file is the thing that was missing.

   ---------------------------------------------------------------- WHAT IT IS

   Two sections and nothing else:

     1 · TOKENS      — the complete vocabulary. Every number and every colour
                       the customer side is allowed to use.
     2 · COMPONENTS  — classes that consume ONLY those tokens.

   A component rule containing a hex, a px font size or a px gap is a bug, and
   `SiteTokensTest::test_no_component_or_page_rule_carries_a_raw_value` says so.

   ------------------------------------------- WHERE THE COLOUR VALUES LIVE

   NOT HERE. `config/brand.php` is and remains the one place a colour is
   decided; App\Support\Brand::cssVariables() prints it into every page as
   `--accent`, `--card`, `--text` and the rest. Every colour token below is an
   ALIAS onto one of those — `var(--card)`, never `#171B28`.

   So there are two homes and the split is a rule rather than an accident:

     · a COLOUR VALUE is a brand decision      -> config/brand.php
     · the TOKEN VOCABULARY is a design decision -> this file

   Which means this file is the one place the SYSTEM is defined, and that file
   is the one place a COLOUR is defined, and neither contains a copy of the
   other. Anything the system needs that the brand source does not have gets
   added THERE and aliased HERE. That is how the surface, line and text ramps
   arrived on 15 Aug — they were sitting in `:root` in site.css, which was
   beside the source rather than in it.

   ------------------------------------------------------------- LOAD ORDER

   layouts/site.blade.php loads this file BEFORE public/css/site.css, and
   prints the brand variables AFTER both. That order is deliberate:

     · system.css first  — so a legacy page rule in site.css can still win on
                           equal specificity while its page is being migrated.
                           No `@layer`, on purpose: a layered rule loses to
                           EVERY unlayered one regardless of specificity, and
                           a silent, invisible loss is the exact failure mode
                           this project has paid for twice with Tailwind
                           utilities in Filament Blade.
     · brand vars last   — custom properties resolve at use time, so the order
                           would not strictly matter; it is last because that
                           is where it already was and moving it would be a
                           change with no reason.

   🔴 NO TAILWIND, NO UTILITY CLASSES, NO BUILD STEP. This file is served as
   written. That is not a preference — Filament ships precompiled CSS and a
   utility class in a custom Blade view compiles to nothing and fails
   SILENTLY, which has bitten this project twice.

   🔴 LOGICAL PROPERTIES ONLY. `margin-inline-start`, `padding-block`,
   `inset-inline-end`, `text-align:start`. The site is RTL-safe by
   construction and every rule below keeps it that way with no [dir="rtl"]
   override. `background-position` has no logical form and is the one
   documented exception; nothing here uses it.
   ========================================================================== */


/* ==========================================================================
   1 · TOKENS
   ========================================================================== */

:root {

  /* ------------------------------------------------------------------ COLOUR

     Roles, not colours. "surface-raised" survives a change of mind about what
     a card is made of; "dark-blue-grey" does not — which is config/brand.php's
     own naming argument, applied one level up.

     Every ratio in a comment below is WCAG 2.1 against `--gt-ink`, computed
     from the two hexes and then pixel-sampled off the rendered /design page.
     Both numbers, because a comment quoting one reads as proof the rule was
     checked once. */

  /* Surfaces — a ladder, and a thing's step says how far off the page it is. */
  --gt-surface-page:    var(--bg);      /* 15.45:1 */
  --gt-surface-sunken:  var(--bg-2);    /* 14.30:1 — inputs, wells, anything recessed */
  --gt-surface-raised:  var(--card);    /* 13.86:1 — cards, panels */
  --gt-surface-overlay: var(--card-2);  /* 12.50:1 — toasts, menus, hover fills */
  /* 🔴 THE MODULE SURFACE — V-27 B2, and it is a ROLE alias over the ladder,
     not a fifth colour. "The page's one decision object sits a step above the
     ordinary card" — the gift page's purchase module is its first reader.

     WHY IT CANNOT SIMPLY READ `--gt-surface-overlay`: the two ladders do not
     agree about what their top step is. On dark, overlay (#1E2434 through
     the emitter) is the top and the module wants it — measured on the
     rendered gift page 30 Aug: raised-on-page was 1.11:1, the "wall rather
     than an object" figure, and overlay-on-page is 1.24:1. On LIGHT the top
     is `raised` (white, 1.13:1 against the page — computed from the two
     hexes) and overlay is the WEAKER step there (1.05:1, computed) — and it
     is not a surface role at all on light: brand.php's own ladder note makes
     it the hover wash and the floating ground, so a module built of it would
     be a panel painted the colour its own hovers wash to. So the light
     blocks below re-point this alias at `raised`, exactly as
     `--gt-accent-hover` re-points for the same the-ladders-disagree reason.
     One role, one name, the theme decides the step — and no new value joins
     brand.php, because both answers already exist on the ladder. */
  --gt-surface-module:  var(--gt-surface-overlay);

  /* Hairlines. Never text and never a fill. --gt-line-strong is the edge of
     every control (input, select, textarea, check, radio, secondary button)
     and since 19 Aug (BUG-344) it clears WCAG 1.4.11's 3:1 on every surface:
     4.08 / 3.78 / 3.66 / 3.30 on page / sunken / raised / overlay — computed
     from config/brand.php, then pixel-sampled by the round's verifier. It is
     still nowhere near what a glyph needs (text is 4.5:1), which is why it
     stays a border. --gt-line is the divider and the decorative card edge,
     which 1.4.11 does not reach. */
  --gt-line:        var(--line);
  --gt-line-strong: var(--line-2);

  /* Text. TWO ROLES, NOT THREE.

     config/brand.php carries a third (`text.faint`, #68748F) because eight
     legacy rules still paint it. It is deliberately NOT given a token here:
     it measures 4.08 / 3.78 / 3.66 / 3.30 against the four surfaces above and
     so fails the 4.5:1 floor on every one of them, including the best case.
     There is no surface in this product on which it is legal, so there is no
     honest token to define. Reaching for it has been a filed bug four times
     (BUG-010, BUG-101, BUG-118, and the hint rule corrected on 14 Aug); a
     token would be the fifth invitation. */
  --gt-ink:       var(--text);    /* 13.86:1 on raised */
  --gt-ink-muted: var(--muted);   /*  6.53:1 on raised — passes AA on all four */

  /* Accent. `--gt-ink-on-accent` is the dark text that goes ON an accent
     fill: the accent is a pale cyan and white on it is 1.3:1. Ink on it is
     11.74:1. */
  --gt-accent:        var(--accent);
  /* 🔴 `--gt-accent-soft` IS AN ARTWORK COLOUR AND IT IS THE SAME IN BOTH
     THEMES. It is the LIGHT END of `--gt-gradient-brand` and of
     `--gt-media-placeholder` — the tile's edge ring and the gradient a game
     with no cover paints — and both of those sit ON or BESIDE artwork, where a
     bright cyan is right whatever the page is made of.

     The light theme's first cut overrode `--accent-2` underneath it so the
     primary button's hover would go darker, and the verifier caught what that
     did: the no-artwork tile's gradient started at a dark teal on a light page,
     and the comment two blocks down claimed these gradients were "deliberately
     not themed" — a statement about a mechanism nobody had measured, which is
     this project's most-repeated defect, reproduced by the round that keeps
     naming it.

     The button's hover has its own token below. One token painting two things
     that need opposite answers is the split this system keeps making. */
  --gt-accent-soft:   var(--accent-2);
  --gt-accent-strong: var(--accent-dim);
  /* THE PRIMARY BUTTON'S HOVER, and it is a SEPARATE token from the artwork's
     light end for the reason above. On dark "more" is brighter, so it is the
     soft accent; on light "more" is darker, so the light block points it at the
     strong one — a button that got paler on hover would lose its own label. */
  --gt-accent-hover:  var(--gt-accent-soft);
  --gt-ink-on-accent: var(--bg);

  /* Status. Meaning, not brand.

     As TEXT these are surface-dependent and danger is the one that bites:
     7.53 / 7.99 / 4.56 on raised, but danger drops to 4.11 on overlay and
     fails there. So the system NEVER paints a message in danger — it paints
     --gt-ink on a danger tint (12.36:1) and lets the tint and the edge carry
     the meaning. The status colour is for the EDGE and the ICON only. */
  --gt-success: var(--green);
  --gt-warning: var(--amber);
  --gt-danger:  var(--red);

  /* The tints those edges sit on. 12% is the measured floor at which the
     tint is distinguishable from the card behind it without dragging the
     text on top of it below AA: danger at 12% over raised leaves --gt-ink at
     12.36:1, success 11.42:1, warning 11.25:1. */
  --gt-tint-success: rgba(var(--success-rgb), .12);
  --gt-tint-warning: rgba(var(--warning-rgb), .12);
  --gt-tint-danger:  rgba(var(--danger-rgb), .12);
  --gt-tint-accent:  rgba(var(--accent-rgb), .12);
  /* COMING SOON — Omar's decision, 19 Aug: the one place the site is orange.
     From config/brand.php `soon` (sampled from the GTA VI artwork's sunset),
     through the emitter's --soon / --soon-rgb. Scoped by BrandPaletteTest to
     the soon tile's badge, wash and decorative hover; nowhere else. */
  --gt-soon:          var(--soon);
  --gt-tint-soon:     rgba(var(--soon-rgb), .16);

  /* A hover wash that works on any surface, because it is the surface plus
     light rather than a named colour. Two steps, because a control on a card
     needs a different amount of lift than one on the page. */
  --gt-wash-soft:   rgba(var(--wash-rgb), .04);
  --gt-wash-strong: rgba(var(--wash-rgb), .07);

  /* And its opposite, for a press. Going DARKER on press and LIGHTER on hover
     is the whole of the depth illusion; see --gt-lift below. */
  --gt-press: rgba(var(--elev-rgb), .18);


  /* -------------------------------------------------------------- TYPE SCALE

     ONE SCALE, NINE STEPS, and `md` is the body. Before this file the
     customer side used .7, .74, .8, .82, .84, .86, .88, .9, 1.05, 1.35 and
     1.5rem AND 12.5px, 13.5px, 14.5px and 20px — twenty-plus sizes, most of
     them within a pixel of a neighbour, none of them chosen against the
     others.

     Every step is a whole pixel at a 16px root, because half-pixel text
     rasterises differently between the two languages' fallback faces and the
     Arabic page ends up a hair taller than the English one for no reason
     anybody can see. */
  /* 🔴 THE FLOOR IS 12px — Omar's decision, 19 Aug 2026 (BUG-342). The two
     smallest steps were 11 and 12; the audit found the micro-labels (COMING
     SOON, .got-label, "Total price", the pay page's NOT AVAILABLE YET tags) at
     11px and the checkout's money help sentences at 12. Both steps are one
     pixel up, so nothing customer-facing renders under 12px, and 2xs now
     MEETS xs at 13px. That is deliberate: the two names name two ROLES (help
     text vs dense UI), and a role keeps its token so the day the floor moves
     again the help text moves with it and the metadata does not. Nine names,
     eight sizes; the /design specimen shows both. */
  /* 12px — uppercase micro-labels, and (since V-06) the media card's meta
     line, which is the smallest thing this product prints on an image. The
     "and nothing else" this comment used to carry stopped being true the
     moment a second role needed the floor; a comment that names its callers
     goes stale the day one is added, so it names the SIZE's job instead: the
     smallest step, for a label or a datum that is read after the thing above
     it. (was 11) */
  --gt-text-3xs: 0.75rem;
  --gt-text-2xs: 0.8125rem;  /* 13px — help text, captions, table headers (was 12) */
  --gt-text-xs:  0.8125rem;  /* 13px — dense UI, secondary metadata */
  --gt-text-sm:  0.875rem;   /* 14px — controls, buttons, inputs */
  --gt-text-md:  1rem;       /* 16px — body. The default, and the only one that is */
  --gt-text-lg:  1.125rem;   /* 18px — card headings */
  --gt-text-xl:  1.375rem;   /* 22px — section headings, money totals */
  --gt-text-2xl: 1.75rem;    /* 28px — page headings */
  --gt-text-3xl: 2.25rem;    /* 36px — the one hero line on a page that has one */
  /* The display size: 36px on a phone to 56px on a desktop, by clamp, so the
     scale has one entry above 3xl rather than a rule per breakpoint.

     🔴 IT SAID "for the ONE headline on the home page" UNTIL V-22, AND THAT
     STOPPED BEING TRUE THE DAY V-22 B3 GAVE IT A SECOND READER: the gift
     card's price, which became that page's one hero line when the two
     repetition rows under it were deleted. A comment naming a caller goes
     stale the day one is ADDED just as surely as the day one is removed —
     the hover-skill records that exact shape — and this one was left
     standing by the batch that added the reader. The scope is now "the one
     hero line on a page that has one", of which there are two.

     🔴 AND THE SECOND READER TAKES THE SIZE WITHOUT THE LEADING, WHICH IS
     STATED RATHER THAN LEFT TO BE FOUND. `--gt-leading-display` exists
     because the home page's headline is DESIGNED to wrap; the price never
     wraps, so it keeps `.total`'s inherited `--gt-leading-tight` (measured:
     56px on a 67.2px line box, 1.2). Defensible, and a divergence from the
     pairing this step declares. Weight and position carry the
     hierarchy from here down; no letter-spacing (the system reserves
     tracking for the micro-label — it is inert or harmful on Arabic, and
     this line will be Arabic when its key lands).

     🔴 AND V-25 B3 TOOK THE SECOND READER BACK OFF, so the clamp is one
     reader again: the home headline. MEASURED at 1536 the ceiling put the
     gift price at 56px against the same page's 28px H1 — exactly double —
     and the figure out-shouted the column it concludes. The price wears
     `--gt-text-price` below now; the two paragraphs above are kept as the
     history of how a caller was added and removed inside three rounds. */
  --gt-text-display: clamp(2.25rem, 1.5rem + 3vw, 3.5rem);

  /* 🔴 THE PRICE STEP — V-25 B3, retuned by V-27 B2, and it is ONE
     component's own size (the `--gt-text-tile` shape), not a new step of the
     scale: the gift card product page's price, read by the two banded
     `.got-gcpage .total` rules in pages.css and by nothing else.

     🔴 40 / 32 ARE OMAR'S NUMBERS, VERBATIM FROM THE V-27 B2 BRIEF — "The
     price: 40px desktop / 32px mobile" — which outranks the scale-position
     argument the 46px version of this comment made (22 -> 28 -> 36 at
     x1.27–1.29; 36 x 1.28 ≈ 46). That derivation was honest and it answered
     a different page: 46 was chosen while the figure stood ALONE in a wide
     panel with nothing beside it. V-27 B2 gives it company — the exchange
     block puts the face value and its label on the same line — and a figure
     with company does not need to shout. Both values measured rendered
     (V-27 B2, 30 Aug): 40px against the same page's 28px H1 on the wide
     band, 32px in the phone bar against the bar's own 44px buttons.

     🔴 ONE TOKEN, TWO VALUES, MOVED BY THE QUERY — the `--gt-text-tile`
     shape exactly, stated at that token in this file: put the value in a
     token and let the breakpoint move the token. The root value is the
     PHONE's (32px); the 56.25em block beside the tile's own breakpoints
     rewrites it to 40px on the wide band. Both banded pages.css rules read
     the one name, so the two bands cannot disagree about anything except
     what the breakpoint says — and the breakpoint is this file's, written
     once. */
  --gt-text-price: 2rem;   /* 32px */

  /* 🔴 THE DISPLAY STEP GETS ITS OWN LEADING — V-06 Batch 5, 22 Aug, and it is
     a FIFTH value rather than a retune of `tight` because the two are answers
     to different questions. `--gt-leading-tight` is 1.2 for a HEADING, which is
     usually one line: what it buys there is room for a descender under an
     ascender when a long one does wrap. The display step is the one line on
     this site that is DESIGNED to wrap — 18ch, balanced, two lines at every
     desktop width — and 1.2 at 56px puts 11px of air between the two halves of
     one sentence, which reads as two sentences. Measured on the rendered page
     at 1536 this round, same element, both values, a real frame between the
     change and the read: the headline's box is 134.38 tall at 1.2 and 120.94
     at 1.08 — 13.44px, which is most of one card's worth of the fold budget
     this batch is spending. The reference file asks for 1.08 at 52px;
     this is that number at the size the scale already decided.

     🔴 ITS SCOPE IS THE HOME PAGE'S HEADLINE, AND IT SAID "the ONE hero line
     on the ONE page that has one" UNTIL V-22. `--gt-text-display` had two
     readers from V-22 B3 to V-25 B3 — the gift card's price took the size,
     without this leading, because a price never wraps — and V-25 B3 moved
     that price onto its own `--gt-text-price`, so the size and this leading
     have ONE reader each again, and it is the same one: the headline. The
     wrap argument above is unchanged and is why this value survives its
     second reader leaving. */
  --gt-leading-display: 1.08;
  --gt-leading-tight:  1.2;   /* headings */
  --gt-leading-snug:   1.35;  /* controls and short labels */
  --gt-leading-normal: 1.6;   /* body prose */

  --gt-weight-normal:   400;
  --gt-weight-medium:   500;
  --gt-weight-semibold: 600;
  --gt-weight-bold:     700;
  --gt-weight-heavy:    800;

  /* Only the 11px uppercase micro-label gets tracking. Letter-spacing on
     Arabic is at best inert and at worst breaks the joins between letters, so
     it is confined to one step that the Arabic pages use for Latin-free
     labels and is never applied to body text. */
  --gt-tracking-label: .07em;

  /* THE FACE — and since V-30 B3 (GAP-176, 2 Sep 2026) it is DELIVERED, not
     hoped for. This token used to say "not a new font: adding a webfont would
     be a network request on a page that needs none". MEASURED before the
     change: `document.fonts.size` 0, zero `@font-face` anywhere, and every
     "Inter" surface on the site rendering in whatever the visitor owned —
     Segoe UI here — so `font-variant-numeric` was answering for a face nobody
     had chosen (TRAPS: a declared font is not a delivered font). Inter
     Variable is now self-hosted under public/fonts/inter/ and declared by
     `Brand::fontFaces()` in the layout head, latin + latin-ext, one variable
     file per subset, `swap` with a preload.

     THIS IS THE ONLY PLACE THE FACE IS DECIDED. `body` in site.css reads it;
     the controls that do not inherit (`.got-btn`, `.got-input`) read it; the
     figure surfaces read `--gt-family-numeric`, which reads it. A second
     spelling anywhere is the defect the batch removed (site.css's body rule
     carried one, and it — not this token — was what the site rendered in).

     The fallbacks after Inter are for a Latin page whose font has not
     arrived yet (`swap`): a metric-near system sans, so the re-flow when
     Inter lands is small (measured per page in the batch report). */
  --gt-family: Inter, -apple-system, "Segoe UI", Arial, sans-serif;
  --gt-family-numeric: var(--gt-family);

  /* THE ARABIC STACK — named, not inherited. Inter carries no Arabic range
     (the batch's impact pass measured an Arabic run at +0.18% under Inter:
     unchanged, every glyph fell past it), so on /ar what matters is what
     FOLLOWS Inter, and "-apple-system, Segoe UI, Arial" is a Latin answer
     that happens to have Arabic glyphs in one member. Chosen Windows-first,
     because that is where this site is built and mostly read. Segoe UI is
     the Windows UI face and carries Arabic at weights 400-700 (MEASURED,
     CSS.getPlatformFontsForNode: every 400-700 Arabic run rendered in Segoe
     UI / Segoe UI Bold before and after this token) — BUT NOT AT 800, where
     the family resolves to Segoe UI Black, a Latin-only face. MEASURED on
     /ar/login, /ar/register and the signed-out /ar/cart, `h1` at weight 800
     28px: BEFORE this token the old Latin list fell past Segoe UI Black and
     Arial to `[Times New Roman ×11, Segoe UI Black ×1]` — every weight-800
     Arabic heading on the site was in a Latin SERIF; AFTER, `[Tahoma ×11,
     Inter ×1]`. Tahoma is second here for exactly that reason: it is the
     older Windows Arabic face and it has the weight-800 Arabic that Segoe UI
     does not. Whether an 800 heading in Tahoma beside 400-700 body in Segoe
     UI is the wanted pairing is Omar's call, filed by the coordinator; the
     stack's ORDER is what this token decides. Then the other platforms by
     name: Geeza Pro (macOS/iOS), Noto Naskh Arabic and Noto Sans Arabic
     (Android and any machine with the Noto set), system-ui — INFERRED, no
     such device was measured. Inter stays FIRST on /ar so a Latin run — a
     brand name, a price — keeps the site's face and its tabular figures;
     the Arabic glyphs never came from it and now fall to a stack chosen for
     them. */
  --gt-family-arabic: Inter, "Segoe UI", Tahoma, "Geeza Pro", "Noto Naskh Arabic", "Noto Sans Arabic", system-ui, sans-serif;


  /* ---------------------------------------------------------------- SPACING

     A 4px rhythm, twelve steps. Named by step and not by size, so a rule
     saying `--gt-space-5` is choosing a position in the rhythm rather than
     asking for 16 pixels — which is what makes the whole scale adjustable
     from here. */
  --gt-space-0:  0;
  --gt-space-1:  0.25rem;   /*  4 */
  --gt-space-2:  0.375rem;  /*  6 */
  --gt-space-3:  0.5rem;    /*  8 */
  --gt-space-4:  0.75rem;   /* 12 */
  --gt-space-5:  1rem;      /* 16 */
  --gt-space-6:  1.25rem;   /* 20 */
  --gt-space-7:  1.5rem;    /* 24 */
  --gt-space-8:  2rem;      /* 32 */
  --gt-space-9:  2.5rem;    /* 40 */
  --gt-space-10: 3rem;      /* 48 */
  --gt-space-11: 4rem;      /* 64 */


  /* ------------------------------------------- THE PAGE SHELL (18 Aug)

     Container, gutter, section rhythm and the tile grid, as tokens — the
     customer-site UI/UX round's Batch 0. The relationship these fix is
     between the container width, the tile width, the gutters and the
     whitespace between blocks; none of them is "bigger". Measured at 1440
     and 1920 before choosing: a 1280 container moved the games row 40px
     and made every tile 6% larger for a three-point change and was rejected;
     1180 stays. What was wrong was two AXES (a centred hero over start-
     aligned sections) and gaps that summed two paddings — the tokens below
     make one axis and one rhythm possible; the pages that read them do the
     rest.

     The gutter and the section rhythm are clamps: 20→40px and 40→64px
     between a phone and a desktop, on the 4px scale at both ends. A `vw` is
     a viewport fact, not a spacing literal, and it lives here with the other
     raw values, which is why the guard permits it. */
  --gt-container: 71.25rem;   /* 1140 of CONTENT — the 1180 box less its two 20px
                                 gutters, kept; the gutter is added outside it, so
                                 the content width the tiles were measured against
                                 does not move when the gutter grows */
  /* 🔴 THE SECOND MEASURE — V-01, 20 Aug. DEFINED HERE AND READ BY NOTHING
     YET, WHICH IS A STATEMENT AND NOT AN OVERSIGHT. Say which of the two kinds
     of unused you have written, next to it: this is the guard-for-a-state-not-
     yet-reached, not the fallback-nothing-can-reach. `ContainerWidthTest` pins
     that it is unused, so the day somebody applies it the test goes red and
     asks whether the catalogue has grown enough to want it.

     THE VALUE IS DERIVED, NOT PICKED. 95rem = 1520px, and 1520 is exactly
     `.got-cat__grid`'s own floor five times over plus its four gaps
     (5 x 288 + 4 x 20 = 1520) — the first width at which the catalogue grid
     yields five equal columns at the measured minimum with nothing left over.
     It also gives the games grid six tracks at 237px against its measured
     224px floor, and it is the largest content width that still fills a 1600
     display exactly once the 40px gutters are taken off. A round number in
     this position would be a number nobody could re-derive.

     🔴 AND IT IS APPLIED TO NOTHING, ON THE MEASUREMENT. Simulated on the
     live site at 1366 / 1440 / 1600 / 1920 in both languages, against 1140,
     1440, 1520 and 1560 of content, every candidate surface got WORSE:

       homepage games grid   3 tiles. 1140 -> 4 tracks, 1 empty.
                             1520 -> 6 tracks at 1600+, THREE empty.
       catalogue grid        4 of the 5 Call of Duty groups hold ONE tile.
                             1140 -> 3 tracks at 367px. 1520 -> 5 tracks at
                             288px: every tile 79px SMALLER and four empty.
       home store strip      4 brands. 1140 -> 4 tracks, exactly full.
                             1520 -> 5 tracks, 1 empty.
       account order list    the row is `1fr auto`, so the whole gain lands
                             between the order title and its price: the gap
                             from title to figure goes 831px -> 1211px.
       account stat tiles    three tiles at 375px become three at 501px, each
                             holding one number and one word.

     `auto-fill` keeps empty tracks, so a wider container at this catalogue
     size buys dead space, not density. THE CONTAINER IS NOT WHAT LIMITS THIS
     PRODUCT TODAY — THREE GAMES IS. Turn this on when a grid is FULL at
     1140: four or more games on the homepage, four or more tiles in a
     catalogue group. It is one class at that point, which is what the token
     is for. */
  --gt-container-wide: 95rem;
  --gt-gutter:    clamp(var(--gt-space-6), 3vw, var(--gt-space-9));
  --gt-section-y: clamp(var(--gt-space-9), 6vw, var(--gt-space-11));
  /* The rhythm INSIDE a section - the store's three shelves, and whatever
     groups come next. It is the section step's own clamp at 60%, so the two
     cannot come apart the day the outer rhythm is retuned; a second literal
     here would be a number nobody could re-derive. */
  --gt-section-y-tight: clamp(var(--gt-space-7), 3.6vw, var(--gt-space-9));
  /* 🔴 THE CATALOGUE TILE'S FLOOR — 240px (V-06 Batch 4, 22 Aug). It was 224
     from 17 Aug, then 260 for the few hours V-05 Batch 2 stood.

     V-05 AND V-06 ARE NOT TWO SETTINGS OF ONE IDEA. V-05 held the shelf to a
     narrow COLUMN (1240, widening to 1560 above 1800) and raised the floor so
     that four cards inside that column landed at ~295px. V-06's container is
     FLUID — 1680 with fixed 48px gutters — and the floor is what DECIDES the
     track count at every width, so it comes back down and nothing hand-writes
     a column anywhere. The old scheme is deleted rather than retuned, and the
     figures it derived (295px cards, a 1560 five-track band, a 595px 1↔2
     boundary) are gone with it: they described a container that no longer
     exists.

     THE ARITHMETIC THIS FLOOR PRODUCES, with the gap at --gt-tile-gap (20)
     and the gutters at 48 / 24 / 16 (see `.container--shelf` in site.css).
     `n` tracks fit a content box of C when 240n + 20(n-1) <= C:

       viewport   content C          tracks              a card is
       1920       1584 (capped)      6  (7 want 1780)    (1584 - 5x20) / 6 = 247
       1536       1440               5  (6 want 1540)    (1440 - 4x20) / 5 = 272
       1280       1184               4  (5 want 1280)    (1184 - 3x20) / 4 = 281
       1100       1052               4  (5 want 1280)    (1052 - 3x20) / 4 = 248
        768        720               2  (3 want  760)    ( 720 - 1x20) / 2 = 350
        390        358               1  (2 want  500)                     358

     🔴 THOSE CONTENT WIDTHS CARRY NO SCROLLBAR, AND AT 1100 THAT WAS NEARLY
     THE WHOLE MARGIN. Every figure above is read off the rendered page with
     the round's harness, which drives a Chrome whose clientWidth equals the
     viewport (measured — at 1536 the grid's box is 1440.00, not 1425). A real
     Windows Chrome takes 15px out of the layout viewport BEFORE the gutter
     resolves, and the CSS only ever sees the layout viewport, so the honest
     way to ask what 1100 does on such a machine is to drive 1085.

     WITH THE GUTTER AT 32 — the reference's number, and this file's for its
     first hour — 1100 gave 1036 of content against the four tracks' 1020: SIX-
     TEEN pixels, of which fifteen were the scrollbar it had just said it
     excluded. Measured: 1085 -> 1021, four tracks by one pixel; 1083 (a 17px
     scrollbar) -> 1019, and the row DROPS TO THREE. The comment claimed 22px
     of slack, which was wrong twice over — the subtraction and the premise.

     THE GUTTER IS THE ONE NUMBER HERE TUNED RATHER THAN DERIVED, so it is what
     was tuned: 24 under 1150, measured at four scrollbar widths —

       1100 no scrollbar     content 1052   4 tracks   32px of slack
       1085 (15px bar)       content 1037   4 tracks   17px
       1083 (17px bar)       content 1035   4 tracks   15px
       1080 (20px bar)       content 1032   4 tracks   12px

     — and no discontinuity where the band ends: 1150 gives 1102 and four
     tracks, 1151 steps to the 48px gutter, gives 1055, and still four.

     AND THE BAND NOBODY LOOKS AT, NAMED RATHER THAN LEFT: two tracks need 500
     of content, so the 1↔2 boundary is viewport 548 (500 + 2x24) — BISECTED at
     one-pixel steps, 1 track at 547 and 2 at 548. Below 520 the gutter steps to
     16 and the content box jumps UP — 473 at a 521 viewport, 488 at 520 — which
     is still one track, so the step is invisible in the count and visible only
     as a wider card. One column runs 320..547.

     EVERY BOUNDARY BISECTED AT ONE-PIXEL STEPS after the gutter moved, and each
     one lands where the floor puts it (a viewport of 260n - 20 + 2 x gutter):
     1->2 at 548, 2->3 at 808, 3->4 at 1068, 4->5 at 1376, 5->6 at 1636, and the
     cap engages at exactly 1680. The gutter's own step at 1150 is NOT a
     discontinuity in the count — 1150 gives 1102 of content and four tracks,
     1151 gives 1055 and four — which is the property a gutter has and a
     hand-written column count does not. */
  --gt-tile-min:  15rem;

  /* THE SHELF'S MEASURE — one measure, one wearer (`.container--shelf`).

     1680 COMES FROM THE APPROVED DESIGN STUDY, and rather than dress it up as
     a derivation it is stated with the band it sits in, so the next person can
     check it. SIX tracks land anywhere from 1636 (6x240 + 5x20 + 2x48, the
     first box that holds them) to 1895 (one under what a seventh needs:
     7x240 + 6x20 + 2x48 = 1896). 1680 is inside that band and near its floor,
     which is what puts the six cards at 247 rather than at the 283 they would
     reach at the ceiling. A seventh track is wider than the displays this
     product is used on. Above 1680 the container stops growing and the page
     gains side margin; below it the container IS the viewport and the floor
     decides the count.

     WHAT IT REPLACES: V-05's `--gt-shelf-max` (1240) and `--gt-shelf-max-wide`
     (1560, behind an 1800 breakpoint). The pair is DELETED, not retuned —
     V-06 has one cap and no breakpoint, and a second measure left defined and
     unread is indistinguishable from one somebody forgot to wire up.

     IT IS STILL SEPARATE FROM --gt-container-wide, which stays defined and
     applied to nothing: that one is derived from `.got-cat__grid`'s floor —
     the SERVICE grid on a game page — and is a statement about a different
     grid that V-06 did not measure. ContainerWidthTest goes on asking the
     next person which grid they widened. This is not that answer. */
  --gt-shelf-max: 105rem;   /* 1680 — six tracks at the floor, gutters included */
  --gt-tile-gap:  var(--gt-space-6);

  /* THE HEADER'S HEIGHT — V-30 B4, the one new layout token the round
     allows. Until this batch the height was written in five places that
     disagreed (site.css `.nav{min-block-size:72px}`; this offset at 5.75rem
     here and 5rem in the phone band; pages.css's basket summary at 5.5rem;
     site.css's order chat at 5.75rem, twice), so a change to the row moved
     four sticky offsets by hand or not at all. Now the row's min-block-size
     (site.css `.nav`), the scroll offset below and — through it — every
     sticky surface, the anchored sections, `#main` and the phone toast
     DERIVE from this line. HeaderHeightTest asserts the exact set of
     declarations and the derived set of consumers.

     4rem is the header's TOTAL height, border included: `.nav` takes
     `calc(var(--gt-header-height) - var(--gt-border-hair))`, so the 1px
     transparent border that carries the gradient hairline is inside the
     figure and the header's border-box lands on the token. MEASURED 2 Sep
     2026, headless Chrome 152: 64.00 on the wide row at 1536, signed out
     and signed in, both languages (V-30 B4, unchanged by B5); on the phone
     the header is NOT this token's value any more — 116.00 at 390, 375, 360
     and 320, both languages, both states (V-30 B5), because the band below
     declares its own. The floor under this one is the wide row's content:
     the tallest item is the pill and the language switch at 46.00 (the
     wordmark is 45.98) + 2×8 padding = 62, one under the 63 this resolves
     to, so the token binds. The phone's floor is two lines — 8 + 44 + 8 +
     46 + 8 = 114 under its own 115 — and is worked out where that band is
     declared (THE HEADER MENUS). */
  --gt-header-height: 4rem;

  /* An in-page anchor's clearance from the sticky header, so a target's
     title is not scrolled UNDER it (the hero's action → #games was, at every
     width — verified 18 Aug). The header plus one --gt-space-6 of air — the
     step the desktop toast keeps from the viewport's edge, so the phone
     toast under the bar and the desktop toast above the edge share one
     distance: 84px over a 64px bar on the wide row and 136 over 116 on the
     phone (V-30 B5), MEASURED in both bands. It was a
     literal 5.75rem here (92 over the old 73) until V-30 B4 made it a
     derivation. */
  --gt-scroll-offset: calc(var(--gt-header-height) + var(--gt-space-6));

  /* THE PILL NAVIGATION'S OWN NUMBERS — V-30 B5 (`.got-tabs`, THE HEADER
     MENUS). Component tokens, beside the header's, because two of them are
     read by an ARITHMETIC: the indicator bar is a fixed-width span placed
     by `translateX()` alone, and its distance is computed from the tab
     width and its own width — `tab × (index + ½) − bar ÷ 2` — so those two
     must be tokens the calc can read, never a measured pixel written into a
     rule (this project has paid four times for a number read off one
     render and baked in; see TRAPS). EQUAL-WIDTH TABS are what make that
     arithmetic true in both languages without measuring either: a tab is
     `--gt-tabs-tab-size` wide whatever its label, so the bar lands on the
     same centre on /ar as on /, and Omar's Arabic labels (GAP-193) will not
     move it when they land. 7rem holds the widest label with air to spare
     (MEASURED in Inter 14px/500 with a Range over the text: Store cards
     76.75px of glyphs in the tab's 100px inner box — 6px padding a side;
     Home 39.55, Games 46.63; بطاقات المتاجر, the proposed Arabic, 85.56px,
     which fits the wide tab and NOT the narrow band's 78px inner box — the
     request sheet says so and offers shorter words) and three of them make
     a 338px pill — which is why the narrow band below steps it down: at 320
     the row's content box is 288.

     The glow is three blurred layers of the accent behind the bar — box-
     shadow, the same device as `--gt-media-glow-soon`, on an element that
     never animates (there is no motion between tabs: every page load
     renders the bar already in place), so the shadow is painted once and
     never re-rasterised. Alpha needs channels, hence `--accent-rgb` from
     the emitter, and hence a token: `rgba()` is banned in a component rule
     (SiteTokensTest) for the reason the header's own literal channels were
     (BUG-096's family).

     The blur is the pill's own backdrop, over the header's glass. MEASURED
     2 Sep at 1536×960 by a pixel diff of the pill with and without it: at
     rest 13,850 (en) / 13,868 (ar) of 15,548 pixels differ by at least
     1/255, 1,554 / 1,921 by more than 1, 91 / 96 by more than 2 and none by
     more than 3/255; scrolled 300px with artwork under the bar, 0 pixels
     differ at all. It
     composites over the header's own blur(12px) without a paint difference
     — which also means it paints nothing that blur has not already
     painted, and it can be dropped without a visible change. Kept as the
     round asked, and said plainly. */
  --gt-tabs-tab-size: 7rem;
  --gt-tabs-bar-size: 2rem;
  --gt-tabs-blur: 12px;
  --gt-tabs-glow:
    0 0 6px rgba(var(--accent-rgb), .55),
    0 0 14px 2px rgba(var(--accent-rgb), .35),
    0 0 26px 6px rgba(var(--accent-rgb), .18);

  /* BUG-037's tap minimum, as a token rather than as `44px` written into
     five rules. It is already quoted by four of them in site.css. */
  --gt-tap-min: 44px;

  /* V-33 B3 — the basket line's artwork box. BOTH axes, written: a ratio
     cannot size an unloaded image (TRAPS), and the summary beside the line
     must not move when the picture lands. 2:3 — the product tile's own
     frame since V-20 B4, so an uploaded card face and a bundled poster are
     both exactly this shape and `object-fit: cover` throws nothing away; a
     store brand's square mark (the parent's cover, GAP-165) is cropped, as
     it is on the product page. The phone band below steps it down where a
     390px line has a name, a stepper and a trash to fit beside it. */
  --gt-cart-art-inline: 5rem;
  --gt-cart-art-block: 7.5rem;

  /* The checkbox and radio box. 18px, and the number is not free: the label
     beside it is `--gt-text-sm` at `--gt-leading-snug`, whose line box is
     14 x 1.35 = 18.9px, so a box of 18 sits on that line with 0.45px to
     spare at each end and needs NO nudge — which is what the two rules this
     component replaces were doing with `margin-block-start: .12rem` and
     `.28rem`, two different magic numbers for the same job. Change the
     control's type step and this number has to move with it; that is why it
     is a token and why this comment states the arithmetic. */
  --gt-check-size: 1.125rem;

  /* The tick and the dot inside it. A fraction of the box rather than a
     second measurement, so the two cannot come apart. */
  --gt-check-mark: 0.625rem;


  /* ----------------------------------------------------------------- RADII */
  --gt-radius-xs:   6px;
  --gt-radius-sm:   8px;
  --gt-radius-md:   10px;
  --gt-radius-lg:   14px;   /* the site's existing --radius; cards */
  --gt-radius-xl:   20px;
  --gt-radius-pill: 999px;


  /* -------------------------------------------------------- BORDER WEIGHTS

     Three, and the third is not a border in the visual sense — it is the
     status bar down the reading edge of a message box, which is why it is
     thick enough to read as a bar rather than as a heavy hairline.

     `--gt-hairline` is ONE DEVICE PIXEL as a primitive, and it exists because
     two unrelated things want exactly that and neither is spacing: the
     hairline border below, and the 1px box the visually-hidden recipe uses.
     Without it `.got-sr-only` would be the only rule in the system carrying a
     px literal, and the test that forbids them would need a carve-out — which
     is a worse outcome than naming the primitive, because a carve-out is a
     list and lists rot. */
  --gt-hairline:     1px;
  --gt-border-hair:  var(--gt-hairline);
  --gt-border-thin:  1.5px;
  --gt-border-bar:   3px;


  /* -------------------------------------------------------------- ELEVATION

     🔴 THE ONE IDEA THIS SYSTEM IS BUILT AROUND, and it is worth stating
     plainly because every component below leans on it and a future edit that
     drops it will make the whole thing look flat without anybody being able
     to say why.

     ONE LIGHT SOURCE, ABOVE. Every raised surface gets a 1px inset highlight
     along its TOP edge and a shadow BELOW it. Nothing else. The highlight is
     what makes a dark card read as an object rather than as a lighter
     rectangle — on a #171B28 card it computes to #212532, which is a quarter
     of the way to the next step of the surface ladder and is why the ladder
     and the lift do not fight each other.

     A press REMOVES the lift and adds a dark wash, so a pressed control looks
     like it has gone into the page rather than merely changed colour. That is
     the entire press interaction in the system, applied identically to every
     control, and it is the reason the buttons, the card and the toast read as
     one family. */
  --gt-lift: inset 0 1px 0 rgba(var(--wash-rgb), .045);

  /* How far a pressed control travels. A TOKEN because it is a decision
     about how the whole system feels, and it was written out five times
     before the test that forbids raw pixels found it — which is exactly the
     kind of thing that guard is for. One number, one place, five callers. */
  --gt-press-shift: 1px;

  /* How far a pressed MEDIA SURFACE shrinks. The game card is a different
     kind of object from a button — a picture, not a control with a label —
     and it presses by scaling down for one instant rather than by moving
     into the page. Omar's decision, 17 Aug: media cards may press differently
     from buttons, AS A TOKEN, so the next media surface reuses this number
     instead of writing .978 again. Buttons and panel cards keep the shift
     above; nothing in the system reads both. */
  --gt-press-scale: .978;

  --gt-shadow-0: none;
  --gt-shadow-1: 0 1px 2px rgba(var(--elev-rgb), .35);
  --gt-shadow-2: 0 4px 12px -2px rgba(var(--elev-rgb), .45);
  --gt-shadow-3: 0 12px 28px -6px rgba(var(--elev-rgb), .55);
  --gt-shadow-4: 0 24px 56px -12px rgba(var(--elev-rgb), .65);


  /* -------------------------------------------- THE FOUR SURFACE LEVELS (V-02)

     🔴 THE RAMP ABOVE IS A VOCABULARY OF SHADOWS. THESE ARE THE FOUR ANSWERS
     THE SYSTEM ACTUALLY GIVES, and they exist because the composition — one
     light source above, so a highlight AND a shadow, never one alone — was
     written out by hand in TWELVE rules before this: both filled buttons at
     rest and on hover, the four `.got-card` weights, the stat tile, the list
     row, the account panel and the basket thumbnail. Twelve private answers to
     "how far off the page is this", and the tell is that they had picked three
     different N for what turned out to be one job.

     (The count says twelve because it was counted off the diff. The first
     draft of this comment said "nine" and named the toast and the sticky price
     bar among them — the round's verifier checked and neither was ever one:
     the toast is exempt on purpose, below, and `.buy`'s sticky bar has only
     ever carried `--gt-shadow-3` with no highlight at all. A comment stating a
     mechanism it has not measured is this project's named defect and it does
     not get a pass for being about shadows.)

     A level is a POSITION, not a shadow: raising the whole product by a step
     is now one edit here instead of nine, and the next surface joins the
     ladder rather than guessing an N.

       base       flat on the page. The highlight alone — no shadow. Account
                  panels stack several of these and a page of shadows reads as
                  a mess; at that size the lit top edge is enough to separate a
                  panel from the ground.
       elevated   an object resting on the page. The default for a card.
       hover      the same object, picked up. This is also the ACTIVE/selected
                  level: a thing under the cursor and a thing that is chosen
                  are the same distance off the page, and giving them two
                  levels would mean a selected card outranks a hovered one for
                  no reason a customer could name.
       featured   the ONE thing on a page that IS the page. Never more than one
                  in a viewport, the same discipline the brand gradient is held
                  to — a page where three things are featured has featured
                  nothing.

     `--gt-shadow-0` is deliberately absent from all four: `none, inset …` is
     not a valid box-shadow list, and a level meaning "no depth at all" is the
     absence of the declaration rather than a token. */
  --gt-level-base:     var(--gt-lift);
  --gt-level-elevated: var(--gt-lift), var(--gt-shadow-1);
  --gt-level-hover:    var(--gt-lift), var(--gt-shadow-2);
  --gt-level-featured: var(--gt-lift), var(--gt-shadow-3);

  /* `--gt-tint-featured` AND `--gt-edge-featured` WERE DEFINED HERE AND ARE
     GONE — V-18d, 27 Aug. Their comment's own last paragraph had already
     said it: "one consumer each, and both are the store card's" — the edge
     was the square card's SEAM and the tint its FALLBACK WASH, and V-18d
     deleted that card wholesale (the store card is the game tile now, whose
     bare state is `--gt-media-placeholder` and whose edge is `__edge`). A
     token nothing reads is indistinguishable from one somebody forgot to
     wire up, so both went with their readers. The A/B history (edge .22
     invisible / .45 chosen / .60 too close to the soon orange; tint .05
     imperceptible / .10 the reviewer's number) lives in the V-18b round
     report if a featured surface is ever built again — measured against a
     card that no longer exists, so it would be a starting point, not an
     answer. */


  /* ------------------------------------------------- THE FOCUS RING, IN FULL

     🔴 TWO TONES, AND THE SECOND ONE IS NOT DECORATION.

     A plain `outline: 2px solid var(--gt-accent)` is what this site had, and
     it is INVISIBLE on the primary button: that button is FILLED with the
     accent, so the ring measures 1.00:1 against the thing it is drawn on.
     Measured, not assumed — it is the reason this token exists in the shape
     it does.

     The answer is a dark separator band between the control and the accent
     ring, so the ring always has something to be seen against:

       control edge | 2px --gt-surface-page | 2px --gt-accent | page

     `outline` draws the accent, `box-shadow` draws the dark band in the
     offset gap. Against the page the band is invisible (it IS the page) and
     the ring does the work; against the accent button the band is 11.74:1 and
     the ring reads off the band. One treatment, correct on every background,
     which is what the round asked for and what one tone cannot deliver.

     `outline-offset` and the ring width are tokens so a component can never
     invent its own ring — `.got-btn:focus-visible` and `.got-field
     input:focus-visible` are literally the same three declarations. */
  --gt-ring-width:  2px;
  --gt-ring-gap:    2px;
  --gt-ring-color:  var(--gt-accent);
  --gt-ring-band:   var(--gt-surface-page);
  --gt-ring-shadow: 0 0 0 var(--gt-ring-gap) var(--gt-ring-band);
}

/* ------------------------------------------------- THE ARABIC PAGE'S FACE
   V-30 B3. `/ar` is `<html lang="ar" dir="rtl">`, and the face is re-pointed
   by LANGUAGE rather than by direction because the stack is about which
   glyphs a font carries, not about which way the line runs. One declaration,
   on the root, so every reader of `--gt-family` — the body, the controls that
   do not inherit, the figure surfaces through `--gt-family-numeric` — follows
   without a second rule. `:root:lang(ar)` is (0,2,0) against the token's
   (0,1,0), so it wins on any page whose root says Arabic and on no other. */
:root:lang(ar) {
  --gt-family: var(--gt-family-arabic);
}

/* ------------------------------------------------ THE RING INHERITS THE STATE

   🔴 A FIELD MAY NOT SAY "WRONG" AND "FINE" AT THE SAME TIME.

   Measured on /register: the name field showed the error dot, the sentence
   "Please enter your name" and a RED border — with a BLUE focus ring around
   it. Six combinations, three states in two languages, and the ring was
   `rgb(122,214,255)` in all six. The border already followed the state; only
   the ring did not.

   ============== WHY THIS IS ONE DECLARATION AND NOT THREE RULES ===========

   The obvious repair is three more `:focus-visible` rules, one per state.
   That is the shape this project has already paid for three times over with
   BUG-234 / BUG-239 / BUG-242 — a list of selectors that has to be widened
   every time somebody adds a case, and is therefore always one case behind.

   `--gt-ring-color` is a CUSTOM PROPERTY, and custom properties INHERIT. So a
   state does not restyle the ring; it says what colour rings mean inside it,
   and every component in that subtree picks it up. The ring rules themselves
   — on the button, the input, the card, the toast close, the links — are
   untouched and never need touching again.

   🔴 A SIXTH STATE IS ONE LINE HERE AND NOTHING ELSE. `.got-field.is-pending
   { --gt-ring-color: var(--gt-warning) }` and every ring inside a pending
   field is amber, including ones written after this comment. That is the
   property three rounds of widening a selector never had.

   ================ AND ONE STATE STROKE, NOT TWO — BUG-247 =================

   🔴 A RING THE SAME COLOUR AS THE BORDER IT SURROUNDS PAINTS THE FIELD
   TWICE. Pixel-sampled on /register, focused, contact handle in its valid
   state — a 16px strip cut across the edge:

     card #171B28 x4 | #22C55E x2 | band #0D0F17 x2 | #22C55E x1 | fill

   TWO green strokes with the dark band between them. It reads as a doubled
   border, not as a ring, and it is the COST of the fix above rather than a
   defect in it: while the ring was always accent it could not match the
   border, so the two-tone geometry always read as two different things. The
   moment the ring learned the state, it could — and did.

   THE FIX IS THE SAME MECHANISM ONE STEP FURTHER. A state now declares ONE
   colour, `--gt-edge-color`, and the ring is DEFINED AS that colour:

     --gt-edge-color: var(--gt-success);
     --gt-ring-color: var(--gt-edge-color);

   so a state cannot recolour one stroke and forget the other — not because a
   test says so, but because there is only one value. And because the state no
   longer writes `border-color` ITSELF, the control's own `:focus-visible`
   rule out-specifies the edge with no source-order argument and no per-state
   exception: WHILE FOCUSED, THE BORDER JOINS THE BAND.

     rest     control edge (state) | page
     focused  control fill | 3px band | 2px ring (state) | page

   One state stroke either way. What tells the two apart is measured below
   and not left to be inferred: at rest a 1px edge hugging the control;
   focused a 2px stroke standing 3px off it — four times the ink, in the same
   colour, with a dark moat that exists in neither the rest state nor the
   unfocused one.

   🔴 `--gt-edge-color` IS DELIBERATELY NOT DEFINED AT `:root`, and its
   ABSENCE is the neutral state. That is what lets ONE declaration serve both
   "no state" and "a state" — `var(--gt-edge-color, var(--gt-line-strong))`
   is the neutral edge until a state supplies one, and the same idiom gives
   hover and disabled their own neutral colours without any of the three
   needing to know which states exist. A `:root` default would make the
   fallback dead code, which in this project is indistinguishable from a bug.

   🔴 A SIXTH STATE IS STILL TWO LINES AND NOTHING ELSE. `.got-field.is-pending
   { --gt-edge-color: var(--gt-warning); --gt-ring-color: var(--gt-edge-color) }`
   and the border, the ring, the hover edge and the disabled edge inside a
   pending field are all amber, including components written after this
   comment.

   THE BAND STAYS THE PAGE COLOUR on purpose. It is what separates the ring
   from the control, and with the border now joining it, it is 3px of the four
   the focused state is made of.

   Measured on the card (#171B28), which is the surface the ring is drawn on
   at `outline-offset: 2px`: accent 10.53:1, danger 4.56:1, success 7.53:1 —
   all clear of the 3:1 floor for a non-text indicator. Pixel-sampled off the
   rendered page in both languages, not computed from the two hexes. */
.got-field--error,
.got-field.is-invalid,
.got-input--error {
  --gt-edge-color: var(--gt-danger);
  --gt-ring-color: var(--gt-edge-color);
}

.got-field.is-valid {
  --gt-edge-color: var(--gt-success);
  --gt-ring-color: var(--gt-edge-color);
}

/* 🔴 AND THE DESTRUCTIVE BUTTON, WHICH THE GUARD FOUND AND I HAD NOT.
   `test_a_state_that_recolours_the_border_recolours_the_ring` named it the
   first time it ran: `.got-btn--destructive` paints its border `--gt-danger`
   and was ringing in accent — the same contradiction as the field, one
   component over, on a control whose whole job is to look consequential.

   It is fixed by EXTENDING the rule rather than excusing the button from it.
   Carving component variants out of the invariant would have meant a list of
   exceptions, which is the shape being replaced; and "a control coloured by
   meaning rings in that meaning" is the better rule anyway. The separator
   band is what keeps the ring readable against the button's own red edge —
   which is more work than it did when the two were never the same colour.

   And it doubled for the same reason the field did, on the same measurement:
   a red 1px border, the band, a red 2px ring. It is fixed by the same one
   token, which is the point — the button is not a variant carved out of the
   invariant, it is a control that declares a state colour, and every control
   that declares one is answered here. */
.got-btn--destructive {
  --gt-edge-color: var(--gt-danger);
  --gt-ring-color: var(--gt-edge-color);
}

:root {


  /* ------------------------------------------------------------------ MOTION

     Durations as tokens is what makes prefers-reduced-motion ONE RULE instead
     of one per component. site.css answers that query in five separate blocks
     today, each naming the classes it happens to know about, and a component
     added after any of them is animated for everybody. Below, the query
     rewrites the tokens and every transition in the system collapses at once
     — including ones written after the query.

     Falsification: delete the @media block at the bottom of this section and
     SiteTokensTest::test_reduced_motion_rewrites_every_duration_token goes
     red — its first assertion is that this block exists. */
  --gt-duration-instant: 80ms;
  --gt-duration-fast:    140ms;
  --gt-duration-base:    200ms;
  --gt-duration-slow:    320ms;

  /* Standard: fast out of the gate, settles gently. Everything that moves
     between two resting states uses it. */
  --gt-ease-standard: cubic-bezier(.2, 0, 0, 1);
  /* Entering: no acceleration at the start, so a thing that appears looks
     like it was already moving. */
  --gt-ease-out: cubic-bezier(0, 0, 0, 1);
  /* Leaving: the reverse, so a dismissal accelerates away. */
  --gt-ease-in: cubic-bezier(.3, 0, 1, 1);

  /* The one composite: hover and press feedback, so a component says
     `transition: var(--gt-transition-control)` rather than restating the
     property list. Restating it is how one control ends up transitioning
     `background` while its neighbour transitions `all`. */
  --gt-transition-control:
    background-color var(--gt-duration-fast) var(--gt-ease-standard),
    border-color     var(--gt-duration-fast) var(--gt-ease-standard),
    color            var(--gt-duration-fast) var(--gt-ease-standard),
    box-shadow       var(--gt-duration-fast) var(--gt-ease-standard),
    transform        var(--gt-duration-instant) var(--gt-ease-standard);

  /* One-shot sweeps — the game card's sheen. A finite animation, not a
     transition, so the reduced-motion block below rewrites it like the
     others AND the card stops the animation itself where it is defined,
     because a 1ms sweep is still a flash. */
  --gt-duration-sweep: 720ms;

  /* 🔴 THE CROP FADE — V-27 B2, the phone band's product-artwork window. A
     MASK, not a paint: the gift page's poster is cropped on a phone (a
     window anchored to the top of the card, Omar's requirement) and its
     lower edge dissolves into whatever the page is made of — a mask reads
     alpha, so the fade lands on the DARK page and the LIGHT page alike with
     no colour named here, where an ink-coloured overlay gradient would have
     painted a dark fog on the light theme (the --gt-scrim family is
     deliberately theme-blind, which is right ON artwork and wrong AROUND
     it). `black` is a keyword for the same reason --gt-mask-ring's is: a
     mask's colour is irrelevant and a hex near it reads as one. Vertical,
     so there is no direction to mirror on /ar. The stops were looked at
     rendered at 390 against 70%/100% and 50%/95%: 62→98 keeps the flag and
     the value fully solid in a 4:3 window and lets the wave dissolve rather
     than end at a cut. */
  --gt-media-crop-fade: linear-gradient(180deg, black 62%, transparent 98%);


  /* -------------------------------------------- MEDIA SURFACES (hover-skill)

     The artwork treatment for a card whose body is a picture: the game and
     store-card tiles on the home grid, and whatever media tile comes next.
     Written as tokens rather than inside the component so a second media
     surface reads the same veil, the same rest and lit filters and the same
     lift, and so
     `SiteTokensTest`'s raw-value guard can hold the component itself to
     tokens only. These are the ONLY places in the system a gradient stop or
     a filter is decided.

     The colours are the site palette — accent, blue, cyan and the page ink,
     all from config/brand.php through the emitter. The hover-skill was first
     drafted against a purple/violet gradient; purple is RETIRED
     (BrandPaletteTest::RETIRED) and Omar's decision of 17 Aug is that the UI
     palette wins and the logo is allowed to differ. */

  --gt-blue: var(--blue);
  --gt-cyan: var(--cyan);
  /* V-40: the pink the site header's hairline already draws. Aliased here for
     the same reason blue and cyan are — a `--gt-*` token is what the sheet is
     allowed to read, and `SiteTokensTest` refuses one the system does not
     define. */
  --gt-accent-secondary: var(--accent-secondary);

  /* 🔴 THE ON-ARTWORK COLOURS — V-06 Batch 6, 22 Aug, AND THE REASON THEY ARE
     A SEPARATE FAMILY IS THE LIGHT THEME.

     V-06 put the title, the meta and the call to action back ON the picture,
     over the veil. The veil is the DARK ink in both themes (see --gt-scrim
     below: artwork does not get lighter because the page did), so at the FOOT
     of the frame a card is a dark context even on a light page — and every
     ordinary text token would walk straight into it and turn dark-on-dark.
     Measured on the rendered glyph, the title is 14.67:1 at 1536 and 15.00:1
     at 390, in both themes.

     🔴 AND "THE CARD IS A DARK CONTEXT" IS ONLY TRUE WHERE THE VEIL IS. It
     is a gradient — clear to 34%, .74 at 74%, .96 at the foot — so the answer
     depends on WHERE a thing sits, and the first version of this comment said
     it of the whole card. That cost a real defect: the TYPE MARK sits at 42%
     of the frame, where the veil is thin, over `--gt-media-placeholder` — so
     it is the one thing on this card whose polarity is decided by the
     PLACEHOLDER rather than by the veil, and Batch 6 pointed it at
     `--gt-on-ink` on the "the card is dark" generalisation and took it from
     5.09:1 to 2.05:1.

     🔴 AND THEN THE GROUND ITSELF MOVED, WHICH IS WHY THIS PARAGRAPH HAS NOW
     BEEN WRONG TWICE. Batch 6's repair was `--gt-on-accent-ink` — a DARK ink,
     correct against the bright gradient it was measured on. Omar's row-rhythm
     rule then put a .6 ink scrim into `--gt-media-placeholder`, and the ground
     under the mark went dark with it, so the dark ink was stranded and the
     mark reads `--gt-on-ink` again. Re-measured on this tree on 23 Aug,
     /games at 1536, the mark hidden so this is the composite alone: the ground
     is rgb(20,66,102), luminance .05004, IDENTICAL IN BOTH THEMES — which is
     the mechanism these tokens exist for, not a coincidence. A dark ink on
     that ground scores 1.82:1, same run. THE MARK'S OWN RENDERED-GLYPH RATIO
     IS NOT REPEATED HERE: it is measured and tabulated beside
     `.got-gamecard__initials`, and a second copy of a number is a number that
     can go stale on one side.

     So the rule these tokens exist for is NOT "the card is dark", and it is
     not "the mark is bright" either — that was the second wrong answer. It is:
     MEASURE THE GROUND WHERE THE THING SITS, choose the polarity from that,
     pin it here so the theme cannot move it, AND RE-MEASURE WHEN ANYTHING
     UNDER IT CHANGES. A colour chosen against a ground is only as current as
     the ground.

     So: `--on-accent`, `--on-accent-ink` and `--on-ink` are emitted by
     Brand::cssVariables() from the DARK palette's own keys and are
     deliberately ABSENT from its light block, which is the mechanism — a
     variable the light block never mentions cannot follow the theme.

     🔴 AND THAT ABSENCE IS GUARDED NOW, 23 Aug — IT WAS AN UNGUARDED PHP
     ARRAY. Every "does not follow the page" assertion in the suite searched
     THIS FILE's light blocks, which is not where the value is decided: one line
     added to Brand::lightBlock()'s map painted this mark the light palette's
     own near-black, rgb(20,26,38), on a light page — with the whole suite green
     and nothing anywhere able to say so. `GameCardTest::test_the_on_
     artwork_family_is_emitted_once_and_the_light_theme_never_names_it` derives
     the `--on-*` set from the emitter, pins it for exactness, and asserts the
     light block names none of it — and closes the naming convention from the
     other side by requiring every `--gt-on-*` alias below to read a member of
     that set.

     WHAT IS NOT HERE AND WHY. The round's brief also asked for `--on-soon`.
     `--soon` is ALREADY theme-independent — the light block does not override
     it either — so `--on-soon` would be a second name for a variable that
     already never follows the page, which is the second-copy shape this file
     exists to refuse. The soon treatment goes on reading `--gt-soon` and
     `--gt-tint-soon`, unchanged. */
  --gt-on-accent:     var(--on-accent);
  --gt-on-accent-ink: var(--on-accent-ink);
  --gt-on-ink:        var(--on-ink);
  /* 🔴 `--gt-on-warning` / `--gt-on-muted` AND THEIR TWO TINTS ARE DELETED —
     V-21 B2, 28 Aug. They existed for exactly one reader: the card-meta
     stock pill, pinned to the dark palette's values because it sat ON THE
     ARTWORK (V-18d's Critical — the theme-following amber measured 1.90:1
     there). V-21 B2 moved the product card's words, pill included, onto the
     CARD SURFACE — which the theme paints — so the pill wears the ordinary
     theme-scoped `.got-pill--muted` treatment again and the pinned family
     would now be the ORIGINAL BUG IN REVERSE: a dark-palette amber fixed
     onto a light card. Deleted with their emitter lines (Brand.php) and
     their four component rules, not orphaned: a token nothing reads is
     indistinguishable from a token somebody forgot to wire up.
     StoreSectionsTest holds the absence. */

  /* The signature gradient: accent → blue → cyan, at the diagonal the edge
     ring wants. (It used to say "the same three stops the site's own headline
     gradient uses"; that gradient was deleted in V-06 Batch 5 — see the note
     below — and a comment naming a caller goes stale the day one is removed
     just as surely as the day one is added.) Diagonal on purpose: a
     horizontal gradient does not
     mirror and needs a rule per caller (CLAUDE.md); a ring's diagonal reads
     the same in both directions and is checked rendered in both. */
  --gt-gradient-brand: linear-gradient(125deg, var(--gt-accent-soft), var(--gt-blue) 45%, var(--gt-cyan));
  /* 🔴 THE EDGE RAMP — V-40. `--gt-gradient-brand` above is the signature
     fill (accent-soft -> blue -> cyan at 125deg) and it carries no pink; the
     site header's own hairline draws blue -> cyan -> PINK and fades at both
     ends, and Study D asks for that ramp on the top-up card's top edge.

     It is a TOKEN rather than a second copy of the header's rule, because
     `site.css`'s `.site-header::after` writes that ramp as an inline literal
     and a third spelling of one gradient is how a palette becomes three.
     The header still carries its literal — converting it is a change to a
     guarded rule and is filed rather than smuggled in here. */
  --gt-gradient-edge: linear-gradient(90deg, transparent, var(--gt-blue) 16%, var(--gt-accent) 40%, var(--gt-accent-secondary) 74%, transparent);
  --gt-gradient-edge-rtl: linear-gradient(270deg, transparent, var(--gt-blue) 16%, var(--gt-accent) 40%, var(--gt-accent-secondary) 74%, transparent);

  /* 🔴 `--gt-gradient-headline` WAS DEFINED HERE AND IN BOTH LIGHT BLOCKS, AND
     ALL THREE ARE GONE — V-06 Batch 5, 22 Aug. It had exactly one reader,
     `.hero h1 .grad`, and the hero's accent half is a flat `--accent` now:
     three lines of gradient survive a batch that deleted their only caller and
     nothing goes red, because no test names a token. Its argument — that a
     gradient painting TEXT needs different stops from one painting an EDGE,
     since `accent-soft → blue → cyan` measures 1.5:1 and 2.2:1 on a light page
     — was right, and the reason to keep it written down is that the next
     gradient somebody puts on type will need it again. `--gt-gradient-brand`
     below is untouched: it paints an edge and a placeholder, both over
     artwork, in both themes. */

  /* 🔴 REST AND LIT — AND THE ARTWORK IS NO LONGER HELD BACK AT REST
     (V-05 Batch 1, 22 Aug, Omar's numbers).

     It used to be `--gt-media-dim: grayscale(38%) brightness(.62)`, on the
     argument written here for a week: hold the colour back so hover has
     somewhere to go. MEASURED ON THE RENDERED GRID, that argument buys one
     thing and costs another, and the trade is bad. What it costs is the
     page: seven posters at 62% brightness and 38% grey read as a page that
     has not finished loading, and the artwork — the thing this storefront
     is selling with — is the part being suppressed. What it buys is a hover
     reward on ONE tile at a time, on a pointer device, which nobody on a
     phone ever sees.

     So rest is now essentially the picture as shot: full saturation, a
     whisper under full brightness so the words on the card still win the
     page, and a touch of contrast. Hover is a RESTRAINED STEP UP from that
     rather than a reveal of something withheld.

     `--gt-media-dim` is GONE rather than retuned, and the rename is the
     point: the token said what it did, and what it does now is the opposite.
     A `--gt-media-dim` holding a value that dims nothing is how the next
     author writes a dimming rule and cannot find where it went.

     🔴 AND `--gt-media-dim-touch` IS GONE WITH IT, WHICH IS A DELETION AND
     NOT AN OVERSIGHT. It existed to say "on a phone there is no hover, so
     hold the artwork back LESS than on a pointer" — a second, softer answer
     to a question that only had to be asked while rest was dark. Rest is
     the picture now, in every input mode, so the touch value would be a
     rule that makes a phone WORSE than a laptop for no reason anybody could
     state. The whole `@media (hover: none)` block in the component went with
     it; the direction is "no desaturation anywhere at rest, on any card, in
     any state except --soon", and a touch screen at rest is a rest state.
     (That block came BACK in V-06, with different content and for a
     different reason — see `--gt-media-touch` immediately below and the
     component's own touch block. This paragraph is left in the past tense
     rather than deleted: the deletion was right about the card it was
     describing, and the card changed.)

     🔴 AND V-06 PUT `--gt-media-dim-touch`'S JOB BACK, UNDER A NEW NAME AND
     FOR THE OPPOSITE REASON. `--gt-media-touch` is NOT the deleted token
     returning: that one held the artwork BACK on a phone because rest was
     dark and there was no hover to give the colour back. It was born when
     V-06 hid the CTA until hover, so a hoverless screen needed the revealed
     state handed to it; V-13 Batch 5 then made the capsule rest-visible on
     EVERY pointer, and what this token hands a phone now is the COMPLETED
     state — full capsule opacity, because there is no hover to complete the
     reward with — with the artwork settled a whisper so the capsule reads
     over it. Reference's own value; the deletion argument in this comment
     stands for the state it was written about. */
  --gt-media-rest:      grayscale(0%) brightness(1) contrast(1.02);
  --gt-media-lit:       grayscale(0%) brightness(1.06) contrast(1.06) saturate(1.04);
  --gt-media-touch:     grayscale(0%) brightness(.94) contrast(1.03);
  /* Coming soon: dimmed HARDER than idle and it never lights — the tile is
     not a control, and the dim says so before the badge does.

     🔴 IT IS NO LONGER A GREYSCALE (V-06). It was `grayscale(70%)
     brightness(.45)`, which was right while the tile was a poster with the
     words underneath: the picture could be drained without draining
     anything else. The words sit ON it now, so draining the picture drains
     the ground the title and the badge are read against, and .45 under a
     .74 veil is a black rectangle with an orange pill on it. The direction
     is unchanged — a soon tile is visibly not a control — and it is now
     carried by BRIGHTNESS ALONE at the reference's .80, which keeps the
     artwork recognisable and keeps the orange the thing that says why. */
  --gt-media-dim-soon:  grayscale(0%) brightness(.80) contrast(1.03) saturate(.92);
  /* The placeholder behind a tile with no artwork of its own: the system's
     gradient, nobody else's picture. GAP-095 covers the real art.

     🔴 ITS BRIGHTNESS IS A MEASURED RULE NOW, NOT A TASTE CALL — Omar, 23 Aug:
     "the fallback's mean luminance must sit inside the range of the real tiles
     at rest, never outside it. A placeholder that reads brighter or darker
     than every real card breaks the row rhythm, which is the one thing a
     fallback must never do."

     IT WAS THE BRIGHTEST OBJECT ON THE PAGE. Measured on the rendered /games
     at 1536, dark, settled, dsf 1 — mean WCAG relative luminance over each
     tile's whole `__media` box, sampled from the SCREENSHOT so every alpha
     layer (veil, scanlines, sheen, the filter) is flattened by the compositor
     rather than by arithmetic:

       fc27 .02512 · call-of-duty .05887 · dota-2 .06189 · clash-royale .08136
       · valorant .08681        <- the five live artwork tiles, min to max
       gta-vi .09151            <- artwork under the coming-soon brightness(.80)
       ROCKET LEAGUE .24456     <- the fallback: 2.82x the brightest real tile

     THE RANGE IS THE FIVE LIVE TILES, and the coming-soon tile is reported
     beside it rather than counted in it: its dim is a SIGNAL that the tile is
     not a control, so a fallback landed against it would read "unavailable".
     It makes no difference to the answer here — .09151 is the wider ceiling
     and the landing sits well inside both.

     🔴 AND THE HEADLINE FIGURE IS MILDLY CIRCULAR — SAID HERE BECAUSE AN
     INSTRUMENT THAT REPORTS ITS OWN LIMIT IS WORTH MORE THAN ONE THAT IS NEVER
     WRONG. The fallback's `__media` box contains THE MARK, so .05538 is a
     reading of the material PLUS the ink this alpha is being used to justify.
     Re-run on this tree on 23 Aug, same box, same instrument, three arms:

       .05538  as it ships, with the light mark on it   <- the headline figure
       .05234  the same box with the mark hidden        <- material alone
       .05207  the same box with the mark forced dark

     So the ink moves the reading by .00331, which is 5% of the live range's own
     width (.02542 to .08681 in that run, .06139 wide), and ALL THREE ARMS LAND
     INSIDE THE RANGE. The circularity is real and it does not reach the
     conclusion — but a figure that includes its own subject should say so
     rather than be quoted as though it did not. (Those four numbers are from
     one run and are comparable to each other; they are not comparable to the
     table above, which is a different run — fc27 read .02542 there against
     .02512 here.)

     🔴 AND THE ALPHA IS A FUNCTION OF TODAY'S CATALOGUE, WHICH NOTHING
     RE-MEASURES. The ceiling is valorant at .08681 and the floor is fc27; five
     live covers decide both. Bundle one brighter poster and the range widens
     upwards with the fallback standing still — the landing stops being central,
     and far enough it could re-open the dark-ink option this token's own
     arithmetic closed. `GameCardTest` pins the .6 so a change to the ALPHA asks
     who re-measured; NOTHING asks when the RANGE moves, because no PHP test can
     sample a pixel. Adding artwork to `public/brand/games` is therefore a
     re-measurement job, and this paragraph is the only thing that says so.

     THE NUMBER. The scrim is ONE alpha and it is derived, not picked: .60 puts
     the fallback at .05538 against a range midpoint of .05596 — 0.0006 out.
     Sampling only the top 55% instead (the artwork above the veil's .74 stop,
     the same window applied to all seven) says the same thing from a different
     window: .07633 against a midpoint of .07778. Two windows, one alpha, and
     the choice between them does not move it.

     A FLAT FILL AND NOT A SHAPE: two identical stops, so the angle is inert
     and there is no direction to mirror on /ar. It reads `--gt-scrim` — the
     dark ink, in both themes — for the same reason the veil does: this
     material sits where artwork would, and artwork does not lighten because
     the page did.

     🔴 AND THE LIGHT THEME READS .05636, NOT .05538, WHICH IS NOT THIS TOKEN
     FOLLOWING THE PAGE. Every tile on the row moves by the same ~.00098 —
     call-of-duty .05887 -> .05981, fc27 .02512 -> .02607, valorant .08681 ->
     .08775 — including the six that are photographs and cannot be affected by
     this declaration at all. It is the card's own surface showing at the
     rounded corners of the sampled box, and the range moves with the fallback,
     so the verdict is INSIDE in both themes. Written down rather than rounded
     away: a comment claiming two numbers are identical when they differ in the
     fourth place is the kind of tidying that hides a real theme dependency the
     next time there is one. (The mark's contrast on this ground is in the type
     tile's own block; this alpha is what decides it.) */
  --gt-media-placeholder:
    linear-gradient(180deg, rgba(var(--gt-scrim), .6), rgba(var(--gt-scrim), .6)),
    linear-gradient(160deg, var(--gt-accent-soft), var(--gt-blue) 45%, var(--gt-cyan));

  /* The image's three sizes, and how far the card lifts — ALL RETUNED DOWN
     (V-05 Batch 1). The old set was a 6px lift and a 9% zoom, which is a
     large movement to put under a cursor on a grid where four tiles are
     within a few hundred pixels of each other; with the artwork already at
     full colour at rest, the hover no longer has to announce itself that
     loudly to be felt.

     REST IS EXACTLY 1 NOW, not a hair over. The 1.01 was there so the
     un-zoom back from hover never showed the frame's edge through a
     sub-pixel gap — a real problem at a 9% zoom, and one that a 3% zoom on
     an `object-fit: cover` image does not have. It also cost a permanent 1%
     crop of every poster for a defect that only ever appeared mid-transition.

     🔴 PRESS IS DERIVED, NOT PICKED, AND IT HAD TO MOVE OR THE PRESS WOULD
     HAVE INVERTED. The old three were 1.01 / 1.09 / 1.05 — press sits
     EXACTLY halfway between rest and hover, so a pressed card eases back
     towards its resting size while the card itself shrinks. Left at 1.05
     against a hover of 1.03, pressing a card would have zoomed the artwork
     FURTHER IN than hovering it, which is the opposite of a press. The
     midpoint of 1 and 1.03 is 1.015, and that is what this is.

     🔴 V-06 RETUNED ALL FOUR BACK UP, AND THE PRESS IS STILL DERIVED. The
     card is the artwork now — there is no body under it competing for the
     eye — so a 3% zoom on a poster that fills the whole tile is a movement
     you have to look for. The reference's numbers are a 6px lift and a 6%
     zoom over a rest that sits a hair over 1 (1.005, which is `cover`'s own
     insurance against a sub-pixel gap at the frame's edge appearing while
     the zoom eases back — the defect the old 1.01 existed for, at a zoom
     large enough to have it again).

     PRESS IS THE MIDPOINT OF REST AND HOVER, as it has been since V-05, and
     it MOVED because they did: (1.005 + 1.06) / 2 = 1.0325. Leaving it at
     1.015 would have put a pressed card BELOW its resting zoom, which is a
     press that pulls the picture away from the finger. The rule is the
     arithmetic, not the number. */
  --gt-media-zoom-rest:  1.005;
  --gt-media-zoom:       1.06;
  --gt-media-zoom-press: 1.0325;
  --gt-media-lift:       6px;
  /* A LIST row lifts less than a poster: two pixels, the same idea. */
  --gt-list-lift:        2px;

  /* 🔴 THE VEIL CARRIES CONTRAST AGAIN — V-06, 22 Aug, and this is the third
     time this token has changed job in a week, so both earlier states are
     kept rather than overwritten.

     Before V-02 it made TEXT LEGIBLE: the words were on the picture, so the
     ink reached .96 at the foot and started climbing at 30%. V-02 moved the
     words onto a card under the frame, the veil stopped carrying any
     contrast, and it shrank to "transparent to 58%, .48 at the foot" — a
     grounding, plus an accent wash that tied a photograph to the palette.

     V-06 PUT THE WORDS BACK ON THE PICTURE, so the contrast job is back and
     the small gradient cannot do it. These are the reference's stops: clear
     to 34%, .74 at 74%, .96 at the foot. What the reference does NOT have is
     the accent wash, and it is deleted here rather than kept: a coloured
     wash over text is a second variable in every contrast measurement taken
     on this card, and the veil is the one thing standing between a title and
     an arbitrary photograph. The wash SURVIVES on the soon tile below, where
     the colour is the message rather than a decoration.

     THE NUMBERS THIS BUYS, as arithmetic against the WORST CASE — a pure
     white frame under the veil, which is the brightest ground any artwork
     can put behind the words:

       veil .96 (the foot)   title 14.22:1   meta 8.63:1   accent 10.81:1
       veil .74 (the 74% stop) title 6.77:1  meta 4.71:1   accent  5.15:1

     Every one clears AA, including the meta at its own worst point. This is
     ARITHMETIC on a flattened composite, not a pixel sample: the sample on
     the rendered grid is the verifier's, and it is the one that counts.
     Alpha needs channels, which is why it reads --gt-scrim rather than a hex
     token. */
  /* 🔴 THE SCRIM IS THE DARK INK IN BOTH THEMES, AND IT HAD TO STOP READING
     THE PAGE'S. `--got-ink` is the rgb triple of the PAGE, and the veil used
     it because on a dark site those are the same colour. They are not the same
     colour on a light one: a light page would have painted a WHITE scrim over
     every poster, which is a fog, not a grounding. Artwork does not get
     lighter because the page did. `--got-ink` is emitted from `brand.ink` and
     the light block deliberately does not override it, so this is one value in
     one place and the light theme simply never touches it. */
  --gt-scrim: var(--got-ink);
  --gt-media-veil:
    linear-gradient(180deg, rgba(var(--gt-scrim), 0) 34%, rgba(var(--gt-scrim), .74) 74%, rgba(var(--gt-scrim), .96) 100%);
  /* The soon tile's veil: the same three ink stops, and the wash that the
     live tile just lost — the soon orange (19 Aug). It stays here for the
     reason it was deleted there: on this tile the colour IS the message, and
     the only words it has to carry are a title and a badge that is itself
     that colour. */
  --gt-media-veil-soon:
    linear-gradient(180deg, rgba(var(--gt-scrim), 0) 34%, rgba(var(--gt-scrim), .74) 74%, rgba(var(--gt-scrim), .96) 100%),
    linear-gradient(200deg, rgba(var(--soon-rgb), .26), transparent 50%);
  /* The soon tile's DECORATIVE hover: a soft orange edge glow, opacity only.
     INSET, because the tile clips its overflow (the artwork's frame) and an
     outer glow would never be seen — measured: only the 1px ring survived. */
  --gt-media-glow-soon:
    inset 0 0 0 1px rgba(var(--soon-rgb), .70),
    inset 0 0 32px 4px rgba(var(--soon-rgb), .32);

  /* The scanline texture — the "gaming" garnish. The layer's OPACITY is
     folded into the gradient stop, so the component never dims itself with
     `opacity`, which SiteTokensTest forbids for a reason (BUG-241: opacity
     fades text and fill together, so a measured contrast stops being the
     figure on screen). The pitch — one line every 3px — is a fact about the
     texture and lives here with the other raw values.

     V-06 states its layer opacities as .14 at rest and .22 lit, so the fold
     is white .10 x .14 = .014 and white .10 x .22 = .022. The fold is
     arithmetic on the two numbers the reference writes, and it is written
     out here because the folded value is the one nobody can read back. */
  --gt-media-scanline:     repeating-linear-gradient(0deg, rgba(255, 255, 255, .014) 0 1px, transparent 1px 3px);
  --gt-media-scanline-lit: repeating-linear-gradient(0deg, rgba(255, 255, 255, .022) 0 1px, transparent 1px 3px);

  /* The one-shot light sweep. White then cyan, so it reads as light and not
     as a tint — and the cyan is the ON-ARTWORK accent, not the page's. It
     read `--accent-rgb` until V-06, which the light block DOES override: on
     a light page the sweep across a poster would have been the dark teal
     `accent_on_light`, over dark artwork. The same defect the veil's scrim
     was fixed for, one layer up. */
  --gt-media-sheen: linear-gradient(90deg, transparent, rgba(255, 255, 255, .16) 45%, rgba(var(--on-accent-rgb), .30) 55%, transparent);

  /* 🔴 THERE IS NO HOVER-GLOW TOKEN, AND ITS ABSENCE IS A RULING RATHER THAN
     AN OMISSION. The V-06 study gives the hovered card a
     `0 22px 48px -18px` blue throw and a cyan hairline, and this block did
     carry a `--gt-media-glow` for about an hour. Omar's resolution #8 of
     17 Aug, re-ruled on 22 Aug against V-06's own reference, is quoted in
     full at the hover rule in the component below and says in part: "the
     hairline border under it never changes; no lift shadow." So the token
     was DELETED rather than left defined and unused — a value nothing reads
     is indistinguishable from a value somebody forgot to wire up, and the
     next author would wire it up. The lift is `transform` alone; the reward
     is the edge, the sheen, the zoom and the capsule. */

  /* 🔴 THE CALL TO ACTION, ON THE ARTWORK — V-06 Batch 3, the CAPSULE.
     A compact pill: the on-artwork accent on its own tint at rest, solid
     with ink words on hover and focus. The reference offers four treatments
     behind a `data-cta` switch on its wrapper; three of them are prototype
     comparisons and only the capsule ships, unconditionally, so there is no
     attribute selector here and no `bar`/`underline`/`glass` to inherit.

     Arithmetic on the worst case (white artwork, .96 veil): the accent on
     its own .13 tint is 8.14:1, and the ink on the solid fill is 11.53:1.
     The .42 border against that tint is 2.21:1 — it is a decoration inside
     a control whose real boundary is the fill, not the control's only
     boundary, so WCAG 1.4.11's 3:1 is not the figure that applies to it. */
  --gt-media-cta-fill: rgba(var(--on-accent-rgb), .13);
  --gt-media-cta-line: rgba(var(--on-accent-rgb), .42);
  /* V-13 Batch 5 (Omar, 24 Aug): the capsule is PRESENT AT REST on every
     pointer — quieter on a hover-capable one, full on touch — so the rise
     token that revealed it is gone (deleted, not orphaned: zero readers,
     and its reduced-motion handling went with it). This is the capsule's
     resting weight; hover takes it to 1, which is what keeps hover a
     REWARD rather than a reveal. Measured at rest on the rendered /games
     grid at .72, label vs the capsule's own sampled ground, 24 Aug: CoD
     5.44, FC27 5.09 (the worst), Valorant 5.50, Dota 5.44, Clash 5.28,
     Rocket League (type tile) 5.25 — identical in both themes, because the
     capsule sits on artwork and artwork does not follow the page. */
  --gt-media-cta-rest: .72;
  /* The arrow's nudge. `translateX()` is physical and has no logical form,
     so like --gt-sweep-to it is a custom property the [dir] rule flips —
     and unlike --gt-sweep-to it needs a SECOND token to flip TO, because a
     component rule may not carry a raw `-3px` (SiteTokensTest's px ban
     reaches every property, and the sweep only escapes it by being a
     percentage). Both values live here, where raw values belong. */
  --gt-media-arrow-nudge:      3px;
  --gt-media-arrow-nudge-rtl: -3px;

  /* 🔴 THE TILE TITLE IS ITS OWN SIZE NOW, AND IT IS A TOKEN BECAUSE A MEDIA
     QUERY MUST REWRITE IT RATHER THAN RESTATE THE RULE (V-06 Batch 3).

     Until V-06 the tile head and the card head were ONE ROLE at
     --gt-text-lg / --gt-weight-heavy, asserted by TypeRegisterTest, and that
     was right while both were words on a card's own surface. The tile head
     is now printed ON the artwork, uppercase, leaning, one line with an
     ellipsis, and it scales with the tile — because the text sits on the
     picture, a bigger title costs no card height at all, which is the whole
     reason the reference lets it move. The card head is untouched.

     Three steps, the reference's: 16/20 on a phone, 17/21 from 700px, 18/22
     from 1200px. 17px is not a step of the type scale and is not being added
     to it — this is ONE component's own size, declared once, and the two
     breakpoints below rewrite the token rather than repeating the rule. That
     is the shape CLAUDE.md asks for after the reduced-motion list-of-classes
     lesson: put the value in a token and let the query move the token. */
  --gt-text-tile:    1rem;
  --gt-leading-tile: 1.25;

  /* The meta line under the title: 12px, the micro-label's size but not its
     treatment, at .76 of the on-artwork ink. Its own token because a raw
     rgba() may not appear in a component rule. */
  --gt-media-meta: rgba(var(--on-ink-rgb), .76);

  /* A ring mask: paints the padding box and cuts the content box out of it,
     so a gradient background becomes a gradient BORDER that can fade. The
     colour is irrelevant — a mask reads alpha — and is written as a keyword
     so no hex appears anywhere near it. */
  --gt-mask-ring: linear-gradient(black 0 0) content-box, linear-gradient(black 0 0);

  /* Text over artwork needs a shadow to survive a bright frame; the two
     text-shadows in the system, and both are ink, not black.

     The first is the TYPE MARK's — a large soft halo behind a 36px letter
     form standing on an ungoverned gradient. The second is the CAPTION's,
     and V-06 is what needed it: a tight 1px offset under the title and the
     meta, which is a different job (an edge, not a halo) at a size where a
     14px blur would smear. The reference writes .45 for the title and .40
     for the meta; that is one value written twice — the two are 2.5% apart
     on an alpha that exists to break a tie against unknown pixels — so it
     is ONE token here and both read it. */
  --gt-media-text-shadow: 0 2px 14px rgba(var(--gt-scrim), .7);
  --gt-media-caption-shadow: 0 1px 2px rgba(var(--gt-scrim), .45);
}

/* THE TILE TITLE'S TWO BREAKPOINTS. Plain media queries and not container
   queries: the reference is written inside a 1536px `.frame` that declares
   `container-type: inline-size`, and production has no such wrapper — the
   catalogue is the full width of the page, so the viewport IS the container
   and a media query is the honest instrument. (CLAUDE.md's warning about
   media queries is about a PANEL widget inside a sidebar, which this is not.)

   The token moves; the rule below never does. */
@media (min-width: 700px) {
  :root {
    --gt-text-tile:    1.0625rem;
    --gt-leading-tile: 1.235;
  }
}
@media (min-width: 1200px) {
  :root {
    --gt-text-tile:    1.125rem;
    --gt-leading-tile: 1.222;
  }
}
/* THE PRICE STEP'S ONE BREAKPOINT — V-27 B2. Same shape as the tile's two
   above; the argument for the pair (Omar's 40/32, verbatim) is on the token.
   56.25em and not 700px/1200px because that is the band the gift page's own
   layout already turns on — the price and the two-column grid move together
   or the phone bar shows a desktop size for the last few pixels before the
   columns arrive. */
@media (min-width: 56.25em) {
  :root {
    --gt-text-price: 2.5rem;   /* 40px */
  }
}


/* 🔴 THE REDUCED-MOTION BRANCH — ONE BLOCK, WHOLE SYSTEM.

   It rewrites the DURATION TOKENS rather than setting `transition:none` on a
   list of classes, so it covers every component in this file and every one
   added to it later. That is the property a list cannot have.

   1ms and not 0: a zero-duration transition never fires `transitionend`, and
   0.01ms is the widely-copied value for exactly that reason. Nothing in this
   system listens for that event today — the animation-driven parts are the
   loading ring and the toast — but a duration of 1ms is indistinguishable to
   a human and leaves the event intact for whatever comes next.

   The loading ring's SPIN is stopped separately where it is defined, because
   a spinner is not a transition and shortening its duration would make it a
   strobe. This block rewrites the duration tokens, so it reaches whatever
   reads them; an `animation` with its own duration does not read them and is
   not reached here. */
@media (prefers-reduced-motion: reduce) {
  :root {
    --gt-duration-instant: 1ms;
    --gt-duration-fast:    1ms;
    --gt-duration-base:    1ms;
    --gt-duration-slow:    1ms;
    --gt-duration-sweep:   1ms;
  }
}
/* Below the header's breakpoint the row is one line — brand, ☰, basket,
   balance, account (Batch 2) — AND, SINCE V-30 B5, A SECOND LINE UNDER IT
   holding the pill navigation, because the first line was already full:
   decision #3's ladder (V-30_BLOCKED.md) was walked on the rendered page
   at 390×844 and stopped at its step 5. MEASURED, both languages, signed
   out and in, hover:none true (B5_nav/README.md): three text tabs on the
   first line overflow it by 192–264px; three 44px icon pills still
   overflow it by 24px for an English guest (39 with a 15px scrollbar) and
   52–60px signed in; the balance off the row leaves the guest row as full
   and the signed-in row 9px clear (0.06 over with the scrollbar); the
   language pair is not on this row to compact.

   So this band's token is its own number, not the wide row's: two lines of
   44px targets, the pill's hairline top and bottom, the row's 8px padding
   above and below and the 8px gap between the lines — 8 + 44 + 8 + 46 + 8
   = 114 of content under a `min-block-size` of 7.25rem − 1px = 115, so the
   TOKEN binds by one pixel and the header's border-box is the token:
   MEASURED 116.00 at 390×844, both languages, signed out and in (the row
   115.00; the first line's targets at y 9.5, the pill's line at 60.75).
   The offset derives exactly as it does above (it was the literal 5rem —
   80 over a 61px bar — until V-30 B4) and is 136 on the phone now; the
   toast and the skip link's landing follow it. One consequence of the
   wrap, measured and accepted: a DESKTOP browser 320 wide with a 15px
   scrollbar and a signed-in row (273.7 of content in a 273 box) folds the
   account cluster onto a third line — 167px — where the brand used to give
   the 0.7px; a phone at 320 has 288 and does not wrap, and a desktop
   window cannot be made that narrow (INFERRED, not measured here). 52em
   is the header's own line, drawn in THE HEADER MENUS. */
@media (max-width: 52em) {
  :root {
    --gt-header-height: 7.25rem;
    --gt-scroll-offset: calc(var(--gt-header-height) + var(--gt-space-6));
  }
}
/* V-33 B3 — the basket line's artwork steps down on a phone (see the token
   for why both axes are written). 32.5em = 520px, the band the header's
   `.container--shelf` already uses for its narrowest padding. */
@media (max-width: 32.5em) {
  :root {
    --gt-cart-art-inline: 4rem;
    --gt-cart-art-block: 6rem;
  }
}
/* The pill's tabs step down where three of them would not fit the row:
   at 320 the row's content box is 288 — 273 in a desktop browser with a
   15px classic scrollbar — and three 7rem tabs are 338. 5.625rem × 3 + the
   pill's two hairlines = 272, under both. 24em (384px) is the line: above it the phone
   row keeps the 7rem tab — MEASURED at 390: 338 in 358, and 338 in 343
   with the scrollbar. MEASURED below the line, both languages: 375, 360
   and 320 (phone) render 90px tabs, a 272px pill, no document overflow;
   a desktop viewport 320 wide with the scrollbar holds 272 in 273. The 2px
   document overflow that viewport does show (307/305) is the home page's
   card grid, present with the pill hidden — not the header's. */
@media (max-width: 24em) {
  :root {
    --gt-tabs-tab-size: 5.625rem;
  }
}
/* 🔴 THE PHONE'S OWN VEIL IS DELETED HERE — V-06, and it is a DELETION
   rather than a retune because all three of its reasons had stopped being
   true and the last one was doing measurable harm.

   It read, at `@media (max-width: 40em)`:

     --gt-media-veil:
       linear-gradient(180deg, rgba(var(--got-ink), 0) 18%,
                       rgba(var(--got-ink), .78) 62%,
                       rgba(var(--got-ink), .96) 100%),
       linear-gradient(200deg, rgba(var(--accent-rgb), .20), transparent 46%);

   and its comment argued: "At 4:3 the body sits higher on the frame than at
   3:4, on a part of the veil that has less ink under it."

     · THE PREMISE IS GONE TWICE OVER. V-02 made the tile 2:3 at EVERY width
       — there is no 4:3 phone tile for a body to sit high on — and V-06 moved
       the body ONTO the artwork, anchored to the foot in every viewport. The
       comment described a layout two rounds dead.
     · IT CARRIED THE ACCENT WASH V-06 DELETED, at a HEAVIER .20, and it read
       `--accent-rgb`, which the light block DOES override. So the wash the
       live veil dropped survived on phones only, in the one theme nobody
       tests, painting the light palette's dark teal over a poster. That is
       BUG-096's family exactly — an override that only fires where nobody
       looks — and it is the defect Batch 6 exists to close, still breathing
       behind a breakpoint.
     · IT SPELLED THE SCRIM `--got-ink` INSTEAD OF `--gt-scrim`, so the one
       decision "the scrim is the dark ink in both themes" had two spellings
       and only one was findable by grepping the token.

   AND IT WAS WHY NO SINGLE MARK COLOUR COULD WORK. Measured with the mark
   hidden, the ground under it: at 1536 rgb(33,142,217), luminance .2468; at
   390 WITH this override, rgb(25,96,149), luminance .1074. The ground crossed
   the light/dark threshold between two widths of the SAME card, so no single
   mark colour could clear AA large at both — measured on the rendered glyph
   with the override in place, the light mark read 2.05 at 1536 and 1.80 at
   390, and the dark mark read 5.43 and 2.86. With the override gone the phone
   reads the same veil as every other width, the crossover goes with it, and
   the dark mark reads 5.43 / 5.11.

   THE COST OF DELETING IT WAS MEASURED, NOT ASSUMED: this veil was buying the
   title 0.14 of contrast at 390 (15.14 → 15.00) and the meta 0.01
   (9.16 → 9.15), and costing the mark 3.31. The full before/after for all
   four elements is beside `.got-gamecard__initials`. */



/* ==========================================================================
   1b · LIGHT MODE — the four things a palette swap cannot fix (V-02, 22 Aug)
   ==========================================================================

   🔴 DARK IS PRIMARY. Everything above is the dark theme and none of it moved.
   `config/brand.php` carries a `light` block and `Brand::cssVariables()` prints
   it into the page, so every one of the forty-odd `--gt-*` COLOUR tokens above
   follows the palette without a line here — that is what makes a second theme
   tractable at all, and it is the whole payoff of the alias rule
   (`SiteTokensTest::test_every_colour_token_reads_from_the_brand_source`).

   FOUR TOKENS ARE NOT COLOURS THOUGH. They are DARK-MODE ASSUMPTIONS written as
   values, and a palette swap leaves every one of them wrong:

     --gt-wash-soft / --gt-wash-strong   WHITE at 4% and 7% — "the surface plus
                                         light". Light on light is invisible.
     --gt-lift                           a white INSET HIGHLIGHT along the top
                                         edge. White cannot be lit further.
     --gt-press                          black at 18% — far too heavy a bruise
                                         on a white control.
     --gt-shadow-1..4                    black at 35-65%, tuned against a
                                         near-black ground where a shadow has
                                         to fight to be seen at all.

   THE LIGHT SOURCE IS STILL ABOVE and that is what keeps this a designed
   equivalent rather than an inversion. What changes is which half of the cue
   carries it: on a dark ground a raised surface is LIT along its top edge and
   the shadow is almost decorative; on a light ground it is the SHADOW that
   separates the card and there is no highlight to give. So `--gt-lift` becomes
   a whisper of a drop shadow rather than an inset — `--gt-level-base` stays
   meaningful, `.got-card--panel` still reads as an object, and no rule below
   had to change.

   The selectors are the emitter's, for the same two reasons: the media query
   carries `:not([data-theme="dark"])` so an explicit dark choice can beat the
   system, and the attribute block is (0,1,1) so an explicit light choice beats
   the dark defaults outright.

   WHAT IS DELIBERATELY NOT THEMED, and it is the same list either way: the
   media surfaces. `--gt-media-veil`, `--gt-media-scanline`, `--gt-media-sheen`,
   `--gt-media-placeholder` and `--gt-media-text-shadow` all sit ON ARTWORK, and
   artwork does not get lighter because the page did — which is why the veil now
   reads `--gt-scrim` (the dark ink, in both themes) instead of the page's own.

   🔴 AND THAT SENTENCE WAS FALSE WHEN IT WAS FIRST WRITTEN, which the round's
   verifier proved rather than argued. `--gt-media-placeholder` and
   `--gt-gradient-brand` both read `--gt-accent-soft`, and the light block was
   overriding `--accent-2` underneath it — so the "not themed" gradients were
   being themed, and a game with no cover started its gradient at a dark teal on
   a light page. The fix is upstream of this list: `--gt-accent-soft` is an
   artwork colour and is no longer overridden, and the button hover that wanted
   a darker step got `--gt-accent-hover` of its own. A comment stating a
   mechanism it has not measured is the defect this file names most often.
   ========================================================================== */

@media (prefers-color-scheme: light) {
  :root:not([data-theme="dark"]) {
    --gt-accent-hover: var(--gt-accent-strong);
    /* The module surface: the light ladder's top step is `raised` (white) and
       `overlay` is the hover wash below the page — the argument is on the
       alias in the token block. */
    --gt-surface-module: var(--gt-surface-raised);
    /* 🔴 ONLY THE ALPHAS AND THE LIFT'S SHAPE. The COLOURS come from the
       emitter — `--wash-rgb` and `--elev-rgb` are white and black on dark and
       the light palette's own ink on light — so there is ONE ramp with two sets
       of alphas rather than two ramps that can drift. The first version of this
       block wrote `rgba(20, 26, 38, …)` eight times, which is the light ink as
       a literal in a stylesheet: the exact thing this system forbids, added by
       the round that was building the theme. */
    --gt-press: rgba(var(--elev-rgb), .08);
    --gt-shadow-1: 0 1px 2px rgba(var(--elev-rgb), .07);
    --gt-shadow-2: 0 4px 12px -2px rgba(var(--elev-rgb), .10);
    --gt-shadow-3: 0 12px 28px -6px rgba(var(--elev-rgb), .14);
    --gt-shadow-4: 0 24px 56px -12px rgba(var(--elev-rgb), .18);
    /* No highlight to give on white: the shadow is the whole cue, so the lift
       stops being an INSET and becomes the faintest drop. Same light source,
       same "above" — the other half of the cue carries it. */
    --gt-lift: 0 1px 1px rgba(var(--elev-rgb), .05);
  }
}
:root[data-theme="light"] {
  --gt-accent-hover: var(--gt-accent-strong);
  --gt-surface-module: var(--gt-surface-raised);
  --gt-press: rgba(var(--elev-rgb), .08);
  --gt-shadow-1: 0 1px 2px rgba(var(--elev-rgb), .07);
  --gt-shadow-2: 0 4px 12px -2px rgba(var(--elev-rgb), .10);
  --gt-shadow-3: 0 12px 28px -6px rgba(var(--elev-rgb), .14);
  --gt-shadow-4: 0 24px 56px -12px rgba(var(--elev-rgb), .18);
  --gt-lift: 0 1px 1px rgba(var(--elev-rgb), .05);
}


/* ==========================================================================
   2 · COMPONENTS

   Every rule below consumes tokens and nothing else. No hex, no px font size,
   no px spacing — `SiteTokensTest` asserts that by parsing this section.

   The permitted px exceptions are the token definitions above (radii, border
   weights, the tap minimum), and they are above the boundary the test uses.
   ========================================================================== */


/* --------------------------------------------------------------- BUTTONS ---

   FOUR KINDS, FIVE STATES EACH, and the states are real rather than a hover
   filter. `filter: brightness(1.06)` is what this site had on every button:
   one declaration standing in for four different hover treatments, which is
   why the quiet button's hover was invisible (brightening a transparent
   background brightens nothing).

     primary      — the one action on the screen. Accent fill, dark text.
     secondary    — a real alternative. Bordered, surface fill.
     quiet        — a minor action beside a real one. No fill until hovered.
     destructive  — it removes something. Danger edge, never a danger fill:
                    a solid red button is the same weight as the primary and
                    invites the press it should be discouraging.

   `.got-btn` and its modifiers are the names the site ALREADY uses in nine
   templates, so taking them over here restyles every existing button rather
   than leaving a second set beside them. That is also why the class names
   keep the `got-` prefix instead of taking a new one: a second prefix would
   be a second system, which is the thing this round exists to end. */
.got-btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: var(--gt-space-3);

  min-block-size: var(--gt-tap-min);
  padding-block: var(--gt-space-3);
  padding-inline: var(--gt-space-6);

  border: var(--gt-border-hair) solid transparent;
  border-radius: var(--gt-radius-md);

  font-family: var(--gt-family);
  font-size: var(--gt-text-sm);
  font-weight: var(--gt-weight-bold);
  line-height: var(--gt-leading-snug);
  text-align: center;

  cursor: pointer;
  user-select: none;
  transition: var(--gt-transition-control);
}

/* The lift is on the two FILLED kinds only. A quiet button has no body for
   the light to catch, and putting a highlight on a transparent box draws a
   1px line across the page. */
.got-btn--primary {
  background: var(--gt-accent);
  color: var(--gt-ink-on-accent);   /* 11.74:1 */
  box-shadow: var(--gt-level-elevated);
}
.got-btn--primary:hover {
  background: var(--gt-accent-hover);
  box-shadow: var(--gt-level-hover);
}
.got-btn--primary:active {
  background: var(--gt-accent-strong);
}

.got-btn--secondary {
  background: var(--gt-surface-overlay);
  color: var(--gt-ink);             /* 12.50:1 */
  border-color: var(--gt-line-strong);
  box-shadow: var(--gt-level-elevated);
}
/* 🔴 THE WASH IS A `background-image`, NOT A `background-color`, and that is
   the only way this hover could be real.

   The first version re-declared `background: var(--gt-surface-overlay)` here —
   THE SAME VALUE THE REST STATE ALREADY HAS. It read as a change and was a
   no-op; the whole hover was carried by the border and a shadow step, which on
   a dark UI is close to nothing. Caught by reading the rule back, not by a
   test: a declaration that sets a property to what it already is cannot fail
   anything.

   A state layer fixes it without inventing a fifth surface. `background-image`
   paints OVER `background-color`, so one wash lightens whatever the kind
   happens to be sitting on and there is no "surface-overlay-but-hovered" token
   to keep in step with the ladder. Same technique, same one line, if a fifth
   button kind ever needs it. */
.got-btn--secondary:hover {
  background-image: linear-gradient(var(--gt-wash-soft), var(--gt-wash-soft));
  border-color: var(--gt-accent);
  box-shadow: var(--gt-level-hover);
}
/* 🔴 THE PRESS GOES DOWN THE LADDER AND ON A LIGHT GROUND IT WENT UP — found
   by the round's verifier, 22 Aug.

   These were TWO `:active` rules: one took the hover wash off, the other set
   the background to `--gt-surface-raised`. On dark that is overlay #1E2434 →
   raised #171B28, which is darker, and the comment beside it said exactly what
   it was for: "the control is going into the page, so it must not also be
   getting lighter". On LIGHT the same two surfaces are #F4F7FC → #FFFFFF, and
   the button got BRIGHTER on press — the rule was written against the dark
   ladder's ORDER, and the light ladder's top is white.

   One rule now, and it reads the system's own press wash rather than a step of
   a ladder whose direction is not the same in both themes. `--gt-press` is
   black at 18% on dark and the light palette's own ink at 8% on light: BOTH
   darken, because "into the page" is the same idea either way. It is the same
   mechanism `.got-btn--secondary:hover` already uses one line up — a state
   layer over whatever the control is sitting on — which is why there is no
   "surface-overlay-but-pressed" token to keep in step. */
.got-btn--secondary:active {
  background-image: linear-gradient(var(--gt-press), var(--gt-press));
}

/* Quiet is MUTED and not faint. 6.53:1 on raised — it is a real control that
   a person has to be able to read, and "quiet" is about visual weight, not
   about being hard to see. This is BUG-118's lesson as a rule.

   🔴 ITS EDGE READS `--gt-edge-color` — V-35 B3 fix pass, 6 Sep 2026, and it
   is the same one-token idiom `.got-btn--destructive` above uses and the
   fields use. The ABSENCE of the token is the neutral state, so the fallback
   `--gt-line-strong` is the quiet edge everywhere no state supplies one and
   nothing on the site changes colour by this line alone. What it buys is the
   thing a state CANNOT get any other way: `.got-pay__copy.is-copied` and
   `.is-failed` say "accent" and "amber" by declaring the token, and the edge
   arrives here — at (0,1,0) and (0,2,0), BOTH of which `.got-btn:focus-visible`
   below out-specifies or beats on source order, so while the control is
   focused the border joins the band and the state colour is painted ONCE, by
   the ring. Written in the state's own stylesheet instead, that declaration
   was (0,2,0) in a sheet that loads later, it beat the focus rule, and the
   control drew an amber edge under an amber ring — BUG-247, measured. There
   is no per-state exception here and no second ring recipe (BUG-508); the
   only thing a sixth state on a quiet control has to do is declare the two
   tokens.

   THE HOVER RULE READS IT TOO, and that is not decoration either: without it
   `.got-btn--quiet:hover` (0,2,0) would repaint the neutral edge over a state
   colour that came from (0,1,0), and the accent or amber a copy button just
   earned would vanish under the pointer that earned it. */
.got-btn--quiet {
  background: transparent;
  color: var(--gt-ink-muted);
  border-color: var(--gt-edge-color, var(--gt-line-strong));
}
.got-btn--quiet:hover {
  background: var(--gt-wash-soft);
  color: var(--gt-ink);
  border-color: var(--gt-edge-color, var(--gt-line-strong));
}
.got-btn--quiet:active {
  background: var(--gt-press);
}

/* Destructive: the danger is in the EDGE and the TEXT, and the fill only
   arrives on hover — and even then as a tint, not a solid. Danger as text on
   raised is 4.56:1, which clears AA; on the tint it would be 4.07 and would
   not, so the hover state moves the text to --gt-ink. Both measured. */
.got-btn--destructive {
  background: transparent;
  color: var(--gt-danger);          /* 4.56:1 on raised */
  border-color: var(--gt-edge-color);
}
.got-btn--destructive:hover {
  background: var(--gt-tint-danger);
  color: var(--gt-ink);             /* 12.36:1 on the tint */
  border-color: var(--gt-edge-color);
}
.got-btn--destructive:active {
  background: var(--gt-tint-danger);
  color: var(--gt-ink);
}

/* ONE RING, EVERY BUTTON, EVERY BACKGROUND. See --gt-ring-* above for the
   measurement that makes the second tone necessary. `:focus-visible` and not
   `:focus`, so a mouse press does not leave a ring behind it.

   The border joins the band here for the same reason it does on a field
   (BUG-247): `.got-btn--destructive` paints its edge `--gt-danger` and now
   rings in it, so focused it drew the same doubled outline — a red 1px edge,
   the dark band, a red 2px ring. Written on `.got-btn` and not on the
   destructive variant on purpose: the invariant is "a focused control paints
   its state colour once", and a rule that named the one variant known to
   break it would be the list-of-selectors shape this file exists to end. On
   the other three kinds it moves an edge nobody can distinguish from the band
   behind it — transparent on primary, `--gt-line-strong` on secondary and
   quiet — and the moat goes from 2px to 3px, which is what a focused field
   does too. */
.got-btn:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  border-color: var(--gt-ring-band);
  box-shadow: var(--gt-ring-shadow);
}

/* Disabled reads as OFF rather than as faded: no lift, no shadow, no pointer,
   and the cursor says so. `not-allowed` rather than `default` because a
   disabled control that looks pressable and does nothing is the complaint.

   🔴 IT IS NOT A PROTECTION. `disabled()` is decoration in this project by
   standing rule, learned 7 Aug — the server refuses, always. This is what the
   refusal looks like, not the refusal. */
.got-btn:disabled,
.got-btn[disabled],
.got-btn[aria-disabled="true"] {
  background: var(--gt-surface-sunken);
  color: var(--gt-ink-muted);
  border-color: var(--gt-line);
  box-shadow: var(--gt-shadow-0);
  transform: none;
  cursor: not-allowed;
  /* Not `opacity`, which would drag the label under AA along with the box.
     --gt-ink-muted on --gt-surface-sunken is 6.74:1 and stays legible: a
     person has to be able to READ what they cannot press. */
}

/* A button that fills its column. A modifier rather than a caller writing
   `inline-size:100%`, so the one-off does not become nine one-offs. */
.got-btn--block {
  inline-size: 100%;
}

/* ------------------------------------------- ICON-ONLY BUTTON — V-33 B3 ---

   A square at the tap minimum, worn BESIDE a kind (`got-btn--quiet
   got-btn--icon`), so the fill, the hover, the press, the ring and the
   disabled treatment are the kind's and cannot drift from the labelled
   buttons around it. It changes only the geometry: the width joins the
   `.got-btn` height at `--gt-tap-min` and the inline padding goes, because
   there is no label to pad. The accessible name is the button's `aria-label`,
   published from Blade through `__()` — an icon-only control with no name is
   announced as "button".

   The glyph is sized BY CLASS (`.got-btn__glyph`), never by a bare `svg`
   rule: sizing a bare element from a system rule is BUG-235's shape and
   `SiteTokensTest` names it. `--gt-space-6` (20px) inside 44 leaves the
   stroke breathing room the trash and the stepper's arithmetic signs both
   measured right at. Two callers today — the basket's trash and its two
   stepper buttons — which is one more than a one-off. */
.got-btn--icon {
  inline-size: var(--gt-tap-min);
  padding-inline: 0;
  flex: 0 0 auto;
}
.got-btn__glyph {
  inline-size: var(--gt-space-6);
  block-size: var(--gt-space-6);
  flex: 0 0 auto;
}

/* 🔴 BUG-241 — THE SUBMIT BUTTON, BROUGHT IN RATHER THAN PATCHED.

   `.got-submit` lived in site.css as three rules on top of a system button:
   full width, `cursor: progress` while submitting, `filter: saturate(.6)` when
   busy — and `opacity: .75` on `:disabled`.

   THE OPACITY WAS THE DEFECT. `public/js/form-validation.js` sets
   `button.disabled = true` and adds `.is-busy` on submit, so this state is on
   screen for every registration. Opacity fades the text AND the background
   together, so the figure on screen is not the figure the declarations
   suggest: MEASURED at 4.33:1, below the 4.5:1 floor, where the system's own
   disabled button is 6.74:1. That is the third time this project has been
   caught by opacity on a control — BUG-118, then the pager step at 3.52:1
   under a comment quoting 6.5:1, now this.

   It is fixed by DELETION, not by choosing a lighter grey. The system's
   disabled state was already measured and already legible; the opacity was
   the only thing dragging it under, and nothing is lost by removing it —
   the button already reads as unavailable from its fill, its border and its
   cursor.

   `cursor: progress` stays and is deliberately NOT the system's
   `not-allowed`: a submitting button is working, not refusing, and those are
   different sentences to the person waiting. */
.got-submit {
  inline-size: 100%;
}
.got-submit:disabled {
  cursor: progress;
}


/* ---------------------------------------------------------------- INPUTS ---

   The field is the wrapper; the control, the label, the help and the error
   are its parts. Help and error are SEPARATE ELEMENTS with separate rules,
   because they are different sentences with different jobs and the pattern of
   using one grey box for both is how an error ends up looking like advice. */
.got-field {
  display: block;
}

.got-field__label {
  display: block;
  font-size: var(--gt-text-2xs);
  font-weight: var(--gt-weight-semibold);
  line-height: var(--gt-leading-snug);
  color: var(--gt-ink);
  margin-block-end: var(--gt-space-2);
}

/* 🔴 BUG-235 — THE REACH OF THIS RULE, AND THE RULE ABOUT REACH.
   ============================================================================

   This rule used to say `.got-field input, .got-field select, .got-field
   textarea, .got-input`. The merge base styled `.got-field input` and nothing
   else. Adding `select` and `textarea` BY POSITION reached two legacy
   components that had never been sized by anything:

     · `.got-combo__select` — the contact channel picker, `flex: 0 0 content`.
       `inline-size: 100%` made its flex base the whole row, so the handle
       input beside it collapsed to its 26px min-content and hung off the card.
       MEASURED, same markup, branch vs merge base: select 105.2px -> 374px,
       input 268.8px -> 26px, 26px of spill. /register in both languages and
       /account/profile, at 1280 and at 390.
     · `.got-order__select` and `.got-order__textarea` on the order page, which
       sit inside `<label class="got-field">` and were never measured at all.

   THE RULE THAT COMES OUT OF IT, and it is the point rather than the patch:

     A SYSTEM RULE THAT CHANGES LAYOUT MAY NOT REACH BY POSITION.
     It reaches by CLASS — `.got-input`, opted into — plus `input`, which is
     the one selector the site already had and ten templates already rely on.

     A rule that only PAINTS may reach by position, because it cannot resize
     anything: the hover border, the focus ring and the error edge below still
     say `.got-field select`. Those are the rules that made five registration
     inputs go from `outline:none` to a real ring, and they are safe.

   So a `<select>` or a `<textarea>` gets the system's box by wearing
   `.got-input`, which /design's do. A legacy control inside a `.got-field`
   keeps whatever it always had. This is the failure the round existed to
   prevent, arriving from the direction nobody was watching — the system
   beating legacy CSS rather than legacy CSS beating the system. */
.got-field input,
.got-input {
  inline-size: 100%;
  min-block-size: var(--gt-tap-min);
  padding-block: var(--gt-space-3);
  padding-inline: var(--gt-space-4);

  background: var(--gt-surface-sunken);
  color: var(--gt-ink);                          /* 14.30:1 */
  /* The state's colour if a state has declared one, the neutral hairline if
     not. See --gt-edge-color above: its ABSENCE is the neutral state, which
     is why this reads a fallback rather than a token defined at :root. */
  border: var(--gt-border-hair) solid var(--gt-edge-color, var(--gt-line-strong));
  border-radius: var(--gt-radius-md);

  font-family: var(--gt-family);
  /* BODY size, not the control size. Every text input measured 14px on
     19 Aug — below the 16px at which iOS Safari zooms the page on focus, so
     a phone user pinched back after every field of the registration form.
     Buttons stay at --gt-text-sm; what is TYPED reads at body size. */
  font-size: var(--gt-text-md);
  line-height: var(--gt-leading-snug);

  transition: var(--gt-transition-control);
}

/* Placeholder is MUTED, not faint — 6.74:1 on sunken. A placeholder is read
   by the person deciding what to type. */
.got-field input::placeholder,
.got-field textarea::placeholder,
.got-input::placeholder {
  color: var(--gt-ink-muted);
  opacity: 1;   /* Firefox dims placeholders by default; the colour is chosen */
}

/* HOVER HAS NOTHING TO SAY ABOUT A CONTROL THAT IS ALREADY FOCUSED, and
   `:not(:focus-visible)` is that sentence rather than a specificity device:
   without it this rule (0,3,1) would out-specify the focus rule below (0,2,1)
   and hand the state's colour back to the border under the pointer — the
   doubled outline returning on hover alone, in exactly the state the fix is
   for. A control whose edge the state owns keeps it; hovering does not
   overrule "this is wrong". */
.got-field input:hover:not(:disabled):not(:focus-visible),
.got-field select:hover:not(:disabled):not(:focus-visible),
.got-field textarea:hover:not(:disabled):not(:focus-visible),
.got-input:hover:not(:disabled):not(:focus-visible) {
  border-color: var(--gt-edge-color, var(--gt-accent-strong));
}

/* THE SAME RING AS THE BUTTON, and — BUG-247 — THE BORDER JOINS THE BAND.
   `:focus-visible` on a text input behaves as `:focus` in every current
   browser — a caret is keyboard interaction by definition — so the two are
   written together and the field never loses its ring to a mouse click.

   The border is painted `--gt-ring-band`, the same page colour the separator
   is, so the control's own edge and the band are ONE 3px moat and the ring is
   the only stroke. It is a token and not `transparent` on purpose: transparent
   shows the control's FILL through the border box, which would put the moat at
   2px on a field and leave it at 3px everywhere else for no reason a reader
   could recover.

   This rule out-specifies the base border above (0,2,1 against 0,1,1) with no
   appeal to source order, and there is nothing left for a state to say here —
   the state's two `border-color` rules that used to sit below this one existed
   only to paint the second stroke, and they are gone. */
.got-field input:focus-visible,
.got-field select:focus-visible,
.got-field textarea:focus-visible,
.got-input:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  border-color: var(--gt-ring-band);
  box-shadow: var(--gt-ring-shadow);
}

/* Error state. The EDGE carries it, and the message below carries the words.
   The field itself does not turn red inside — coloured text in a field is
   what the person is typing, and recolouring it would be recolouring their
   words to say something about our validation.

   TWO SPELLINGS, ONE RULE. `--error` is the system's name, set by the server
   when it renders a refusal. `.is-invalid` is what `public/js/form-validation.js`
   writes as you type, and it was defined separately in site.css. They are the
   same state — a field that is wrong — and they were two rules that disagreed
   about what that looks like: the legacy one turned the border `--red` and the
   message `#FCA5A5`, a light red that appears nowhere else in the product and
   in no palette. One rule, both spellings, so the server's refusal and the
   script's cannot look like different things.

   🔴 AND THE RULES THAT USED TO LIVE HERE ARE GONE — BUG-247. Both spellings,
   the valid state and the two `:focus-visible` rules that repainted the
   border under the ring were SIX declarations of one idea: this state's
   colour. They are one `--gt-edge-color` at the top of this file now, and the
   deletion is the fix rather than a tidy-up — a state that cannot write
   `border-color` cannot write it UNDER a ring of the same colour.

   The one thing those rules did that a wrapper token does not is REACH a
   `<select>` and a `<textarea>`, which the base rule above deliberately does
   not size (BUG-235). So the reach is kept, as a rule that only PAINTS —
   which may reach by position, because it cannot resize anything. Its neutral
   value is `--gt-line-strong`, and both legacy controls it lands on
   (`.got-combo__select`, `.got-order__select/textarea`) already declare
   `1px solid var(--line-2)`, WHICH IS THE SAME COLOUR: measured, not assumed,
   and the reason this rule changes nothing until a state speaks.

   The old rules also re-declared `background: var(--gt-surface-sunken)` —
   the value all five selectors already had (the base rule for `input` and
   `.got-input`, `var(--bg-2)` in site.css for the two legacy controls). It
   set a property to what it already was and is not carried over. */
.got-field select,
.got-field textarea {
  border-color: var(--gt-edge-color, var(--gt-line-strong));
}

/* 🔴 AND THE `:not([type="checkbox"])` THE DELETED RULES CARRIED IS NOT
   CARRIED OVER, BECAUSE IT WAS NEVER DOING ANYTHING. Measured on the consent
   checkbox on /register, wrapper forced to `.is-invalid`, sampled at 4x:

     as shipped          border-top-width 0px   pixels #767676 (Chrome's own)
     CONTROL, appearance:none   border-top-width 1px   pixels = --gt-line-strong as it then was (ours)

   Chrome zeroes the border on an `appearance:auto` checkbox and draws its
   own; the author colour reaches it only when something sets
   `appearance:none`, and nothing in this product does. The exclusion was
   describing a rule that could not fire, which is this project's own
   definition of a fallback indistinguishable from a bug — so it is written
   down here as a measurement instead of forward as a rule. If a checkbox is
   ever given `appearance:none`, it will take the state's edge like any other
   control, and that is the right answer rather than a regression. */

.got-field input:disabled,
.got-field select:disabled,
.got-field textarea:disabled,
.got-input:disabled {
  background: var(--gt-surface-page);
  color: var(--gt-ink-muted);       /* 7.28:1 — readable, as above */
  /* The same idiom as the rest and the hover edge, and it PRESERVES what the
     cascade already did: `.got-field.is-valid input:not([type="checkbox"])`
     (0,3,1) out-specified this rule (0,2,1), so a disabled field in a state
     kept the state's edge. It still does — said in the declaration now rather
     than fallen into by a specificity count. */
  border-color: var(--gt-edge-color, var(--gt-line));
  cursor: not-allowed;
}

/* ------------------------------------------------- THE STEPPER — V-33 B3 ---

   `−  [n]  +`. Two icon buttons (`.got-btn--quiet .got-btn--icon`, so their
   states are the system's) either side of a REAL number input (`.got-input`,
   so its surface, ring and disabled treatment are the system's too). This
   block adds only what a stepper is that a field is not:

   · the row itself, a flex line at the control gap;
   · the box's width — three digits and the padding, centred, in tabular
     figures so `9` and `10` do not shift the buttons. Written as
     `.got-stepper .got-stepper__input` (0,2,0) ON PURPOSE: the box is a
     `.got-input` inside a `.got-field`, and `.got-field input { inline-size:
     100% }` is (0,1,1), which beat a lone `.got-stepper__input` (0,1,0) —
     MEASURED by the batch's verifier as 106px beside 70px on one page, the
     box following Chrome's intrinsic number-input width from its `max`
     attribute while this comment described a width that never won. It is
     64px on every line now, whatever the `max`, and `BasketStepperTest`
     reads the cascade for which rule wins;
   · the browser's own spin buttons hidden, because the two buttons beside
     the box ARE the spinner, and two spinners on one control is two answers.
     `appearance: textfield` is what the platforms understand for a number
     input; the arrow keys and the wheel still work, which is the half that
     matters to a keyboard;
   · the reason line — the sentence beside a control that is disabled at a
     bound (ruling 13). Muted, on the caption step; the same tone as help
     text, because that is what it is.

   NOTHING HERE MULTIPLIES. The figure beside the stepper comes from the
   server on every press (ruling 14); this stylesheet, like the script, holds
   no arithmetic. */
.got-stepper {
  display: inline-flex;
  align-items: center;
  gap: var(--gt-space-2);
}
.got-stepper .got-stepper__input {
  inline-size: calc(var(--gt-tap-min) + var(--gt-space-6));
  flex: 0 0 auto;
  text-align: center;
  font-variant-numeric: tabular-nums;
  appearance: textfield;
}
.got-stepper__input::-webkit-outer-spin-button,
.got-stepper__input::-webkit-inner-spin-button {
  appearance: none;
  margin: 0;
}
.got-stepper__reason {
  margin: 0;
  font-size: var(--gt-text-2xs);
  line-height: var(--gt-leading-normal);
  color: var(--gt-ink-muted);
}

/* Help text: what to type. Muted at 6.53:1, NOT faint at 3.66 — this is the
   exact rule the hint correction of 14 Aug landed on. */
.got-field__help {
  display: block;
  margin-block-start: var(--gt-space-2);
  font-size: var(--gt-text-2xs);
  line-height: var(--gt-leading-normal);   /* prose, not a control: 1.35 was under the 1.4 floor (19 Aug) */
  color: var(--gt-ink-muted);
}

/* Error text: what went wrong. --gt-ink and not --gt-danger, because this
   sentence has to be readable everywhere the field can appear and danger
   fails AA on the overlay surface. The danger is the dot and the field's
   edge; the words are plain. */
.got-field__error {
  display: flex;
  align-items: flex-start;
  gap: var(--gt-space-2);
  margin-block-start: var(--gt-space-2);
  font-size: var(--gt-text-2xs);
  line-height: var(--gt-leading-normal);   /* a sentence: 1.35 was under the 1.4 floor (19 Aug) */
  color: var(--gt-ink);
}
/* The marker. `::before` and not a character in the string, so the sentence
   in lang/{en,ar} stays a sentence a translator can read. */
.got-field__error::before {
  content: "";
  flex: 0 0 auto;
  inline-size: var(--gt-space-3);
  block-size: var(--gt-space-3);
  margin-block-start: var(--gt-space-1);
  border-radius: var(--gt-radius-pill);
  background: var(--gt-danger);
}

/* 🔴 AN EMPTY ERROR ELEMENT RENDERS NOTHING, and this rule is FUNCTIONAL
   rather than cosmetic.

   `public/js/form-validation.js` writes into a span that the auth forms
   render ALWAYS PRESENT AND EMPTY, so it has somewhere to put a message
   without creating a node. Without this rule the marker dot and the box's
   spacing would be painted under every field on every auth page from the
   moment it loads, saying nothing.

   It came from site.css, where it was one line under `.got-field__error`,
   and it had to come WITH the component rather than be left behind — the
   system's version of this element has a `::before`, so leaving the rule
   behind would have made the defect worse than it was before the system. */
.got-field__error:empty {
  display: none;
}
.got-field__error a {
  color: var(--gt-accent);
  text-decoration: underline;
  text-underline-offset: var(--gt-space-1);
}


/* -------------------------------------------------- CHECKBOX AND RADIO ---

   🔴 THE ONE CONTROL THIS SYSTEM NEVER HAD, AND IT SHIPPED AS THE BROWSER'S.

   Measured on /register before this component existed: the terms checkbox was
   **16.8 x 44**, painted **#FFFFFF over 28% of its box** on a #171B28 card —
   a white hole in a dark page — and its label sat on a line the box did not
   share. On /login the same control was **13 x 13**. One idea, two sizes,
   neither of them ours.

   The 44 is the interesting number. `.got-field input` sets
   `min-block-size: var(--gt-tap-min)`, which is right for a text field and
   absurd for a checkbox, and `site.css` set `block-size: 1.05rem` WITHOUT
   `min-block-size` — so the floor won and stretched an 18px box into a 44px
   column. That is BUG-235's trap arriving on a different element type: A
   SYSTEM RULE THAT CHANGES LAYOUT MAY NOT REACH BY POSITION, and `input` is
   the one position selector that rule kept for compatibility.

   Which is why the box rule below is `.got-check .got-check__box` and not
   `.got-check__box`: at (0,2,0) it out-specifies `.got-field input` at
   (0,1,1) OUTRIGHT, with no appeal to source order. Writing it as one class
   would tie on nothing and lose. The alternative — narrowing `.got-field
   input` with `:not([type="checkbox"])` — raises that rule to (0,3,1), where
   it would beat `.got-field input:focus-visible` at (0,2,1) and hand BUG-247
   straight back. Measured before writing, not discovered after.

   ============ EVERY STATE, AND THE INVALID ONE IS NOT WRITTEN ============

   rest, hover, focus, checked and disabled are below. **THE INVALID STATE
   APPEARS NOWHERE IN THIS SECTION** — `--gt-edge-color` is set by the field
   the box sits in and inherits, so `.got-field.is-invalid` recolours the
   box's edge, its hover edge and its ring with no rule here knowing that
   state exists. A sixth state is still two lines at the top of this file and
   nothing here. That is the whole argument for the token, and this component
   is the first one written AFTER it to prove it. */
.got-check {
  display: flex;
  align-items: flex-start;
  gap: var(--gt-space-3);

  /* The whole row is the target, because it is a <label>. The padding is
     what lifts a single-line row off the 18px of the box itself: measured
     31px on "Remember me", which clears the 24px WCAG 2.5.8 AA floor. It
     does NOT reach 44 — that is `--gt-tap-min`, the AAA figure, and a
     44px-tall row for one line of text is a gap nobody asked for. The number
     is stated rather than left to be inferred from the token it did not
     use. */
  padding-block: var(--gt-space-2);

  font-size: var(--gt-text-sm);
  line-height: var(--gt-leading-snug);
  color: var(--gt-ink);
  cursor: pointer;
}

/* 🔴 `appearance: none` IS THE WHOLE COMPONENT. Everything below is only
   possible once the browser stops drawing its own control: an author
   `background` is ignored on an `appearance: auto` checkbox, and so is an
   author `border` — measured, Chrome reports `border-top-width: 0px` and
   paints #767676 regardless of what the stylesheet says. `accent-color` is
   the only hook the native control offers and it colours the CHECKED fill
   only, which is why the unchecked box stayed white. */
.got-check .got-check__box {
  appearance: none;
  -webkit-appearance: none;   /* Safari still needs it on form controls */

  flex: 0 0 auto;
  display: grid;
  place-content: center;

  inline-size: var(--gt-check-size);
  block-size: var(--gt-check-size);
  min-block-size: 0;          /* the field rule's tap floor is for a text box */
  margin: 0;
  padding: 0;

  background: var(--gt-surface-sunken);
  border: var(--gt-border-hair) solid var(--gt-edge-color, var(--gt-line-strong));
  border-radius: var(--gt-radius-xs);

  cursor: pointer;
  transition: var(--gt-transition-control);
}

/* A radio is a checkbox that is round. That is the entire difference, and
   writing it as one declaration rather than a second component is what stops
   the two drifting — every state below is shared. */
.got-check .got-check__box[type="radio"] {
  border-radius: var(--gt-radius-pill);
}

/* The mark, drawn by the box rather than by a second element, so there is
   nothing in the markup a template can forget. It is SCALED rather than
   toggled: `display:none` cannot transition, and a mark that appears
   instantly on a control that animates everything else reads as a glitch.
   The duration is a token, so the reduced-motion block at the end of this
   file already stops it. */
.got-check .got-check__box::before {
  content: "";
  inline-size: var(--gt-check-mark);
  block-size: var(--gt-check-mark);
  background: var(--gt-ink-on-accent);
  transform: scale(0);
  transition: transform var(--gt-duration-fast) var(--gt-ease-standard);
  /* The tick, as a clipped rectangle. No SVG, no data URI, no font glyph —
     it inherits the mark colour and scales with the token. */
  clip-path: polygon(14% 44%, 0% 65%, 50% 100%, 100% 16%, 80% 0%, 43% 62%);
}
.got-check .got-check__box[type="radio"]::before {
  clip-path: none;
  border-radius: var(--gt-radius-pill);
}

/* CHECKED. The fill is the state's colour when the field has one and the
   accent when it does not — the same `var(--gt-edge-color, …)` idiom the
   field's rest, hover and disabled edges use, so a checked box inside an
   invalid field is red and not a blue tick sitting inside a red field
   saying two things at once (BUG-245's rule, on the one control that can
   show both at the same time).

   `--gt-ink-on-accent` is the dark ink, and it is PIXEL-SAMPLED off /design
   on every fill a box can actually be in — the first version of this comment
   said "4.81:1 on danger, 7.40:1 on success", both of them arithmetic on two
   hexes and one of them for a state that was not on the page at all:

     --gt-accent            11.74:1
     --gt-danger  #EF4444    5.08:1   (the comment said 4.81)
     --gt-success #22C55E    8.39:1   (the comment said 7.40, unexercised)
     disabled     #0D0F17    7.28:1   mark is --gt-ink-muted, not the ink

   The accent is named and not printed on purpose: `BrandPaletteTest` refuses
   the three accent hexes anywhere outside `config/brand.php`, and it caught
   this comment. A ratio is a fact about a colour; the colour's value has one
   home.

   The lowest clears 3:1 for a non-text graphic comfortably. A `.is-valid`
   specimen was added to /design to make the third figure measurable rather
   than asserted — `data-rule="checked"` on the registration terms box means
   a green checkbox is a state a customer reaches by ticking it. */
.got-check .got-check__box:checked {
  background: var(--gt-edge-color, var(--gt-accent));
  border-color: var(--gt-edge-color, var(--gt-accent));
}
.got-check .got-check__box:checked::before {
  transform: scale(1);
}

/* HOVER, and the `:not(:focus-visible)` is the same sentence it is on the
   field: hover has nothing to say about a control that is already focused,
   and without that word this rule out-specifies the focus rule below and
   paints the state's colour back under the ring. */
.got-check .got-check__box:hover:not(:disabled):not(:focus-visible) {
  border-color: var(--gt-edge-color, var(--gt-accent-strong));
}

/* FOCUS. Literally the same four declarations as `.got-field input`, reading
   the same tokens — the ring inherits the state, and the border joins the
   band so the box paints one stroke and not two. */
.got-check .got-check__box:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  border-color: var(--gt-ring-band);
  box-shadow: var(--gt-ring-shadow);
}

/* DISABLED reads as OFF, and it wins over `:checked` above by source order
   on an equal count — a disabled ticked box must not keep the live accent
   fill, which is the one combination where getting this backwards is
   visible. The mark goes muted rather than disappearing: what was ticked is
   still information. */
.got-check .got-check__box:disabled {
  background: var(--gt-surface-page);
  border-color: var(--gt-line);
  cursor: not-allowed;
}
.got-check .got-check__box:disabled::before {
  background: var(--gt-ink-muted);
}
.got-check:has(.got-check__box:disabled) {
  color: var(--gt-ink-muted);      /* 6.53:1 on raised — readable, not faded */
  cursor: not-allowed;
}

/* The label takes the rest of the row and wraps like a sentence. `min-inline-size:0`
   because a flex item's default `min-width:auto` refuses to shrink below its
   longest word, which is how a long consent sentence pushes its own box off
   the card. */
.got-check__label {
  flex: 1 1 auto;
  min-inline-size: 0;
}
.got-check__label a {
  color: var(--gt-accent);
  text-decoration: underline;
  text-underline-offset: var(--gt-space-1);
}
/* The consent sentence's Terms and Privacy links. They carried a ring from
   site.css as `.got-consent__text a` and that selector stopped matching the
   moment the label became `.got-check__label` — a ring lost by a RENAME,
   which is the failure mode BUG-234's list is built out of. It moves here
   with the component rather than being repointed there, so the two cannot
   come apart again. */
.got-check__label a:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  border-radius: var(--gt-radius-sm);
  box-shadow: var(--gt-ring-shadow);
}


/* ----------------------------------------------------------------- CARDS ---

   One card, three weights. `.got-card` is the object; the modifiers say how
   far off the page it sits, and they map onto the surface ladder rather than
   inventing shadows of their own.

   `.got-card--panel` IS THE ACCOUNT PAGES' TREATMENT — the thing
   `.got-account__panel` does today — expressed once so /account, /cart,
   /account/wallet and the order page stop each having a card. */
.got-card {
  background: var(--gt-surface-raised);
  border: var(--gt-border-hair) solid var(--gt-line);
  border-radius: var(--gt-radius-lg);
  padding: var(--gt-space-7);
  box-shadow: var(--gt-level-elevated);
}

/* The panel: the same object, flatter, because account pages stack several of
   them and a page of shadows reads as a mess. The BASE level — the highlight
   alone is enough to separate it from the page at this size. */
.got-card--panel {
  box-shadow: var(--gt-level-base);
}

/* Raised: a single card that IS the page's content, e.g. the sign-in box.
   That is the FEATURED level and this is where the name of the level came
   from — the modifier keeps its older name because six templates wear it,
   but "the one thing on the page that is the page" is exactly what it has
   always meant. Never more than one in a viewport. */
.got-card--raised {
  box-shadow: var(--gt-level-featured);
}

/* Interactive: a card that is a link or a button. Everything a control gets. */
.got-card--interactive {
  transition: var(--gt-transition-control);
  cursor: pointer;
}
.got-card--interactive:hover {
  border-color: var(--gt-line-strong);
  background: var(--gt-surface-overlay);
  box-shadow: var(--gt-level-hover);
}
.got-card--interactive:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  box-shadow: var(--gt-ring-shadow);
}

/* 🔴 THE TYPE REGISTER, stated once so a new surface has something to
   join rather than a number to guess (V-01, 20 Aug):

     page title    --gt-text-2xl / heavy   28  h1 on every page
     section head  --gt-text-xl  / heavy   22  .section-head h2 is 2xl
                                               because the homepage's h1
                                               is the display clamp
     card head     --gt-text-lg  / heavy   18  this rule, and the media
                                               tile's title
     body          --gt-text-md  / normal  16
     lead / meta   --gt-text-sm  / normal  14  muted

   THE WEIGHT WAS `bold` (700) HERE AND `heavy` (800) ON
   `.got-gamecard__title` — the same role, one page over, two answers.
   Measured on the rendered page: 18px/700 against 18px/800. The card head
   is heavy, which is also what puts real distance between it and the
   400-weight lead directly under it.

🔴 TRACKING IS THE THIRD LEVER AND IT IS DELIBERATELY NOT PULLED — AND
   THE REASON IS NOT THE ONE THIS COMMENT FIRST GAVE. It said negative
   letter-spacing "CRUSHES ARABIC, whose script joins", which reads as a
   mechanism and was never measured. An independent verifier could not
   reproduce it and reported the opposite ("Chrome ignores letter-spacing on
   Arabic entirely"). Both are wrong. MEASURED in Chrome on the same button
   with the same rule and a Latin control on the same page, reading the TEXT
   RUN with a Range rather than the element box:

     ls  10px   'Browse the games'  119.89 -> 279.89px  (+133.5%, = 10px x 16 gaps)
                'تصفّح الألعاب'        78.69 ->  88.69px  ( +12.7%, = 10px x 1 gap)
     ls -3px    Latin               119.89 ->  71.89px  (-40.0%)
                Arabic               78.69 ->  75.69px  ( -3.8%)

   Chrome applies tracking at NON-JOINING boundaries only — word spaces and
   the letters that do not connect forward — and does NOT break the joins. So
   the real problem is not crushed glyphs, it is that A VALUE TUNED FOR LATIN
   IS VERY NEARLY INERT ON ARABIC: the same declaration moves one script by
   40% and the other by 4%, and the two would drift apart typographically with
   no single value able to serve both.

   That is why the token has to be SCOPED TO A LANGUAGE if it is ever wanted,
   and it is Omar's decision that it is not wanted before launch. The site
   ships Arabic in v1. `.got-label`'s comment reserves tracking for the
   micro-label for the neighbouring reason (Arabic has no case), and the
   wordmark's lean is Latin-only. The size and weight above are measurable and
   were measured. */
.got-card__title {
  font-size: var(--gt-text-lg);
  font-weight: var(--gt-weight-heavy);
  line-height: var(--gt-leading-tight);
  color: var(--gt-ink);
}
.got-card__lead {
  margin-block-start: var(--gt-space-2);
  font-size: var(--gt-text-sm);
  line-height: var(--gt-leading-normal);
  color: var(--gt-ink-muted);
}

/* The 11px uppercase micro-label. The only place --gt-tracking-label is used,
   and the only step of the scale that is allowed to be uppercase: Arabic has
   no case, so an uppercase treatment is a decoration that exists on exactly
   one of the two languages and must never carry meaning on its own. */
.got-label {
  display: block;
  font-size: var(--gt-text-3xs);
  font-weight: var(--gt-weight-semibold);
  letter-spacing: var(--gt-tracking-label);
  text-transform: uppercase;
  color: var(--gt-ink-muted);
}


/* ------------------------------------------------- EMPTY / LOADING / ERROR

   Three patterns, one shape: an icon or mark, a sentence, and — for empty and
   error — a way out. They share a container so a page swapping between them
   does not jump.

   The rule they encode, from this project's own history: an empty state that
   hides the only action is an empty shell, and an error that does not say
   what to do next is a dead end. */
.got-state {
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  gap: var(--gt-space-4);
  padding-block: var(--gt-space-9);
  padding-inline: var(--gt-space-6);
  /* 🔴 A CENTRED BLOCK THAT IS SOMEBODY ELSE'S FLEX ITEM IS NOT CENTRED, AND
     ITS OWN `align-items: center` SAYS NOTHING ABOUT WHERE THE BOX LANDS.
     Four of the five callers put this element in a block container, where it
     fills the line and the centring above is the whole story. The wallet puts
     it inside `.got-ledger__row`, which is `display: flex` with
     `justify-content: space-between` — so it became a shrink-to-fit item and
     `space-between` left it at the row's START. MEASURED on /account/wallet
     at 1920 before this line existed: the state was 255px wide inside a
     1090px row, hard against the start edge, with 835px of empty box beside
     it (and mirrored correctly to the right edge on /ar, which is what made
     it look like a directional bug and is not). The round brief read the
     screenshot the other way round — as an icon inside a 250px box — and the
     numbers say the box IS the 255px.

     `flex` is inert on anything that is not a flex item, so this one
     declaration is invisible to the four block callers and is the whole fix
     for the fifth. It is written here, on the element that is wrong, rather
     than as a `.got-ledger__row > .got-state` rule: a new selector in this
     file must have a specimen on /design or DesignPageTest's reachability
     sweep names it, and a rule that exists for exactly one shape is a rule
     the next author has to go and find. `100%` rather than `auto` because
     the row also carries `flex-wrap: wrap` — the state takes its own line,
     which is the only line there is when the ledger is empty. */
  flex: 1 1 100%;
}

.got-state__title {
  font-size: var(--gt-text-lg);
  font-weight: var(--gt-weight-semibold);
  line-height: var(--gt-leading-tight);
  color: var(--gt-ink);
}

.got-state__body {
  max-inline-size: 42ch;   /* a measure, not a width: it is in characters */
  font-size: var(--gt-text-sm);
  line-height: var(--gt-leading-normal);
  color: var(--gt-ink-muted);
}
.got-state__body a {
  color: var(--gt-accent);
  text-decoration: underline;
  text-underline-offset: var(--gt-space-1);
}

.got-state__actions {
  display: flex;
  gap: var(--gt-space-3);
  flex-wrap: wrap;
  justify-content: center;
  margin-block-start: var(--gt-space-2);
}

/* The mark. A ring in the state's colour, sized off the type scale so it
   stays in proportion if the scale moves. */
.got-state__mark {
  inline-size: var(--gt-space-10);
  block-size: var(--gt-space-10);
  display: grid;
  place-items: center;
  border-radius: var(--gt-radius-pill);
  background: var(--gt-tint-accent);
  color: var(--gt-accent);
}
.got-state--error .got-state__mark {
  background: var(--gt-tint-danger);
  color: var(--gt-danger);
}

/* LOADING.

   🔴 THE SENTENCE IS THE INDICATOR; the ring is decoration on top of it.
   That is not a style note — it is the same standing rule the toast below
   follows. A person with reduced motion, a person with images off and a
   screen reader all get the words; only the ring needs the animation.

   `aria-live="polite"` belongs on the element in the MARKUP, not here. CSS
   cannot announce anything and a comment claiming it does would be the kind
   of overstated guard this project files bugs about. */
.got-state__spinner {
  inline-size: var(--gt-space-8);
  block-size: var(--gt-space-8);
  border: var(--gt-border-bar) solid var(--gt-line-strong);
  border-block-start-color: var(--gt-accent);
  border-radius: var(--gt-radius-pill);
  animation: got-spin var(--gt-duration-slow) linear infinite;
}

@keyframes got-spin {
  to { transform: rotate(360deg); }
}

/* 🔴 THE ONE THING THE TOKEN REWRITE CANNOT REACH, handled where it lives.

   The reduced-motion block at the top rewrites --gt-duration-slow to 1ms.
   For a TRANSITION that is correct. For this INFINITE SPIN it would be a
   strobe — 1000 revolutions a second — which is worse than the motion it was
   meant to remove and is a genuine seizure risk.

   So the spin is stopped outright and the ring becomes a static, complete
   circle in the accent: still a mark, no longer a motion. The sentence beside
   it was always the thing carrying the meaning, which is why this costs
   nothing. */
@media (prefers-reduced-motion: reduce) {
  .got-state__spinner {
    animation: none;
    border-color: var(--gt-accent);
  }
}


/* ---------------------------------------------------------------- MESSAGES

   The inline status/error box — the shape `.got-auth__status` and
   `.got-auth__errors` already have, made into one component with four tones.

   `padding-inline` with two values, NOT a physical shorthand. This is the
   14 Aug correction (BUG-209): the box has a `border-inline-start`, so a
   physical `padding: a b c d` strands the marker gutter on the wrong edge in
   RTL and the list bullet paints on top of the border. Measured on /ar/cart.

   The tone is carried by the EDGE and the TINT. The text is --gt-ink in all
   four, for the reason given on --gt-danger above: danger as text fails AA on
   one of this product's four surfaces, and a message component that is only
   readable on three of them is a bug with a delay on it. */
.got-message {
  padding-block: var(--gt-space-4);
  padding-inline: var(--gt-space-6) var(--gt-space-5);
  border-radius: var(--gt-radius-md);
  border-inline-start: var(--gt-border-bar) solid var(--gt-line-strong);
  background: var(--gt-surface-overlay);
  color: var(--gt-ink);
  font-size: var(--gt-text-xs);
  line-height: var(--gt-leading-normal);
}
.got-message--success {
  background: var(--gt-tint-success);
  border-inline-start-color: var(--gt-success);
}
.got-message--warning {
  background: var(--gt-tint-warning);
  border-inline-start-color: var(--gt-warning);
}
.got-message--error {
  background: var(--gt-tint-danger);
  border-inline-start-color: var(--gt-danger);
}
.got-message--info {
  background: var(--gt-tint-accent);
  border-inline-start-color: var(--gt-accent);
}
/* 🔴 THE INLINE MESSAGE — ONE ROW, SENTENCE AND ACTION TOGETHER.
   V-42b2 Batch 3, 11 Sep 2026. Omar, on the transfer page:
   «الكلام جاي رص ورا بعض».

   MEASURED BEFORE, in headless Chrome at device-scale 1: the verification
   notice was 110px tall at 1440 and 151–172px at 390, against a step rail of
   21px, and it held THREE STACKED CHILDREN — a sentence, a 44px outlined
   button and a link. On a page whose whole job is one amount and one address,
   the tallest and loudest block was a notice that blocks nothing.

   It is a ROW now: the sentence takes the space it needs, the action and the
   aside sit at the end of the same line, and the three wrap only when the
   sentence cannot fit. NOTHING IS REMOVED — Omar's 21 Aug policy is that
   verification is strongly encouraged and NEVER blocking, so the action stays
   a real 44px control and the aside stays a real link.

   🔴 THE CHILDREN ARE NAMED, NOT POSITIONAL. `> p` and `> form` were the
   first spelling and `SiteTokensTest::test_a_system_layout_rule_never_reaches
   _a_legacy_control` refused them on sight — BUG-235's rule: a system rule
   that sizes whatever happens to sit inside it reached `.got-combo__select`
   and left the field beside it 26px wide. Size by class.

   `align-items: center` and not `baseline`: the action is 44px and the
   sentence is one or two lines, and baseline would hang the button off the
   first line of a wrapped sentence. */
.got-message--inline {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gt-space-3) var(--gt-space-5);
  margin-block-end: var(--gt-space-7);
}
/* The sentence takes the row. 22rem is the width below which it wraps to a
   third line anyway, so that is where the ROW breaks instead. */
.got-message__text {
  flex: 1 1 22rem;
  min-inline-size: 0;
  margin: 0;
}
.got-message__action,
.got-message__aside {
  flex: 0 0 auto;
  margin: 0;
}

/* 🔴 BUG-240 — `.got-message ul` AND `.got-message ol` ARE DELETED.

   They styled a list INSIDE a message box. Every real usage puts the class ON
   the list — `<ul class="got-message got-message--error">` in the basket and
   on /design, `<ul class="got-auth__errors">` in the auth shell and the
   profile — so the descendant selector could not match a single element in
   the project. Measured: `document.querySelectorAll('.got-message ul')`
   returns 0 on the one page that renders a message list.

   Nothing changed visually when they went, which is exactly why they survived:
   `margin:0` and `padding:0` are already given by the global reset, and
   `outside` is the initial value. A rule nothing can exercise is
   indistinguishable from a bug — this project's own words — and two of them
   sitting under a comment about list markers read as the thing keeping those
   markers right.

   `.got-message li + li` DOES match (the list IS the message, so its items are
   descendants) and it is the rule doing the real work. */
.got-message li + li {
  margin-block-start: var(--gt-space-2);
}
/* Links inside a message box — the verification notice's "change it in your
   profile" link and the reminder's "confirm your email" link (Batch 1
   verifier's Low, 21 Aug). They fell through to the browser's default ring:
   visible, so accessibility held — branding did not. The ring lives HERE,
   beside the component it rings, never in a list somebody maintains: the
   `.got-consent__text a` ring was lost by a RENAME precisely because it
   lived away from its component (SiteTokensTest's own note). Same four
   declarations as every link ring in this file. */
.got-message a:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  border-radius: var(--gt-radius-sm);
  box-shadow: var(--gt-ring-shadow);
}


/* ------------------------------------------------------------------ TOAST

   🔴 READ THIS BEFORE ADDING ONE.

   A toast is JavaScript, and on this site JavaScript is decoration and never
   protection. So the rule, which is a build rule and not a styling one:

     WHATEVER A TOAST SAYS MUST ALSO BE SERVER-RENDERED.

   Both halves or neither. The server-rendered half is a `.got-message` in the
   page flow; the toast is the SAME SENTENCE lifted into the corner by script,
   and the script REMOVES the flow copy when it does so — so a person with
   JavaScript sees one message and a person without sees one message, and
   neither sees two. `public/js/toast.js` is where that happens and it is
   `defer`red; if it 404s, the flow copy simply stays where it is and nothing
   is lost.

   `SiteTokensTest::test_a_flash_message_is_rendered_without_javascript` is
   the falsification: delete the flow copy from the Blade and it goes red.

   ------------------------------------------------------------------------

   `position: fixed` with `inset-inline-end`, so it lands in the corner the
   language reads towards — bottom-right in English, bottom-LEFT in Arabic —
   with no [dir="rtl"] override. That is the whole reason for logical
   properties and it is the one place on the site where getting it wrong would
   put a message under the browser's own UI. */
.got-toast-region {
  position: fixed;
  inset-block-end: var(--gt-space-6);
  inset-inline-end: var(--gt-space-6);
  z-index: 60;

  display: flex;
  flex-direction: column;
  gap: var(--gt-space-3);

  inline-size: min(24rem, calc(100vw - var(--gt-space-10)));
  /* The region is a container for messages, not a surface: it must not eat
     clicks on the page behind it when it is empty. Each toast turns pointer
     events back on for itself. */
  pointer-events: none;
}

/* 🔴 ON A PHONE THE TOAST COMES FROM THE TOP, AND V-25 B4's TESTER IS WHY —
   30 Aug 2026.

   V-25 B4 made the gift card's add-to-basket confirmation a toast, and on a
   phone it landed ON TOP of the button that produces it. MEASURED at 390x844,
   signed in, both languages:

     toast          x 28  y 777.20  w 342  h 46.80   (bottom 824)
     #add-to-cart   x 20  y 791     w 350  h 44
     elementFromPoint(button centre) -> DIV.got-toast    blocked: true

   `.got-toast` sets `pointer-events: auto` above and `public/js/toast.js`
   NEVER auto-dismisses, by the written decision there ("a message a customer
   needs must not vanish before it is read"), so after one add the page's
   secondary call to action was dead until the customer found the 24px dismiss
   glyph. Height-independent: the region sat 20px off the bottom and the
   service page's sticky buy bar is ~134px tall, so it held on every phone.
   The same collision is visible on /cart, where a toast covers a product link
   (that one you can scroll out from under; a sticky bar you cannot).

   🔴 IT IS A SYSTEM-WIDE DECISION AND NOT A GIFT-CARD PATCH, BECAUSE THE THREE
   NARROWER FIXES WERE EACH REFUSED BY A GUARD DOING ITS JOB, AND THE REFUSALS
   ARE THE ARGUMENT:

     · `body:has(.cfg .buy) .got-toast-region` in PAGES.CSS — refused by
       `SiteTokensTest::test_nothing_the_system_owns_is_redefined_by_the_legacy_
       stylesheet`, whose own words are "a clash in pages.css is a migrated page
       reaching back into the system it was migrated onto, which is a mistake
       rather than a queue." Its predicate strips `:has(...)` with every other
       pseudo-class, so no page-context selector written that way could satisfy
       it — it was pointing at the right answer, not at a technicality.
     · The same selector moved HERE — refused twice over. It made `.cfg` and
       `.buy` system-owned (the owned set is DERIVED from the classes this file
       styles), which immediately reddened pages.css's own `.cfg` and `.buy`
       rules as redefinitions; and `DesignPageTest` refused it as a state
       `/design` cannot be in.
     · A `--lifted` modifier the layout applies — same `DesignPageTest` wall,
       and it needs a template change and a /design specimen for a positioning
       fix.

   What is left is the honest version of the thing anyway: on a phone, a
   bottom-anchored overlay competes with sticky action bars, the home
   indicator and the keyboard, and every one of those lives at the bottom
   edge. The top is where a phone's transient messages belong.

   THE BLOCK AXIS ONLY. `inset-inline-end` is untouched, so the toast still
   lands in the corner the language reads towards — that is the rule above and
   it is the one thing here that may not move. Re-MEASURED after this fix at
   390x844: x 29 English, x 19 Arabic, correctly mirrored.

   THE OFFSET IS `--gt-scroll-offset`, NOT A NUMBER: what everything else on
   this site uses to clear the sticky header, and since V-30 B4 a derivation
   from `--gt-header-height` in this band. MEASURED at 390x844 on 30 Aug:
   header 60.8px, the toast opens at y80 with 19px of air under it and 583px
   of clearance above the bar's y710, and `elementFromPoint` at the button's
   centre returns the button in both languages. RE-MEASURED 2 Sep (V-30 B4):
   header 64.00 and the region's block-start at 84px — 20px of air.
   RE-MEASURED 2 Sep (V-30 B5, the two-line phone header): header 116.00
   and the region's block-start at 136px — the same 20px of air. */
@media (max-width: 56.24em) {
  .got-toast-region {
    inset-block-start: var(--gt-scroll-offset);
    inset-block-end: auto;
  }
}

.got-toast {
  pointer-events: auto;
  display: flex;
  align-items: flex-start;
  gap: var(--gt-space-4);

  padding-block: var(--gt-space-4);
  padding-inline: var(--gt-space-5);

  background: var(--gt-surface-overlay);
  color: var(--gt-ink);
  border: var(--gt-border-hair) solid var(--gt-line-strong);
  border-inline-start: var(--gt-border-bar) solid var(--gt-accent);
  border-radius: var(--gt-radius-md);
  /* 🔴 NOT ON THE LEVEL LADDER, AND THAT IS THE POINT (V-02, 22 Aug). The
     four levels say how far a surface sits off THE PAGE. A toast does not sit
     on the page at all — it floats over it, at a distance nothing else in the
     product shares — so it keeps `--gt-shadow-4` directly. Adding a fifth
     level for one caller would make the ladder a list of every shadow in use
     rather than a statement about depth. */
  box-shadow: var(--gt-lift), var(--gt-shadow-4);

  font-size: var(--gt-text-xs);
  line-height: var(--gt-leading-normal);

  animation: got-toast-in var(--gt-duration-base) var(--gt-ease-out) both;
}
.got-toast--success { border-inline-start-color: var(--gt-success); }
.got-toast--error   { border-inline-start-color: var(--gt-danger); }
.got-toast--warning { border-inline-start-color: var(--gt-warning); }

.got-toast__body { flex: 1 1 auto; }

/* The dismiss control. A real button with the tap minimum, because a toast
   that cannot be got rid of on a phone is a toast covering the page. */
.got-toast__close {
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  inline-size: var(--gt-space-7);
  block-size: var(--gt-space-7);
  margin-block-start: calc(var(--gt-space-1) * -1);
  margin-inline-end: calc(var(--gt-space-2) * -1);

  background: transparent;
  border: 0;
  border-radius: var(--gt-radius-sm);
  color: var(--gt-ink-muted);
  cursor: pointer;
  transition: var(--gt-transition-control);
}
.got-toast__close:hover {
  background: var(--gt-wash-strong);
  color: var(--gt-ink);
}
.got-toast__close:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  box-shadow: var(--gt-ring-shadow);
}

/* THE ACTION — V-33 B3, ruling 15's Undo. A real `.got-btn .got-btn--quiet`
   that `public/js/toast.js` builds when the message names a call, so its
   ring, hover, press and tap minimum are the system's; this class only sets
   it at the caption size and keeps it off the body's baseline. It keeps the
   44px floor rather than shrinking to the toast's line — a control a thumb
   has to hit on a phone is not a place to save 12px. */
.got-toast__action {
  flex: 0 0 auto;
  align-self: center;
  padding-inline: var(--gt-space-4);
  font-size: var(--gt-text-2xs);
}

/* It enters from the reading edge, so the movement mirrors with the language.

   🔴 THIS IS THE ONE PLACE IN THIS FILE THAT NAMES A DIRECTION, and the
   [dir="rtl"] rule below is a necessity rather than an oversight. `transform:
   translateX()` is PHYSICAL by specification — there is no logical transform
   and no `translate-inline` — so a keyframe cannot mirror itself the way
   every other rule in this file does. The distance is therefore a custom
   property that the [dir] rule flips, which keeps the keyframe itself
   direction-free and puts the whole of the exception on one line.

   This is the same class of exception `background-position` is: a property
   with no logical form. It is written down for the same reason. */
@keyframes got-toast-in {
  from { opacity: 0; transform: translateX(var(--gt-toast-from, 1rem)); }
  to   { opacity: 1; transform: translateX(0); }
}
[dir="rtl"] .got-toast { --gt-toast-from: -1rem; }


/* ------------------------------------------------------------ VISUALLY HIDDEN

   The label that a screen reader reads and a screen does not show. Written
   out here once; site.css carries two hand-copied versions of it, and their
   own comments say they were duplicated because a utility reached across
   block boundaries is one somebody edits for a single caller. That argument
   held while there was no system; there is one now, and this is where it
   lives. */
/* ------------------------------------------------------------- THE PRESS ---

   🔴 ONE PRESS, EVERY CONTROL — one rule, not the same declarations under
   five selectors.

   Lift removed, shadow removed, moved down: the control goes INTO the page.
   Each kind above changes only its COLOUR on `:active`; the movement is here,
   once, so "every control in this system presses the same way" is something
   the stylesheet enforces rather than something five rules happen to agree
   on.

   It was five copies until `SiteTokensTest`'s no-raw-pixels rule found
   `translateY(1px)` written out five times. The guard doing its job on its
   first run, against its own author.

   🔴 IT IS LAST IN THIS SECTION AND THAT IS LOAD-BEARING. Every selector
   involved is (0,2,0) — a class plus a pseudo-class — so SOURCE ORDER is the
   only thing deciding, and a control that is hovered AND pressed at the same
   time is the normal case rather than an edge one. Placed higher, this block
   sat above `.got-card--interactive:hover`, and pressing a hovered card kept
   the hover's raised shadow: the card stayed up while the buttons went down.
   Caught by reading the cascade, not by a test — text cannot see this, which
   is why it is written here in words. */
.got-btn:active,
.got-card--interactive:active {
  box-shadow: var(--gt-shadow-0);
  transform: translateY(var(--gt-press-shift));
}


/* ------------------------------------------------------------ VISUALLY HIDDEN */
.got-sr-only {
  position: absolute;
  inline-size: var(--gt-hairline);
  block-size: var(--gt-hairline);
  padding: 0;
  margin: calc(var(--gt-hairline) * -1);
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
  border: 0;
}


/* ------------------------------------------------- THE SKIP LINK (GAP-103, 19 Aug)

   The first thing in every customer page's <body>: an anchor to `<main
   id="main">`. At rest it is the visually-hidden recipe above — the same five
   declarations, not a second copy of them by another name — so it takes no
   space, paints nothing and is still the first Tab stop. Focused, it becomes
   a pill pinned to the page's INLINE-START corner (logical inset, so it sits
   top-left on /en and top-right on /ar with no [dir] override), above the
   sticky header (z-index 50) and its sheets (60), on the overlay surface with
   the strong edge and the system's two-tone ring.

   `:focus-visible` and not `:focus` on purpose: only a keyboard can reach a
   control that has no box, and it is the pseudo-class every other ring in
   this file answers to — a skip link that showed on a programmatic focus and
   nothing else would be the one control with a private answer.

   The tap floor is deliberately NOT applied: a control nobody can point at
   has no tap target, and forcing 44px on a one-line pill in the corner would
   only push the header down. */
.got-skip {
  position: absolute;
  inset-block-start: var(--gt-space-2);
  inset-inline-start: var(--gt-space-2);
  z-index: 70;

  inline-size: var(--gt-hairline);
  block-size: var(--gt-hairline);
  padding: 0;
  margin: calc(var(--gt-hairline) * -1);
  overflow: hidden;
  white-space: nowrap;
  clip-path: inset(50%);
  border: 0;

  font-family: var(--gt-family);
  font-size: var(--gt-text-sm);
  font-weight: var(--gt-weight-semibold);
  line-height: var(--gt-leading-snug);
  text-decoration: none;
  color: var(--gt-ink);                       /* 12.50:1 on the overlay it reveals onto */
}
.got-skip:focus-visible {
  inline-size: auto;
  block-size: auto;
  margin: 0;
  overflow: visible;
  clip-path: none;

  padding-block: var(--gt-space-3);
  padding-inline: var(--gt-space-4);
  background: var(--gt-surface-overlay);
  border: var(--gt-border-hair) solid var(--gt-ring-band);   /* the edge joins the band, as on every focused control */
  border-radius: var(--gt-radius-md);
  box-shadow: var(--gt-ring-shadow), var(--gt-shadow-3);

  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
}


/* ------------------------------------------ THE HEADER MENUS (18 Aug, Batch 2)

   Three disclosures that share one header row: the NAV MENU (☰ — the games,
   the language switch, and for a guest the sign-in), the LANGUAGE SWITCH and
   the ACCOUNT MENU (the customer's name; My orders / My wallet / Profile /
   Sign out). The language switch and the account menu lived in site.css and
   are migrated here whole — their site.css copies are DELETED in the same
   pass (SiteTokensTest refuses a legacy redefinition of a system selector).

   ============ ONE DOM AT EVERY WIDTH, AND WHY THE ☰ IS A <details> ==========

   HeaderWalletTest slices the header and asserts exactly ONE <form> (the
   sign-out), ONE <button>, ONE <input> (the CSRF token) and NO <select>. So
   the ☰ cannot be a <button> and cannot be a checkbox hack; it is a
   <details class="got-navmenu"> with a <summary> — the account menu's own
   argument, one element over — and it wraps the EXISTING .got-lang and
   (guest) the sign-in link. Nothing is duplicated: the navigation itself is
   the pill (`.got-tabs`, below), OUTSIDE this disclosure since V-30 B5, on
   the row at every width; HeaderWalletTest counts its three links once.

   ============ 🔴 A CLOSED <details> DOES NOT RENDER ITS CONTENT ============

   Phase 1 planned "display:contents on the details and display:none on its
   summary above the phone width, so the desktop row lays out as now". That
   is NOT ENOUGH, and it was measured before a line of this was written: a
   closed <details> puts its content in a `::details-content` box that is
   `content-visibility: hidden` — the links inside had a rect and TOOK NO
   TAB STOP. `display:contents` on the element changes nothing about that.

   So on the wide row the content is FORCED OPEN through the pseudo-element
   the platform provides for exactly this: `.got-navmenu::details-content {
   content-visibility: visible; display: contents }` (Chrome 131, Firefox
   138, Safari 18.4). Where that pseudo-element is unsupported the rule is
   ignored, and the wide-row block is guarded by `@supports
   selector(::details-content)` so the ☰ STAYS VISIBLE AND WORKING there: an
   older browser on a desktop gets the phone's disclosure rather than a row
   with its navigation hidden behind a control that has been hidden too. A
   graceful step down, and stated here rather than left to be found.

   ============ THE PHONE ROW: ONE LINE, AND TWO SHEETS THAT DO NOT GROW IT ==

   MEASURED before this: the guest header was 103px at 390 (two rows) and the
   signed-in one 159px (three rows, the account toggle alone on the last with
   233px empty beside it) — 19% of every 844px screen was sticky header, and
   OPENING the account menu grew it to 414px because the panel was
   `position:static` at that width. Now: one row — brand · ☰ · basket ·
   balance · account — and BOTH panels are absolutely positioned SHEETS
   anchored to `.site-header` (a positioned ancestor: it is `sticky`),
   `inset-inline: 0` and `inset-block-start: 100%`, full width, so neither
   can leave the viewport at 320 (a toggle-anchored panel is what site.css
   records leaving it) and neither adds a pixel to the header.

   The breakpoint is the width at which the WIDE ROW STOPS FITTING, measured
   with the tap targets at their 44px sizes (brand 198 + links + switch +
   basket + balance + account, both languages) — 52em (832px) — and it is a
   viewport fact, not a rhythm step. Below it, the sheet; at it and above,
   the row. --gt-scroll-offset follows the same line, because it is the
   header's height it clears.

   ============ EVERY TARGET IS 44 (GAP-099) ================================

   The switch's items are the target now (the group lost its padding so the
   item IS the 44px row); the toggles are 44 square; every sheet row is 44. */

/* -------- the nav menu: the disclosure ------------------------------------- */
.got-navmenu {
  position: static;   /* the sheet anchors to .site-header, not to this box */
}
/* The toggle: 44 square, the icon drawn by the markup (three bars), the
   platform's disclosure triangle removed by both mechanisms it is drawn
   with (`list-style` and the WebKit marker). */
.got-navmenu__toggle {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--gt-tap-min);
  min-block-size: var(--gt-tap-min);
  border: var(--gt-border-hair) solid var(--gt-line-strong);
  border-radius: var(--gt-radius-md);
  color: var(--gt-ink);
  transition: var(--gt-transition-control);
}
.got-navmenu__toggle::-webkit-details-marker {
  display: none;
}
.got-navmenu__toggle:hover {
  background: var(--gt-wash-soft);
}
.got-navmenu__toggle:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  box-shadow: var(--gt-ring-shadow);
}
.got-navmenu[open] .got-navmenu__toggle {
  background: var(--gt-wash-strong);
}
.got-navmenu__icon {
  inline-size: var(--gt-space-6);
  block-size: var(--gt-space-6);
  display: block;
}

/* The sheet: full width under the header, the page's raised surface, a
   column of 44px rows. It holds the games row, the switch and the guest's
   sign-in; each keeps its own class and its own rules and only their LAYOUT
   changes here. */
.got-navmenu__sheet {
  position: absolute;
  inset-inline: 0;
  /* 100% of the header's PADDING box would paint the sheet over its 1px
     bottom border (measured: y=60 sheet colour, border at 60–61); one
     hairline down and the row keeps its rule above the sheet. */
  inset-block-start: calc(100% + var(--gt-border-hair));
  z-index: 1;
  display: flex;
  flex-direction: column;
  gap: var(--gt-space-2);
  padding-block: var(--gt-space-3) var(--gt-space-4);
  padding-inline: var(--gt-gutter);
  background: var(--gt-surface-raised);
  border-block-end: var(--gt-border-hair) solid var(--gt-line);
  box-shadow: var(--gt-shadow-3);
}
/* The sheet's two rows keep their own classes and their own rules; only
   their LAYOUT changes here. (The games row, `.nav-links`, was the third
   until V-30 B5 moved the navigation out of the sheet.) */
.got-navmenu__sheet .got-lang,
.got-navmenu__sheet .got-btn {
  align-self: flex-start;
  flex-shrink: 0;   /* on the wide row only the brand gives (site.css) */
}

/* -------- the centre zone — V-30 B4, re-homed by V-30 B5 -------------------
   The header row is THREE ZONES: the brand at the inline start, the
   navigation in the centre, and language · basket · account at the end.
   This element is the centre, and since B5 it holds the pill navigation
   (`.got-tabs`, below) and sits OUTSIDE the ☰ disclosure, between the brand
   and the <details>.

   Three shapes, one per band:
     · the wide row (the @supports block below) — the flex item that takes
       the row's slack and centres the pill: `flex: 1 0 auto`, it grows and
       never shrinks, so the brand stays the one item that gives;
     · the phone band — a full-width SECOND LINE of the same `.nav` row
       (`flex: 0 0 100%` under `.nav{flex-wrap:wrap}`, site.css), ordered
       after every first-line item, the pill centred in it. The first line
       was measured full before the tabs existed (B4: 84 and 77px free at
       390, signed in) and decision #3's ladder stopped at its step 5;
       the phone band's `--gt-header-height` above carries this line;
     · top level, `display: contents` — what a wide viewport gets where
       `::details-content` is unsupported and the @supports block is
       skipped: the tabs join the row inline beside the ☰, which fits at
       any width the wide row fits.
   A <div> and not a landmark: the pill inside it is the <nav>. */
.got-header__nav {
  display: contents;
}
@media (max-width: 52em) {
  .got-header__nav {
    display: flex;
    justify-content: center;
    flex: 0 0 100%;
    /* `order: 1` draws the pill on the SECOND line while the DOM keeps
       brand · tabs · ☰ · account — so the Tab walk in this band goes line
       1 → line 2 → line 1. MEASURED at 800×900 and at 390×844 (touch),
       both languages: brand y8.3 → Home / Games / Store cards y61.8 → ☰
       y8.3 → Create account y8.3. THE COORDINATOR'S DECISION (V-30 B5,
       recorded as V-30_BLOCKED.md #13 for Omar): KEEP the DOM order. Brand,
       primary navigation, then utilities is a meaningful sequence in both
       bands, and moving the zone last in the DOM would put the same
       mismatch on the WIDE row instead — where the majority of visitors
       are. The visual/tab mismatch is confined to viewports under 52em. */
    order: 1;
  }
}

/* -------- the end zone — V-33 B4 ---------------------------------------------
   The third of the header's three zones, and the reason the row can be a
   three-column grid: the ☰ disclosure and the account cluster are two
   siblings, and a `1fr auto 1fr` row with four children puts the fourth on
   a second grid row. This wrapper holds both, so the row's children are
   exactly brand · centre · end (HeaderHeightTest reads the layout for that).
   A flex row inside, `align-items: center`, its gap THE ROW'S: on the wide
   row the disclosure is `display: contents` (below) and the switch, the
   guest's sign-in and the cluster are this box's items at the same
   `--gt-space-5` the row put between them before. `justify-self: end` for
   the same reason the brand is `start` (site.css): a stretched grid item
   would spread the cluster across the whole track. Its track is
   `minmax(max-content, 1fr)`, so the box never has to shrink and carries no
   `min-inline-size` — the B4 verifier MEASURED one here rendering
   byte-identically deleted; the zone that CAN shrink is the brand, on its
   `minmax(0, 1fr)` track.

   ON THE PHONE THE BOX IS `display: contents`. The first version of this
   batch laid the ☰ and the cluster out INSIDE the box at an 8px gap, and
   the verifier MEASURED the ☰ 22–74px nearer the cluster than at V-30 B5
   (guest / x 130.08 → 186.50; signed in /ar 246.52 → 224.67) while the
   report called the phone header unchanged. Transparent, the ☰ and the
   cluster are the row's own `space-between` items again — brand · ☰ ·
   cluster on line 1 — which is V-30 B5's rule and V-30 B5's geometry
   (re-measured to the pixel in the B4 fix pass). Not positioned in either
   band: both sheets under it anchor to `.site-header`, and HeaderHeightTest
   asserts that pair exactly. */
.got-header__end {
  display: flex;
  align-items: center;
  gap: var(--gt-space-5);
  justify-self: end;
}
@media (max-width: 52em) {
  .got-header__end {
    display: contents;
  }
}

/* -------- the wide row: the disclosure disappears and its content is inline -
   Guarded by @supports (see the head note): where the pseudo-element is not
   understood the block is skipped and the ☰ stays.

   The summary is `display:none` here and that is the ONE hide of an
   interactive control this system makes on purpose: at this width its
   content is forced open two lines below, so the control has NOTHING TO DO —
   a Tab stop on it would be a stop with no effect. SystemCoverageTest names
   it in an excused set asserted for exactness; the day the ☰ has a job on
   the wide row, the entry comes out and the hide goes with it. */
@supports selector(::details-content) {
  @media (min-width: 52.0625em) {
    .got-navmenu,
    .got-navmenu__sheet {
      display: contents;
    }
    .got-navmenu::details-content {
      content-visibility: visible;
      display: contents;
    }
    .got-navmenu__toggle {
      display: none;
    }
    /* The centre zone becomes a box — V-33 B4: the MIDDLE COLUMN of the
       row's grid (`.nav`, site.css: `minmax(0,1fr) auto minmax(max-content,1fr)`),
       and it sits in the centre of that `auto` column. It was `flex: 1 0
       auto` — the row's slack as its width, the pill in the middle of the
       SLACK, which is the header's centre only when the two side zones
       happen to be equal (they never are: MEASURED −98/+98 guest and
       −125/+129 signed in at 1440, per locale). A box and not `contents`
       because a `justify-self` needs a box to place. */
    .got-header__nav {
      display: flex;
      justify-self: center;
    }
    /* The sheet's start-alignment would otherwise follow the switch and the
       sign-in onto the row and pin them to its top (measured: 7px above the
       account toggle at 1440). On the row they align like every other item. */
    .got-navmenu__sheet .got-lang,
    .got-navmenu__sheet .got-btn {
      align-self: auto;
    }
  }
}

/* -------- the pill navigation — V-30 B5 -------------------------------------
   Home · Games · Store cards: three equal-width tabs in a hairline pill on
   the page's soft wash, and ONE indicator bar — a fixed-width accent span
   with a three-layer glow — standing over the tab that is lit. The look is
   the reference the round named (21st.dev's pill nav) rebuilt natively:
   no package, no script, tokens only.

   THE BAR MOVES BY `transform: translateX()` AND NOTHING ELSE (hover-skill
   §5 rule 3; ui-ux-pro-max `transform-performance`): never a width, never
   an inset. Its distance is arithmetic on two tokens — `tab × (index + ½)
   − bar ÷ 2` — and the index comes from `data-active` on the <nav>, which
   the layout writes from App\Support\HeaderNav. Every page load therefore
   renders the bar already in position, and there is NO transition on it:
   a page navigation is a new document, and a slide between tabs would be
   a slide nobody sees. The three `[data-active]` rules are an ORDER
   COUPLING with the layout's three links — index 0 must be the first <a>
   — and HeaderNavTest measures that on the rendered page rather than
   trusting it.

   RTL: `translateX` is physical, so `--gt-tabs-flip` is the sign the
   `[dir="rtl"]` rule flips, exactly as `--gt-sweep-to` is flipped on the
   game card. `inset-inline-start: 0` puts the bar's origin at the right
   edge on /ar and the negative distance walks it left across the mirrored
   tab order — MEASURED rendered on /ar, not reasoned (B5's README).

   THE ENTRANCE: opacity and a 4px rise over `--gt-duration-base` on the
   pill as a whole, once per page load (ui-ux-pro-max `duration-timing`:
   150–300ms). Both durations are tokens, so the system's reduced-motion
   block collapses it — and the block at the end of this section stops it
   outright, because a 1ms fade from opacity 0 is still one dark frame
   (the sheen's own reasoning). The colour states sit OUTSIDE that band and
   survive it (hover-skill §5 rule 7: less movement, not less information).

   THREE STATES, ONE RING. Hover and focus-visible get the same reward —
   ink and the strong wash — and focus-visible adds the system's three ring
   declarations; the press is the system's `--gt-press-shift` and dark
   wash. The lit tab wears the strong wash and ink by the bare
   `[aria-current]`, because the layout writes "page" on /games and "true"
   on a game page and both must paint (CatalogueUxTest's rule since V-04).

   EQUAL WIDTHS, and why: see the tokens. Every tab is 44 tall (GAP-099,
   HeaderMenuTest's floor) and 90–112 wide; the pill carries no padding —
   the tab IS the target, the `.got-lang` rule. */
.got-tabs {
  --gt-tabs-index: 0;
  --gt-tabs-flip: 1;
  --gt-tabs-x: calc(var(--gt-tabs-tab-size) * (var(--gt-tabs-index) + .5) - var(--gt-tabs-bar-size) / 2);
  position: relative;
  display: inline-flex;
  align-items: center;
  flex-shrink: 0;   /* on the wide row only the brand gives (site.css) */
  border: var(--gt-border-hair) solid var(--gt-line-strong);
  border-radius: var(--gt-radius-pill);
  background: var(--gt-wash-soft);
  backdrop-filter: blur(var(--gt-tabs-blur));
  line-height: var(--gt-leading-tight);
  animation: got-tabs-enter var(--gt-duration-base) var(--gt-ease-standard) 1 backwards;
}
[dir="rtl"] .got-tabs {
  --gt-tabs-flip: -1;
}
.got-tabs[data-active="home"] {
  --gt-tabs-index: 0;
}
.got-tabs[data-active="games"] {
  --gt-tabs-index: 1;
}
.got-tabs[data-active="cards"] {
  --gt-tabs-index: 2;
}
.got-tabs__tab {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  inline-size: var(--gt-tabs-tab-size);
  min-block-size: var(--gt-tap-min);
  padding-inline: var(--gt-space-2);
  border-radius: var(--gt-radius-pill);
  color: var(--gt-ink-muted);
  font-size: var(--gt-text-sm);
  font-weight: var(--gt-weight-medium);
  white-space: nowrap;
  text-decoration: none;
  transition: var(--gt-transition-control);
}
.got-tabs__tab:hover,
.got-tabs__tab:focus-visible {
  color: var(--gt-ink);
  background: var(--gt-wash-strong);
}
.got-tabs__tab:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  box-shadow: var(--gt-ring-shadow);
}
.got-tabs__tab:active {
  background: var(--gt-press);
  transform: translateY(var(--gt-press-shift));
}
.got-tabs__tab[aria-current] {
  color: var(--gt-ink);
  background: var(--gt-wash-strong);
}
/* The bar: its width is declared HERE and in no state rule (HeaderNavTest
   asserts the set), its place is the transform, and it takes no pointer —
   it lies over the tab it names. */
.got-tabs__bar {
  position: absolute;
  inset-block-start: 0;
  inset-inline-start: 0;
  inline-size: var(--gt-tabs-bar-size);
  block-size: var(--gt-space-1);
  border-radius: var(--gt-radius-pill);
  background: var(--gt-accent);
  box-shadow: var(--gt-tabs-glow);
  transform: translateX(calc(var(--gt-tabs-x) * var(--gt-tabs-flip)));
  pointer-events: none;
}
@keyframes got-tabs-enter {
  from {
    opacity: 0;
    transform: translateY(calc(-1 * var(--gt-space-1)));
  }
}
/* Reduced motion: the entrance is stopped here, not merely shortened —
   the token rewrite leaves a 1ms fade whose first frame is the pill
   absent. The colour rules above are outside this band on purpose. */
@media (prefers-reduced-motion: reduce) {
  .got-tabs {
    animation: none;
  }
}

/* -------- the language switch (migrated from site.css) ---------------------
   A segmented pill: the language you are reading is a <span> (clicking it
   would do nothing) and the other is an <a>; both wear a class, because a
   system rule that SIZES may not reach a bare element (BUG-235). Logical properties throughout,
   so it mirrors under dir="rtl" with no override. GAP-099: the ITEM is the
   44px target — the group used to carry 3px of padding around 27px items. */
.got-lang {
  display: inline-flex;
  align-items: center;
  gap: var(--gt-space-1);
  border: var(--gt-border-hair) solid var(--gt-line-strong);
  border-radius: var(--gt-radius-pill);
  background: var(--gt-wash-soft);
  line-height: var(--gt-leading-tight);
}
.got-lang__item,
.got-lang__current {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  min-inline-size: var(--gt-tap-min);
  min-block-size: var(--gt-tap-min);
  padding-inline: var(--gt-space-4);
  border-radius: var(--gt-radius-pill);
  font-size: var(--gt-text-xs);
  font-weight: var(--gt-weight-semibold);
  white-space: nowrap;
  text-decoration: none;
}
.got-lang__item {
  color: var(--gt-ink-muted);
  transition: var(--gt-transition-control);
}
.got-lang__item:hover {
  color: var(--gt-ink);
  background: var(--gt-wash-strong);
}
.got-lang__item:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  box-shadow: var(--gt-ring-shadow);
}
/* The current language: dark on accent, the same rule as .got-btn--primary. */
.got-lang__current {
  background: var(--gt-accent);
  color: var(--gt-ink-on-accent);
  cursor: default;
}
/* Arabic glyphs sit smaller at the same size — one step up. By attribute,
   which the current span carries too, so one rule reaches both. */
.got-lang [lang="ar"] {
  font-size: var(--gt-text-sm);
}

/* -------- the account menu (migrated from site.css) ------------------------
   `position:relative` on the WIDE row so the panel hangs from the toggle's
   inline edge; `static` on the phone so the panel is a sheet anchored to
   the header (which is what stops opening it from growing the sticky header
   — measured 414px at 390 before this). The toggle (V-30 B6): 44 tall, an
   avatar disc, the name and a caret, ONE <summary> and so one tab stop; on
   the phone the name collapses (max-inline-size 0, still in the DOM and
   still announced) and the disc alone is the 44px square. */
.got-accountmenu {
  position: relative;
}
.got-accountmenu__toggle {
  list-style: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  gap: var(--gt-space-2);
  min-block-size: var(--gt-tap-min);
  min-inline-size: var(--gt-tap-min);
  /* Tighter at the start, where the disc sits (its own edge is the pill's
     curve); the text end keeps the control's padding. */
  padding-inline: var(--gt-space-2) var(--gt-space-4);
  border: var(--gt-border-hair) solid var(--gt-line-strong);
  border-radius: var(--gt-radius-md);
  color: var(--gt-ink);
  font-size: var(--gt-text-xs);
  font-weight: var(--gt-weight-semibold);
  white-space: nowrap;
  transition: var(--gt-transition-control);
}
.got-accountmenu__toggle::-webkit-details-marker {
  display: none;
}
.got-accountmenu__toggle:hover {
  background: var(--gt-wash-soft);
}
.got-accountmenu__toggle:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  box-shadow: var(--gt-ring-shadow);
}
.got-accountmenu[open] .got-accountmenu__toggle {
  background: var(--gt-wash-strong);
}
/* The name is the only part that can be long, so it is the only part allowed
   to give: a ch bound tracks the font the name is set in. */
.got-accountmenu__name {
  max-inline-size: 14ch;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* THE AVATAR (V-30 B6): a filled disc carrying one character. Dark on the
   accent, the rule `.got-lang__current` and `.got-btn--primary` share — a
   SOLID fill, so its contrast is one number and not a function of what the
   translucent header happens to be over (MEASURED, the fill pixel-sampled
   from the render against the computed ink: 11.74:1 dark, 5.06:1 light).
   The character is finished in PHP (upper-cased, never empty); the disc is
   `aria-hidden` because the name beside it is the control's name. 32px in
   the toggle, 40px in the menu's identity block — both steps of the scale. */
.got-accountmenu__avatar {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: 0 0 auto;
  inline-size: var(--gt-space-8);
  block-size: var(--gt-space-8);
  border-radius: var(--gt-radius-pill);
  background: var(--gt-accent);
  color: var(--gt-ink-on-accent);
  font-size: var(--gt-text-sm);
  font-weight: var(--gt-weight-bold);
  line-height: 1;
  user-select: none;
}
.got-accountmenu__avatar--lg {
  inline-size: var(--gt-space-9);
  block-size: var(--gt-space-9);
  font-size: var(--gt-text-lg);
}
/* The caret rotates when the disclosure opens — a transform on an SVG has
   no logical form and needs none: down is down in both directions. */
.got-accountmenu__caret {
  inline-size: var(--gt-space-5);
  block-size: var(--gt-space-5);
  flex: 0 0 auto;
  transition: transform var(--gt-duration-fast) var(--gt-ease-standard);
}
.got-accountmenu[open] .got-accountmenu__caret {
  transform: rotate(180deg);
}
/* The panel: hangs from the toggle's inline END on the wide row (logical, so
   it hangs from the correct edge in both languages), on the overlay surface,
   above the header's own stacking.

   🔴 BUG-588 (V-30 B4 → fixed in B6). The panel's containing block is the
   `<details>` — the toggle's own 44px box, centred in the row — because
   that is what keeps its inline edge on the trigger's. `calc(100% + air)`
   from that box put the panel's top at 59.5, 4.50px INSIDE the 64px bar
   (MEASURED, both languages). Anchoring it to the header instead (as the
   two sheets are) would move it to the header's inline edge, off the
   trigger (rendered by B4's verifier: en x 1328 / ar x 0).

   So the block offset READS THE HEADER'S HEIGHT and the inline edge stays
   the trigger's. The toggle sits (header − border − toggle) / 2 below the
   header's top, so from the toggle's top the header's bottom edge is
   (header + border + toggle) / 2 — and the air token goes on top of that.
   The three inputs are each held elsewhere: the row is the token less its
   border (HeaderHeightTest), the toggle is the tap floor tall
   (HeaderMenuTest), and the row centres its items (HeaderHeightTest, this
   batch). MEASURED after: panel top 70.00 both languages = 64 + 6, edge
   deltas 0 / 0. HeaderHeightTest's sweep classifies this as a read of the
   token and lists it among the consumers; the exemption it carried is gone. */
.got-accountmenu__panel {
  position: absolute;
  inset-block-start: calc((var(--gt-header-height) + var(--gt-border-hair) + var(--gt-tap-min)) / 2 + var(--gt-space-2));
  inset-inline-end: 0;
  z-index: 2;
  min-inline-size: 15rem;
  max-inline-size: 18rem;
  display: flex;
  flex-direction: column;
  gap: var(--gt-space-1);
  padding: var(--gt-space-2);
  background: var(--gt-surface-raised);
  border: var(--gt-border-hair) solid var(--gt-line);
  border-radius: var(--gt-radius-lg);
  box-shadow: var(--gt-shadow-3);
}
/* The identity block (V-30 B6): the disc, the name, the address. Nothing
   to press, and the first thing in the panel because it is the question a
   menu under a name answers first. `min-inline-size: 0` on the column so
   the address can be clipped instead of widening the panel. */
.got-accountmenu__identity {
  display: flex;
  align-items: center;
  gap: var(--gt-space-3);
  padding-block: var(--gt-space-3);
  padding-inline: var(--gt-space-3);
  min-inline-size: 0;
}
.got-accountmenu__who {
  display: flex;
  flex-direction: column;
  gap: var(--gt-space-1);
  min-inline-size: 0;
}
.got-accountmenu__fullname {
  margin: 0;
  font-size: var(--gt-text-sm);
  font-weight: var(--gt-weight-semibold);
  color: var(--gt-ink);
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
/* The three destinations are BARE `<a href>` — HeaderWalletTest counts each
   `<a href="…/account/…">` byte-exact, so no class may join them — and a
   system rule may not SIZE a bare element (BUG-235). So the 44px row is
   made of PADDING, which that guard permits: 12 + 12 around a --gt-text-sm
   line at --gt-leading-normal (14 × 1.6 = 22.4) is 46.39px, MEASURED (V-30
   B6 re-measured it with the glyph in the row: the 20px glyph sits inside
   the 22.4px line and adds nothing). The nearest token pair that lands at
   or above the 44px floor; the sign-out below takes the same pair so the
   four rows are one height. The glyph carries its own class (sized by it,
   not by this rule) and `currentColor`, so it follows the row's ink. */
.got-accountmenu__panel a {
  display: flex;
  align-items: center;
  gap: var(--gt-space-3);
  padding-block: var(--gt-space-4);
  padding-inline: var(--gt-space-4);
  border-radius: var(--gt-radius-sm);
  font-size: var(--gt-text-sm);
  line-height: var(--gt-leading-normal);
  color: var(--gt-ink);
  white-space: nowrap;
  text-decoration: none;
  transition: var(--gt-transition-control);
}
.got-accountmenu__panel a:hover {
  background: var(--gt-surface-overlay);
}
.got-accountmenu__panel a:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: calc(var(--gt-ring-gap) * -1);
  box-shadow: var(--gt-ring-shadow);
}
/* V-33 B4: `--gt-space-7` with the basket's and the wallet's glyphs — the
   cluster is one icon family through one token (HeaderAccountTest), and the
   header's two glyphs stepped up from 20 to 24. */
.got-accountmenu__glyph {
  flex: 0 0 auto;
  inline-size: var(--gt-space-7);
  block-size: var(--gt-space-7);
}
/* The address: the quiet second line of the identity block. */
.got-accountmenu__email {
  margin: 0;
  font-size: var(--gt-text-xs);
  color: var(--gt-ink-muted);
  /* The address is dir="ltr" inside a panel that may be RTL, so its own
     `start` is the LEFT — in the /ar phone sheet the text floated mid-row
     (verifier: x 126–268 while the rows' text ends at 358). So the BOX
     shrinks to the text and sits at the column's start (align-self on the
     column's cross axis: right on /ar, left on /), and the text fills it.
     `text-align: match-parent` was tried first and MEASURED computing to
     `start` in Chrome 151 — the child's start, i.e. no change. */
  align-self: flex-start;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  max-inline-size: 100%;
}
/* The rule spans the panel: margin-inline matches the panel's padding. */
.got-accountmenu__rule {
  border: 0;
  border-block-start: var(--gt-border-hair) solid var(--gt-line);
  margin-block: var(--gt-space-2);
  margin-inline: calc(var(--gt-space-2) * -1);
}
/* The sign-out is SHAPED LIKE THE LINKS ABOVE IT — it is the last item of
   the same list as far as the customer is concerned; a button styled as a
   button here would read as the menu's primary action. The form is
   `display:contents` so it adds no box between the column and the button. */
.got-accountmenu__signout {
  display: contents;
}
.got-accountmenu__signout-btn {
  display: flex;
  align-items: center;
  gap: var(--gt-space-3);
  inline-size: 100%;
  min-block-size: var(--gt-tap-min);
  padding-block: var(--gt-space-4);   /* the links' pair, so this row is their height (MEASURED 46.39, was 44) */
  padding-inline: var(--gt-space-4);
  border: 0;
  border-radius: var(--gt-radius-sm);
  background: none;
  cursor: pointer;
  font: inherit;
  font-size: var(--gt-text-sm);
  line-height: var(--gt-leading-normal);
  text-align: start;
  color: var(--gt-ink-muted);
  transition: var(--gt-transition-control);
}
.got-accountmenu__signout-btn:hover {
  background: var(--gt-surface-overlay);
  color: var(--gt-ink);
}
.got-accountmenu__signout-btn:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: calc(var(--gt-ring-gap) * -1);
  box-shadow: var(--gt-ring-shadow);
}

/* -------- the phone: the account menu is a sheet, the toggle a square ------ */
@media (max-width: 52em) {
  .got-accountmenu {
    position: static;
  }
  .got-accountmenu__toggle {
    padding-inline: 0;
    gap: 0;   /* the collapsed name is still a flex item; a gap beside it would push the disc 3px off centre */
    justify-content: center;
    inline-size: var(--gt-tap-min);
  }
  /* Collapsed, not removed: still in the DOM, still the summary's accessible
     name. `display:none` would drop the name from what a screen reader says
     the control is. */
  .got-accountmenu__name {
    max-inline-size: 0;
  }
  .got-accountmenu__caret {
    display: none;
  }
  .got-accountmenu__panel {
    inset-inline: 0;
    inset-block-start: calc(100% + var(--gt-border-hair));   /* under the header's border, as the nav sheet */
    min-inline-size: 0;
    max-inline-size: none;
    border-radius: 0;
    border-inline: 0;
    padding-inline: var(--gt-gutter);
  }
}


/* ---------------------------------------- THE PASSWORD REVEAL (18 Aug, Batch 4)

   The eye button that sits inside a password field on the sign-in, register
   and reset forms. Migrated from site.css whole: the wrapper, the button (44
   square by the tap floor — BUG-243 gave it the number from the system, which
   is using the system and not being IN it; it is in it now), the two eyes and
   the revealed state. The RING is the system's three declarations, not the
   `outline:2px solid var(--accent)` the legacy rule hand-wrote — a control on
   the sign-in path with a ring the rest of the site does not share.

   `.got-reveal input` is a rule that reaches by position and PADDING ONLY,
   which BUG-235's rule permits: it makes room for the button at the inline
   end and sets `unicode-bidi: plaintext` so a password of Latin characters
   keeps its own direction on the Arabic page (BUG-137 territory — a mixed
   run in an RTL box is what a mirrored password looks like). Never widen it
   past `.got-reveal input`: the checkout's amount box has an `input[type=text]`
   tick that must not inherit this padding (site.css says why). */
.got-reveal {
  position: relative;
  display: flex;
}
.got-reveal input {
  padding-inline-end: calc(var(--gt-tap-min) + var(--gt-space-2));
  unicode-bidi: plaintext;
}
.got-reveal__btn {
  position: absolute;
  inset-inline-end: var(--gt-space-1);
  inset-block-start: 50%;
  transform: translateY(-50%);
  display: grid;
  place-items: center;
  min-inline-size: var(--gt-tap-min);
  min-block-size: var(--gt-tap-min);
  padding: 0;
  border: 0;
  border-radius: var(--gt-radius-sm);
  background: transparent;
  color: var(--gt-ink-muted);
  cursor: pointer;
  transition: var(--gt-transition-control);
}
/* The two eyes, sized by CLASS: a system rule that sizes may not reach a
   bare element (BUG-235). */
.got-reveal__eye,
.got-reveal__eye-off {
  inline-size: var(--gt-space-6);
  block-size: var(--gt-space-6);
}
.got-reveal__btn:hover {
  color: var(--gt-ink);
  background: var(--gt-tint-accent);
}
.got-reveal__btn:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: calc(var(--gt-ring-gap) * -1);   /* inside the field's box, so the ring is not clipped by the input's edge */
  box-shadow: var(--gt-ring-shadow);
  color: var(--gt-ink);
}
/* One eye at a time — the closed eye means the password is currently shown.
   The state is a class the script sets (`is-revealed`); the button's own
   `title` says show/hide in the page's language. */
.got-reveal__eye-off {
  display: none;
}
.got-reveal__btn.is-revealed .got-reveal__eye {
  display: none;
}
.got-reveal__btn.is-revealed .got-reveal__eye-off {
  display: block;
}
.got-reveal__btn.is-revealed {
  color: var(--gt-accent);
}


/* --------------------------------------------- THE BREADCRUMB (18 Aug, Batch 3)

   Home / Game / Service, muted, at the top of every catalogue page. Each
   link is a 44px target by the .got-account__ref recipe — padding-inline
   with an equal negative block margin, so the row keeps its own height and
   the ink stays where the text is (GAP-099 / GAP-100: the links were 22px
   tall). The separator is a glyph with no direction of its own, so the row
   mirrors with no override. The current page is a span, not a link. */
.got-crumb {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gt-space-4);   /* the links wear negative inline margins, so this is what is left between two targets: ≥8 (was 5.1 measured, 19 Aug) */
  padding-block: var(--gt-space-6) var(--gt-space-2);
  font-size: var(--gt-text-xs);
  color: var(--gt-ink-muted);
}
.got-crumb__link {
  display: inline-flex;
  align-items: center;
  min-block-size: var(--gt-tap-min);
  padding-inline: var(--gt-space-2);
  margin-inline: calc(-1 * var(--gt-space-2));
  border-radius: var(--gt-radius-xs);
  color: var(--gt-ink-muted);
  text-decoration: none;
  transition: var(--gt-transition-control);
}
.got-crumb__link:hover {
  color: var(--gt-ink);
  background: var(--gt-wash-soft);
}
.got-crumb__link:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  box-shadow: var(--gt-ring-shadow);
}
.got-crumb__sep {
  color: var(--gt-ink-muted);
}
.got-crumb__current {
  color: var(--gt-ink);
}

/* ------------------------------------ THE CARD'S HEAD AND FOOT (18 Aug, Batch 3)

   The placement rule the inventory derived — title at the start, status at
   the inline END of the title row, secondary under the title, the CTA on
   the END row — as two rows a card can wear. The head puts a pill beside a
   title without the title reflowing under it; the foot is the last row,
   pushed to the bottom of a flex-column card, a rule above it, a figure at
   the start and the "go" word at the end. Both are LAYOUT ONLY: what sits
   in them keeps its own class and rules. */
.got-card__head {
  display: flex;
  align-items: flex-start;
  justify-content: space-between;
  gap: var(--gt-space-3);
}
.got-card__head .got-pill {
  flex: none;
}
.got-card__foot {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gt-space-4);
  margin-block-start: auto;
  padding-block-start: var(--gt-space-4);
  border-block-start: var(--gt-border-hair) solid var(--gt-line);
}
.got-card__figure {
  font-size: var(--gt-text-lg);
  font-weight: var(--gt-weight-heavy);
  line-height: var(--gt-leading-tight);
  color: var(--gt-ink);
  font-variant-numeric: tabular-nums;
}
.got-card__unit {
  margin-inline-start: var(--gt-space-1);
  font-size: var(--gt-text-xs);
  font-weight: var(--gt-weight-normal);
  color: var(--gt-ink-muted);
  /* V-32 B7 (B-2): the note is `site.game.per_unit` («/ :unit») and inside
     the RTL foot a digit-led unit came apart — MEASURED on
     /ar/games/call-of-duty, Range rects left→right: «points 1000 / 9.00 $».
     `plaintext` gives the span the direction of its own first strong
     character, so the phrase keeps its internal order in either language. */
  unicode-bidi: plaintext;
}
/* -------- the facts, stacked: a column of them (the service page's "what you
   get"), the same item, the same dot. */
.got-facts--stack {
  flex-direction: column;
  gap: var(--gt-space-2);
}


/* ------------------------------------------- THE DISCLOSURE ROW (V-27 B3)

   The gift page's About-region fold: a native <details> panel whose
   <summary> is the whole ruled row. It is HERE and not in pages.css because
   a <summary> is a form-like CONTROL the browser draws, and
   SystemCoverageTest holds every such control to a class this file styles —
   the first cut styled it from pages.css and that sweep found it unclaimed
   on its first run. The fold's LAYOUT (panel padding, body grid, phone-band
   density) stays in pages.css, which loads later and wins its overrides.

   STATES, per hover-skill §4/§5: hover and open-state changes are COLOUR
   and TRANSFORM only — the chevron rotates through the duration tokens, so
   the reduced-motion block collapses it with everything else (MEASURED:
   transitionDuration 0.001s under emulated reduce); focus-visible is the
   system's three ring declarations, never a parallel treatment (MEASURED on
   a real Tab: 2px solid ring colour, 2px offset, band shadow); press needs
   no rule of its own — a disclosure row toggles, it does not travel.

   The glyph is a VERTICAL caret (\25BE, flipped by rotate when open), so
   there is no reading-direction arrow to mirror and no [dir] rule — the
   exchange arrow's lesson does not apply to it. */
.got-gc__fold {
  padding: 0;
}
.got-gc__foldhead {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--gt-space-4);
  padding: var(--gt-space-5) var(--gt-space-7);
  cursor: pointer;
  list-style: none;
  border-radius: inherit;
}
.got-gc__foldhead::-webkit-details-marker {
  display: none;
}
.got-gc__foldtitle {
  margin: 0;
}
.got-gc__foldhead::after {
  content: "\25BE";
  color: var(--gt-ink-muted);
  transition: transform var(--gt-duration-base) var(--gt-ease-standard), color var(--gt-duration-base) var(--gt-ease-standard);
}
.got-gc__fold[open] > .got-gc__foldhead::after {
  transform: rotate(180deg);
}
@media (hover: hover) {
  .got-gc__foldhead:hover .got-gc__foldtitle,
  .got-gc__foldhead:hover::after {
    color: var(--gt-accent);
  }
}
.got-gc__foldhead:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  box-shadow: var(--gt-ring-shadow);
}

/* -------------------------------------------------- THE FACTS ROW (18 Aug)

   A short list of true things — "Verified pro players · 7-day warranty ·
   Private & secure · Order chat with our team" — that used to be three
   private answers: the home hero's `.pill` (a bordered chip), the game and
   cards pages' eta `.pill`, and the register card's `.got-trust` with EMOJI
   as icons. One component: an inline list, small and muted, a success dot
   before each item as the second cue, no border, no chip. It wraps as a row,
   which is what puts the four facts on one or two lines at 390 instead of
   two rows of 37px chips. Reached by class only. */
.got-facts {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gt-space-2) var(--gt-space-5);
  list-style: none;
  margin: 0;
  padding: 0;
  font-size: var(--gt-text-xs);
  line-height: var(--gt-leading-snug);
  color: var(--gt-ink-muted);
}
.got-facts__item {
  display: inline-flex;
  align-items: center;
  gap: var(--gt-space-2);
}
.got-facts__item::before {
  content: "";
  flex: none;
  inline-size: var(--gt-space-2);
  block-size: var(--gt-space-2);
  border-radius: var(--gt-radius-pill);
  background: var(--gt-success);
}


/* ------------------------------------------- THE STATUS PILL (client area)

   One word about where a thing is, coloured by WHOSE MOVE IT IS — the tone
   is decided in ClientBrief::STATUS_TONES and this only paints it. The words
   are always there (colour is never the only channel) and the dot is a second
   cue for the colour-blind.

   CONTRAST, MEASURED BY THE VERIFIER (pixel-sampled on the raised card, 12px
   semibold so 4.5:1 applies): attention 6.49:1 · active 8.02:1 · done 6.14:1
   · muted 5.89:1 — and DANGER INK ON THE DANGER TINT IS 4.07:1, WHICH FAILS.
   The first draft of this comment quoted five higher figures derived from
   the tokens; every one overstated the render (the tint composites onto the
   card, not onto black). So the danger pill takes the destructive button's
   answer: the WORDS are --gt-ink on the tint (13.9:1) and only the DOT is
   danger — the tone stays visible, the sentence stays readable. */
.got-pill {
  display: inline-flex;
  align-items: center;
  gap: var(--gt-space-2);
  padding-block: var(--gt-space-1);
  padding-inline: var(--gt-space-3);
  border-radius: var(--gt-radius-pill);
  font-size: var(--gt-text-2xs);
  font-weight: var(--gt-weight-semibold);
  line-height: var(--gt-leading-snug);
  white-space: nowrap;
  background: var(--gt-wash-strong);
  color: var(--gt-ink-muted);
}
.got-pill::before {
  content: "";
  flex: none;
  inline-size: var(--gt-space-2);
  block-size: var(--gt-space-2);
  border-radius: var(--gt-radius-pill);
  background: currentColor;
}
.got-pill--attention { background: var(--gt-tint-warning); color: var(--gt-warning); }
.got-pill--active    { background: var(--gt-tint-accent);  color: var(--gt-accent); }
.got-pill--done      { background: var(--gt-tint-success); color: var(--gt-success); }
.got-pill--danger    { background: var(--gt-tint-danger);  color: var(--gt-ink); }
.got-pill--danger::before { background: var(--gt-danger); }
.got-pill--muted     { background: var(--gt-wash-soft);    color: var(--gt-ink-muted); }
/* V-33 Batch 1a — checking_payment: proof is in, the move is ours. Neither
   the warning tint (nothing is asked of the customer) nor the accent (no work
   has started): the strong wash with full ink, so it reads as a held state. */
.got-pill--checking  { background: var(--gt-wash-strong);  color: var(--gt-ink); }


/* --------------------------------------------------- THE STAT (client area)

   A number with a label under it. The account home's "at a glance" strip is
   three of them; the wallet's balance is one, `--hero`, at the top of the
   scale. Figures are tabular so a column of them lines up, and the figure
   sits at the container's START by flex, not by text-align — a `dir="ltr"`
   money figure on the Arabic page is a block whose `start` is the wrong
   edge, and shrinking it to its content is what puts it beside its label
   without a [dir] rule (measured on /ar/account/wallet). */
/* Three across at every width — the strip is read as one line, and at 390
   the tiles are ~110px, which the figure and a wrapped two-word label fit.
   auto-fit put the third tile alone on a second row on a phone (measured). */
.got-stats {
  display: grid;
  gap: var(--gt-space-3);
  grid-template-columns: repeat(3, minmax(0, 1fr));
}
.got-stat {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gt-space-1);
  padding-block: var(--gt-space-4);
  padding-inline: var(--gt-space-5);
  background: var(--gt-surface-raised);
  border: var(--gt-border-hair) solid var(--gt-line);
  border-radius: var(--gt-radius-lg);
  box-shadow: var(--gt-level-base);
}
.got-stat__figure {
  font-family: var(--gt-family-numeric);
  font-size: var(--gt-text-xl);
  font-weight: var(--gt-weight-bold);
  font-variant-numeric: tabular-nums;
  line-height: var(--gt-leading-tight);
  color: var(--gt-ink);
}
.got-stat__label {
  font-size: var(--gt-text-xs);
  line-height: var(--gt-leading-snug);
  color: var(--gt-ink-muted);
}
.got-stat--attention .got-stat__figure { color: var(--gt-warning); }
.got-stat--active    .got-stat__figure { color: var(--gt-accent); }
.got-stat--done      .got-stat__figure { color: var(--gt-success); }
/* The hero: the one figure a page is about — wider padding and room beside
   it for the page's one action. Its figure is the wallet's own
   `.got-credit__balance` (bridged below, at the top of the type scale), so
   there is no `--hero .got-stat__figure` rule: a rule nothing on /design
   could wear is a rule DesignPageTest refuses, and it did. */
.got-stat--hero {
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--gt-space-4) var(--gt-space-6);
  padding-block: var(--gt-space-6);
  padding-inline: var(--gt-space-6);
  /* 🔴 TWO POLES NEED A CARD NARROW ENOUGH FOR THE EYE TO CROSS IT.
     `space-between` is the right arrangement for a figure and the one action
     on it, and it is wrong at the content column's full width: MEASURED on
     /account/wallet, the gap between the balance and "Add to your balance"
     was 816px at 1920 AND 816px at 1440 — identical, because `.container`
     caps at `--gt-container` (1140 of content) long before 1920. So the void
     is this card's, not a container's, and widening the page would make it
     worse rather than better. (V-01's measurement that a wider container
     harms this catalogue already said the same thing from the other side.)
     The same figure is 639px at 1024, 398 at 768 and 26 at 390 — the defect
     arrives with the width and the phone never had it.

     A cap rather than a different `justify-content`: `flex-start` moves the
     void to the trailing edge without closing it, and one figure plus one
     button cannot furnish 1140px in any arrangement. At this value the card
     holds its two poles with air between them and stops being a rule across
     the page. Below the cap nothing changes at all, which is why 390 and 768
     are untouched.

     It also answers the brief's fifth observation without touching the type
     scale: the balance reads as "the loudest thing on the page" because it
     was a 36px figure alone on a 1140px bar, not because 36px is wrong. The
     figure IS this page's subject and shrinking it would be hiding the
     number the customer came for. Guarded by nothing before this round —
     `falsify.php` has no arm on `.got-stat--hero` and no test constrains its
     `justify-content` — so V-36 adds one. */
  max-inline-size: 32rem;
}
.got-stat__body {
  display: flex;
  flex-direction: column;
  align-items: flex-start;
  gap: var(--gt-space-1);
}
/* THE HERO'S ACTIONS — V-37. One lone button reads as a card that is not
   finished; two read as an action row, which is the whole of why the second
   one is here. It is a GROUP so that `--hero`'s `space-between` keeps the
   figure at one pole and the actions at the other: two bare children would
   have put one button at each end with the figure between them.

   `flex-wrap` and not a breakpoint — at 390 the hero is already a wrapped
   column and the two buttons stack there on their own. */
.got-stat__actions {
  display: flex;
  align-items: center;
  gap: var(--gt-space-3);
  flex-wrap: wrap;
}
/* WHAT LAST MOVED THE BALANCE. The reason and the date, at the smallest step
   the muted ink is legible at, directly under the figure it belongs to.

   It carries the REASON as well as the date because a bare date under a money
   figure reads as an expiry, and store credit here has none — the argument is
   written where the markup is. Not tabular and not isolated: it is a sentence
   about a movement, not a figure, and the date inside it is already a
   finished localised string. */
.got-credit__latest {
  font-size: var(--gt-text-xs);
  line-height: var(--gt-leading-snug);
  color: var(--gt-ink-muted);
}


/* ------------------------------------------- THE LIST LINE (client area)

   The customer's orders as CARDS that behave like the home grid: one link
   is the whole row (number, service, status pill, date, price), and the
   hover-skill's states apply — lift on hover, focus identical to hover plus
   the system's ring, the gradient edge fading in on its own layer, the press
   as `--gt-press-scale`. Reduced motion stops the transforms here, where
   they are defined, the way the media card does.

   🔴 ONE TAB STOP FOR THE ORDER, AND THE ACTIONS ARE SIBLINGS. "Pay for this
   order" and "View proof" are DIFFERENT destinations, so they are separate
   controls beside the row — never inside the link (a control inside a
   control is the account-menu mistake). A payable order is therefore two
   stops: the order, then its payment. That is the honest count for a row
   that has two things to do, and it is written in the partial as well.

   The row wraps as a whole under a narrow viewport: the link takes the full
   width and the actions drop under it — no table, no Price column off the
   edge (audit C9, 390px). */
.got-lines {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--gt-space-3);
}
.got-line {
  display: flex;
  flex-wrap: wrap;
  align-items: stretch;
  gap: var(--gt-space-3);
}
.got-line__link {
  position: relative;
  flex: 1 1 18rem;   /* a measure: below it the actions wrap under the row */
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  grid-template-areas:
    "head figure"
    "meta figure";
  column-gap: var(--gt-space-4);
  row-gap: var(--gt-space-1);
  align-items: center;
  min-block-size: var(--gt-tap-min);
  padding-block: var(--gt-space-4);
  padding-inline: var(--gt-space-5);
  border-radius: var(--gt-radius-lg);
  background: var(--gt-surface-raised);
  border: var(--gt-border-hair) solid var(--gt-line);
  box-shadow: var(--gt-level-base);
  color: var(--gt-ink);
  text-decoration: none;
  transition: transform var(--gt-duration-base) var(--gt-ease-standard);
}
/* The edge: the media card's gradient ring, on its own layer, opacity only. */
.got-line__link::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  border-radius: var(--gt-radius-lg);
  padding: var(--gt-hairline);
  background: var(--gt-gradient-brand);
  -webkit-mask: var(--gt-mask-ring);
          mask: var(--gt-mask-ring);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  opacity: 0;
  transition: opacity var(--gt-duration-base) var(--gt-ease-standard);
}
.got-line__head {
  grid-area: head;
  display: flex;
  /* BUG-350 (19 Aug): NOWRAP. With wrap, the one row whose pill was long
     ("Pending Payment", head 206px at 390) dropped its pill under the ref
     while its neighbours kept theirs on the row — two card shapes in one
     list. The ref keeps its width, the pill may shrink and wrap its OWN
     words instead (min-inline-size: 0 on it), so every row keeps one shape. */
  flex-wrap: nowrap;
  align-items: center;
  gap: var(--gt-space-2) var(--gt-space-3);
  min-inline-size: 0;
}
.got-line__head > .got-pill {
  flex: 0 1 auto;
  min-inline-size: 0;
  white-space: normal;
  text-align: center;
}
.got-line__ref {
  flex: 0 0 auto;
  font-weight: var(--gt-weight-bold);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.got-line__title {
  font-size: var(--gt-text-sm);
  color: var(--gt-ink);
  overflow-wrap: anywhere;
  /* V-32 B7 (B-1): the title is «<bdi>service</bdi> · <bdi>game</bdi>», and
     as inline text an RTL paragraph reorders the two isolates while the line
     breaks INSIDE the second one — MEASURED on /ar/account at 390 (touch,
     193.6px meta column): line 1 «Call of · Nuke / Objective», line 2 «Duty».
     A flex row wraps BETWEEN the isolates and never inside one, and the gap
     replaces the collapsed inter-item whitespace. Measured after, same page:
     «Nuke / Objective ·» then «Call of Duty», each isolate whole; a
     29-character snapshot («PlayStation Gift Card $10 USA», 197.1px unwrapped
     at this size, wider than the column) still wraps INSIDE itself through
     `overflow-wrap`, and in reading order. */
  display: flex;
  flex-wrap: wrap;
  column-gap: var(--gt-space-2);
}
.got-line__meta {
  grid-area: meta;
  display: flex;
  flex-wrap: wrap;
  gap: var(--gt-space-1) var(--gt-space-3);
  font-size: var(--gt-text-xs);
  line-height: var(--gt-leading-snug);
  color: var(--gt-ink-muted);
}
.got-line__figure {
  grid-area: figure;
  align-self: center;
  font-family: var(--gt-family-numeric);
  font-weight: var(--gt-weight-semibold);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  text-align: end;
}
/* The list as a GRID — the home page's store-card strip (18 Aug): the same
   line, two or three abreast when the width allows, one when it does not.
   The track floor is a measure. */
.got-lines--grid {
  grid-template-columns: repeat(auto-fill, minmax(min(16rem, 100%), 1fr));
}
/* A line that is not a link — a coming-soon brand on the strip: the same
   box, no href, and no state can reach it (the state rules below select
   the class on an interactive element; a static span wears --static so a
   test can tell them apart, and takes no lift). */
.got-line__link--static {
  cursor: default;
}

/* The siblings: real controls, system buttons, 44px by the system's floor,
   8px apart — the touch floor between adjacent targets (the verifier caught
   6px). */
.got-line__actions {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: var(--gt-space-3);
}

.got-line__link:not(.got-line__link--static):hover,
.got-line__link:not(.got-line__link--static):focus-visible {
  transform: translateY(calc(-1 * var(--gt-list-lift)));
}
.got-line__link:not(.got-line__link--static):hover::after,
.got-line__link:not(.got-line__link--static):focus-visible::after {
  opacity: 1;
}
.got-line__link:not(.got-line__link--static):focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  box-shadow: var(--gt-ring-shadow);
}
/* Press after hover in source: same specificity, order decides. */
.got-line__link:not(.got-line__link--static):active {
  transform: scale(var(--gt-press-scale));
  transition-duration: var(--gt-duration-instant);
}
@media (prefers-reduced-motion: reduce) {
  .got-line__link:not(.got-line__link--static):hover,
  .got-line__link:not(.got-line__link--static):focus-visible,
  .got-line__link:not(.got-line__link--static):active {
    transform: none;
  }
}


/* -------------------------------------------- THE LEDGER (the wallet page)

   The store-credit history as ROWS rather than a four-column table: what
   happened and when at the start, the amount and the balance after it at the
   end. At 390 the table wrapped its date into three lines beside a figure
   that could not; a row keeps the two halves readable at any width. The
   figures inside carry the legacy `.got-credit__amount` (bridged below) and
   its bidi reasoning; the ROW aligns them by flex, so no `dir` and no
   text-align is asked of the figure. */
.got-ledger {
  list-style: none;
  margin: 0;
  padding: 0;
}
.got-ledger__row {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  justify-content: space-between;
  gap: var(--gt-space-2) var(--gt-space-5);
  /* V-38, Study D: more air per row. The ledger is the page now, so a row is
     a statement line rather than a table cell — measured off the study at
     ~1.05rem, which is `--gt-space-5` (16px) on the token scale. */
  padding-block: var(--gt-space-5);
  border-block-end: var(--gt-border-hair) solid var(--gt-line);
}
.got-ledger__row:last-child {
  border-block-end: 0;
}
.got-ledger__what {
  flex: 1 1 14rem;
  min-inline-size: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gt-space-1);
  /* 🔴 A COLUMN FLEX CONTAINER STRETCHES ITS ITEMS ACROSS THE CROSS AXIS, AND
     THAT TURNS AN inline-flex CHIP INTO A FULL-WIDTH BAR.

     V-37 MEASURED it on the rendered page the moment the reason became a
     `.got-pill`: every chip ran the whole width of the column, so the ledger
     read as five coloured bars rather than five labels. `.got-pill` is
     `display: inline-flex`, which decides how it lays out INSIDE itself and
     says nothing about how its parent sizes it — the default here is
     `stretch`.

     This is BUG-689's shape one component over: a box that is somebody else's
     flex item does not get to decide its own width. `.got-ledger__money`
     opposite has carried `align-items: flex-end` since it was written, for
     the same reason; this side simply never needed it until something in it
     was narrower than the column. `flex-start` and not `start` because it is
     the spelling the sibling uses, and it mirrors under [dir] either way. */
  align-items: flex-start;
}
.got-ledger__reason {
  font-size: var(--gt-text-sm);
  color: var(--gt-ink);
}
/* 🔴 THE CHIP IN A LEDGER ROW MAY WRAP, AND `.got-pill` MAY NOT.

   `.got-pill` is `white-space: nowrap` everywhere else because it carries an
   order status — one or two words that must not break. In the ledger it
   carries a REASON, and the longest of the five is "Refunded to your balance".
   At 390 the row's `__what` column is about 300px and a nowrap chip that long
   pushes the row wider than the panel, which takes the whole page sideways —
   BUG-100's shape, on a page that has already had it once.

   So the wrap is granted HERE, in the context that needs it, rather than
   taken away from the component every other caller relies on. */
.got-ledger__what .got-pill {
  white-space: normal;
}
/* 🔴 THE ALTERNATING WASH IS GONE — V-38, Study D.

   V-37 added `.got-ledger__row:nth-child(odd){background:var(--gt-wash-soft)}`
   as a tonal step, and it was right for a row with 12px of block padding: at
   that density the band did the grouping the hairline could not. Study D
   raised the row to 16px and made the ledger the page, and MEASURED on the
   rendered page the two together read as stripes rather than as a statement —
   the head row in particular came out looking like a table header.

   The hairline between rows was always doing the separating; the wash was
   doing emphasis nobody had asked for. Removed rather than retuned, and the
   V-37 argument is kept above so the next round can see it was considered
   and superseded rather than forgotten. (BUG-697 — the wash landing on the
   empty state — goes with it; the `odd` fix that avoided it is no longer
   load-bearing and the empty state is guarded by its own render.) */
.got-ledger__when {
  font-size: var(--gt-text-xs);
  color: var(--gt-ink-muted);
}
.got-ledger__money {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: var(--gt-space-1);
  text-align: end;
  /* When the row wraps at 390 the money block is alone on its line; auto
     start margin keeps it at the row's END there too, so its end-aligned
     items and its position agree (the verifier saw "ADDED" floating). */
  margin-inline-start: auto;
}
.got-ledger__after {
  font-size: var(--gt-text-xs);
  color: var(--gt-ink-muted);
}
/* Study D's end column: the signed amount is the row's own figure and the
   balance after it is the quiet consequence. The amount was inheriting the
   body step; it takes the money step now, one level under the capsule's. */
.got-ledger__money > .got-credit__amount {
  font-size: var(--gt-text-lg);
  font-weight: var(--gt-weight-bold);
}
.got-ledger__after .got-credit__amount {
  display: inline;
  font-weight: var(--gt-weight-normal);
}
/* The head row: the four column labels, in two groups, so the ledger keeps
   its shape when it is empty. Tighter than a movement row. */
.got-ledger__row--head {
  padding-block: var(--gt-space-2);
}
/* The carried-forward line at the foot of a page that is not the last. */
.got-ledger__row--carry .got-ledger__reason {
  color: var(--gt-ink-muted);
}
/* 🔴 A COLUMN LABEL OVER ZERO ROWS LABELS NOTHING — V-36, 7 Sep, Omar's own
   observation on the empty wallet. `--empty` is set on the LEDGER because
   that is the thing that is empty; the head row is only where the symptom
   shows. The caller derives it from the same paginator the `@forelse` reads,
   and the reasoning for why that is not BUG-141 is on the caller.

   `display: none` and not `visibility: hidden`: the row is `aria-hidden` in
   the markup, so it is already outside the accessibility tree, and reserving
   34px of empty rule above a centred empty state is the void this round is
   closing. It is a decorative label with no state of its own, so there is
   no focus guard to defeat — `display:none` is invisible to every focus
   guard, which is a hazard only where something inside can take focus. */
.got-ledger--empty .got-ledger__row--head {
  display: none;
}


/* ------------------------------------------- THE STORE CARD IS THE GAME TILE
   V-18d, 27 Aug — Omar's design reversal, and the whole `.got-store` block
   that stood here (a seam, a bloom, a chip, a note row, an arrow lane, a
   container query, a no-preference motion block — V-18b/V-18c) is DELETED,
   not migrated. The store card is `.got-gamecard` in TWO FRAMES since V-20
   B4 — the CATEGORY tile (`--square`, 1:1) and the PRODUCT card
   (`--product`, 2:3) — and since V-25 B2 (29 Aug) the two frames wear ONE
   FRAMED ANATOMY, stated once below: the card is a flex column on the
   raised surface, the artwork framed in a media box IN FLOW with nothing
   overlaid on it, and the words BELOW it on the card. One partial renders
   all of it (`components/media-tile.blade.php`); no second implementation
   of the edge, the sheen or the capsule exists under store-specific names,
   and the states — hover zoom+lit, the 6px lift, press, ring, touch block,
   reduce block — are the TILE'S RULES reached by class, never copied.

   HOW THE ANATOMY GOT HERE, each step measured before it was written:

     · V-21 B2 (28 Aug) took the PRODUCT's words off the artwork: gift-card
       covers are light product scans carrying their own typography, and
       the overlay title sat on the art's own lockup.
     · V-25 B2 (29 Aug) takes the SQUARE the same way, for the square's own
       measured reason: V-22 B4 re-centred the six 600×600 brand marks in
       their canvases — CORRECT, AND IT STAYS — and the overlay layout had
       been relying on the marks sitting wrongly high. Ink bounding boxes
       sampled per cover: the marks now END at 77.2–84.3% of canvas height
       (playstation 21.0–81.5, xbox 23.8–77.2, steam 21.2–80.3, google-play
       23.3–77.2, nintendo 20.7–81.2, apple 23.2–84.3) while the overlay
       body began at 54.3–54.5% of the frame (rendered, 1536). The marks
       sat under the words. DO NOT fix that by un-centring the art, and do
       not reach for an `object-position` nudge — that is the FOCUS map,
       deleted by V-19a for the reason its docblock gives (a focus value a
       frame cannot crop is a number that looks measured and does nothing).
       The words move; the artwork stands.

   🔴 THE STORE VEIL IS DELETED WITH ITS LAST WEARER — V-25 B2. V-21 B2
   left the SQUARE as `--gt-media-veil-store`'s only reader; with the
   square's words off the artwork a scrim over a picture with no words on
   it is a contrast instrument holding nothing (the product's own V-21 B2
   argument, verbatim), so the square renders NO veil span, the two veil
   rules that stood here are deleted (DesignPageTest refuses a rule no page
   can reach), and the `--gt-media-veil-store` / `-store-soon` tokens are
   deleted with their reader — a token nothing reads is a token somebody
   will wire up. Their pixel-anchored derivation (stops counted from the
   bottom edge in px, .74 at 120px) died with them; git holds it. The soon
   square loses the orange WASH that rode in the soon variant of that
   token: what still says "on its way" is the dim, the badge, and the
   decorative hover glow — all of them the tile's own soon grammar. The
   TILE's veils (`--gt-media-veil`, `-soon`) are untouched: the games grid
   is still the overlay card, and HomepageTest pins exactly that pair.

   THE SOON GLOW (`.got-gamecard--soon::after`, inset on the CARD in the
   compared block) NOW RINGS THE PANEL TOO. The 1px ring traces the whole
   card exactly as the live card's `__edge` does, and at z-index 4 under
   the body's 5 it never touches the GLYPHS. Confining it to the media box
   would need a third `--soon:hover` selector, and ComingSoonGameTest pins
   exactly two. The compared block's comment that the glow "does not wash
   orange over the title and the badge ABOVE it" is written about the
   overlay tile, where it remains true as stated.

   🔴 BUT "THE BLOOM READS AT THE ARTWORK'S EDGES" WAS WRITTEN FROM LOOKING
   AND THE PIXELS SAY OTHERWISE — V-25's tester, decoding the rendered
   composite of a hovered XBOX square, 6×6px means:

     panel, bottom-left    light 255,255,255 -> 254,230,219   dark 23,27,40 -> 74,53,52
     panel, bottom-right   light 255,255,255 -> 255,231,219   dark 23,27,40 -> 74,53,52
     panel, right of title light 255,255,255 -> 255,243,238   dark 23,27,40 -> 48,40,46

   In the LIGHT theme the whole white footer goes peach on hover. Contrast
   is unharmed — the badge's ink on the washed tint still computes ~13.9:1 —
   and the LIVE square's panel stays pure white, so this is soon-only. The
   glow is LEFT AS IT IS pending Omar's call, and this paragraph now says
   what the wash actually does rather than what its author expected: this
   was the one light-theme surface in the batch described rather than
   measured, in a round that measured two others out at 4.44 and 1.90.

   🔴 ALL SIX CARDS ARE THE SAME HEIGHT BY CONSTRUCTION, AND THE RESIDUAL IS
   A DEVICE PIXEL — NOT NOTHING, WHICH IS WHAT THIS BATCH FIRST CLAIMED.

   The mechanism: nothing in the body can grow the card differently. The
   media box is a ratio, the title is one line with an ellipsis, `__meta`
   reserves one line whether or not a tile has a count, and the soon badge
   reserves THE CAPSULE'S OWN ARITHMETIC (its `min-block-size` in the
   compared block is written as the capsule's padding, line and hairlines,
   never as the number 27) so a badge card's slot matches a capsule card's.

   MEASURED at dpr 1, all six identical. MEASURED IN A REAL BROWSER AT
   dpr 1.25 — Omar's own machine — they are not:

     330.25 (the live card) · 330.65 · 330.663 · 330.65 · 330.65 · 330.663
     spread 0.413px

   The cause is exact and it is not a layout fault: the capsule DRAWS a 1px
   border, which Chrome snaps to one device pixel = 0.80 CSS px at dpr 1.25,
   so the capsule measures 26.6; the badge draws none and takes its height
   from a `min-block-size` that is a CSS length and is not snapped, so it
   measures 27.0. Width-independent, so it holds at every breakpoint.

   🔴 AND THE COMMIT SAID "the 0.4px spread does not reproduce", MEASURED AT
   "dsf 1 and 1.25", WHICH THE INSTRUMENT COULD NOT SEE. Headless Chrome does
   not device-snap borders — it reports `borderTopWidth: 1px` at emulated dsf
   1, 1.25, 1.5 and 2 alike — so the sweep that produced that null result was
   incapable of expressing the state it was run to test. That is this
   project's own rule, MEASURE THE INPUT THE GUARD ACTUALLY READS, applied to
   a measurement instead of a guard: an emulated device pixel ratio is not a
   device pixel ratio.

   IT IS LEFT AT 0.413px DELIBERATELY. Closing it means either giving the
   badge the capsule's whole box (padding, line-height and a real transparent
   border) — a rule in the BYTE-COMPARED block, which drags the hover-skill's
   §6 into a sub-pixel fix — or a `--square` fork that restates the capsule's
   metrics, which is the second copy the badge's derived `min-block-size`
   exists to prevent. Filed instead, with the cause, so the next person
   spends the byte-compare on something worth it.

   THE SHARED BLOCK IS (0,2,0) ON PURPOSE. This section sits EARLIER in
   the file than THE MEDIA CARD block, and the base `.got-gamecard` carries
   `aspect-ratio: 2 / 3` at (0,1,0) — a bare one-class rule here would TIE
   at (0,1,0) and lose on source order (V-18a Batch 5's exact lesson, one
   component over). Two classes win on specificity wherever the rule sits,
   which is what lets the store's rules live here, OUTSIDE the
   byte-compared block, keeping the skill's §6 diff to genuinely stale
   prose. */

/* THE FRAMED ANATOMY — ONE BLOCK, TWO WEARERS (V-25 B2). The ratio comes
   off the card (its height is the artwork plus the panel), the flex column
   lays the media box over the body, and the panel ground is the RAISED
   surface — the words sit on the card now, so the card is a card, not a
   well (`--gt-surface-sunken`, the base rule's ground, is the well a
   picture sits in).

   🔴 AND THE SENTENCE THAT STOOD HERE SAID "nothing in the suite reads this
   property, so its absence here would be silent" — WHICH WAS FALSE WHEN IT
   WAS WRITTEN, BY THIS BATCH'S OWN GUARD. It was true of the tree the impact
   pass read, and the shared-block regex added in the same commit pins
   `background: var(--gt-surface-raised)`. V-25's tester mutated it to
   `--gt-surface-sunken` and got a RED carrying that guard's own message.
   Kept rather than deleted: a comment describing the tree it was drafted
   against, shipped beside the guard that had already made it wrong, is this
   project's named shape and the correction is worth more than the tidy
   version.
   A selector LIST rather than a third class: the frame classes already
   name the two wearers, the markup stays exactly what every XPath
   equality in the suite pins, and a rule both frames wear cannot drift
   into two copies. */
.got-gamecard.got-gamecard--product,
.got-gamecard.got-gamecard--square {
  aspect-ratio: auto;
  display: flex;
  flex-direction: column;
  background: var(--gt-surface-raised);
}
/* THE MEDIA BOX, in flow: positioned so the absolute `__img` keeps it as
   its containing block, the type-mark padding zeroed (it lifted the mark
   clear of an overlay body that no longer exists — the mark centres in
   the frame now), and the REAL EDGE under the artwork: the same hairline
   the card itself wears, so the divider and the frame read as one drawn
   boundary. `overflow: hidden` is the base rule's — the hover zoom clips
   against this box, not the card. */
.got-gamecard--product .got-gamecard__media,
.got-gamecard--square .got-gamecard__media {
  position: relative;
  inset: auto;
  padding-block-end: 0;
  border-block-end: var(--gt-border-hair) solid var(--gt-line);
}
/* THE FRAME is the one thing the two wearers disagree on, each stated
   once: the product keeps the 2:3 its covers are cut to (V-20 B4 — the
   1:1 frame kept 66.7% of every 1024×1536 cover, GAP-160), the category
   keeps the square its 600×600 marks are cut to. StoreSectionsTest counts
   the aspect-ratio declarations per frame and refuses a second copy.

   AND `--gt-media-focus` (the base card's `center 30%`, on `__img`'s
   object-position) IS AS GOOD AS INERT ON TODAY'S ASSETS, MEASURED per
   rendered image (V-25 B2, 1536): every 2:3 cover crops HORIZONTALLY
   (272.2 of scaled width in a 271.3 box), where the Y component decides
   nothing; the six squares carry ~1px of vertical slack, so `30%` moves
   their crop 0.2px against `center`.

   🔴 THAT SLACK IS THE IMAGE'S, NOT THE BOX'S, and the first cut of this
   paragraph named the wrong element — V-25's tester. The MEDIA BOX measures
   228.000 × 228.000, ratio 1.00000 (`aspect-ratio` applies to the border
   box, so the block-end hairline is inside it, not added to it). The
   229.1 × 228.1 reading is the `__img` element under the rest
   `scale(1.005)`, and it is the image's overhang against a 1:1 source that
   leaves `object-position` anything at all to decide. The conclusion is
   unchanged and confirmed: 0.2px. It STAYS despite that:
   it is the only thing deciding where an ADMIN-UPLOADED cover taller
   than its frame crops — uploads never get a GameArtwork::FOCUS entry,
   so the day one arrives this default is the whole answer, and `center
   30%` (frame the upper third, where a subject sits) is the value that
   was chosen by looking. Nothing in the suite asserts its VALUE; this
   measurement is the record. */
.got-gamecard--product .got-gamecard__media {
  aspect-ratio: 2 / 3;
}
.got-gamecard--square .got-gamecard__media {
  aspect-ratio: 1 / 1;
}
/* THE BODY joins the flow: `position: static` makes the base rule's inset
   properties inert (they only apply to positioned boxes) and the flex
   column above lays it under the media box. Padding and gap are the base
   rule's own; the base `z-index: 5` still applies to a static flex item,
   which is what keeps the panel's words above the card-wide sheen. */
.got-gamecard--product .got-gamecard__body,
.got-gamecard--square .got-gamecard__body {
  position: static;
}

/* ------------------------- THE SQUARE'S SURFACE VOICE — V-25 B2, 29 Aug

   The category tile KEEPS the poster-wordmark title voice — uppercase,
   the Latin lean, one line with an ellipsis, `--gt-text-tile`, all the
   base rule's — and re-inks it for the ground it actually sits on. The
   base title and meta read the ON-ARTWORK family (`--gt-on-ink`,
   `--gt-media-meta`) under the caption shadow, pinned against the dark
   veil; on the raised panel that family is the follows-the-ground error
   pointing the other way — near-white ink on a white card the moment the
   light theme paints the panel (V-21 B2's own reasoning at the product
   title fork, and the V-18d Critical read in reverse). So: theme-scoped
   surface inks, no shadow (a shadow under ink-on-surface is smear, not
   separation). The meta is `--gt-ink-muted` — secondary by colour, as its
   .76-alpha on-artwork value was — AA on every surface by the figures on
   its definition. Only colour and shadow fork here: the title's layout
   properties stay the base rule's own, which the title-ownership sweep in
   StoreSectionsTest holds to exactly two rules, and this is neither. */
.got-gamecard--square .got-gamecard__title {
  color: var(--gt-ink);
  text-shadow: none;
}
.got-gamecard--square .got-gamecard__meta {
  color: var(--gt-ink-muted);
  text-shadow: none;
}
/* THE CAPSULE ON THE SURFACE. The base capsule is clothed in the
   on-accent family (`--gt-media-cta-fill/-line`, `--gt-on-accent`) at a
   .72 resting opacity — measured against artwork, which does not follow
   the page. The panel does follow it, so the square's capsule takes
   theme-scoped clothing: a GHOST at rest — the accent as text on the bare
   panel (the role `.got-pill--active` and the V-22 B2 price hover already
   use it in), the neutral strong hairline, at FULL opacity, because on a
   themed surface the resting quiet is carried by the clothing, as every
   quiet button carries it, not by fading the whole control (BUG-241:
   opacity fades text and fill together, so a measured contrast stops
   being the figure on screen). NOT the accent 12% tint under it: that was
   built first and MEASURED OUT — the light accent is a dark teal and its
   own tint dragged the label to 4.44:1 in the light theme, under the 4.5
   floor for a 13px label. Hover and focus-visible complete the ghost to
   the solid accent with `--gt-ink-on-accent` words — the same completion
   grammar V-13 gave the capsule, in the theme's own colours. The pair is
   (0,5,0) BECAUSE IT MUST BEAT the base completion rule: that rule is
   (0,4,0) and LATER in the file, and on the panel its on-accent fill is
   the wrong family (source order decides ties, and losing is silent).
   THE FIGURES ARE NOMINAL RATIOS ON THE SPECIFIED COLOURS, dark / light,
   at 1536: label at rest 10.14 / 5.27 on the panel, completed 11.40 / 4.95
   on the solid accent; the square title 13.86 / 17.42 and the count
   6.53 / 6.37 beside it. Every figure clears the 4.5 body floor in both
   themes, and the tightest — the completed capsule in the light theme —
   was independently re-measured at 5.05.

   🔴 THEY WERE LABELLED "worst glyph pixel over the flattened composite",
   WHICH THEY ARE NOT — V-25's tester, who re-took them and could tell
   because the title and count figures are BYTE-IDENTICAL to the token
   comments at the top of this file, and a per-pixel sample cannot land on a
   nominal ratio twice. Re-sampled by the tester over the real composite at
   scale 4, the same colours read 10.53 / 5.73 and 10.52 / 15.35 — every one
   MORE generous than what is written here — while a true worst-core-glyph
   sample reads 7.95 / 4.24 for the resting label and 5.15 / 4.47 for the
   count, which is antialiasing at the glyph edge rather than the colour
   pair this block decides. So the CONCLUSION survives on every method and
   the numbers here are conservative; only the method label was a stronger
   claim than the arithmetic behind it, and this file has paid for that
   exact shape before. */
.got-gamecard--square .got-gamecard__go {
  opacity: 1;
  background: transparent;
  border-color: var(--gt-line-strong);
  color: var(--gt-accent);
}
/* `border-color: transparent`, NOT the accent: the fill paints under the
   border area, so the pixels are identical either way — and an accent
   border on a `:focus-visible` rule is a stroke the RING can share a
   colour with, which FocusVisibilityTest refuses (a ring the same colour
   as a border under it paints the control twice; this rule was caught by
   exactly that guard on its first run). */
.got-gamecard.got-gamecard--square:not(.got-gamecard--soon):hover .got-gamecard__go,
.got-gamecard.got-gamecard--square:not(.got-gamecard--soon):focus-visible .got-gamecard__go {
  background: var(--gt-accent);
  border-color: transparent;
  color: var(--gt-ink-on-accent);
}
/* THE SQUARE'S BADGE READS INK ON THE SOON TINT — the status-token rule
   ("the system NEVER paints a message in the status colour on a themed
   surface — it paints --gt-ink on the status TINT and lets the tint carry
   the meaning") applied to the one soon treatment that now sits on a
   surface the theme paints. The base badge is soon-on-tint (Omar, 19 Aug)
   and that stays right on the GAME tile, whose badge sits on artwork
   behind the veil; on the square's panel the theme-independent orange
   glyph measured 1.90:1 the moment the light theme painted the panel
   (pixel-sampled, 1536) — the V-18d Critical's exact shape. Ink on the
   tint samples 9.87 dark / 13.15 light, and the orange still says why.

   (0,4,0) AND BOTH PROPERTIES PINNED ON PURPOSE: the decorative hover's
   brighten (`--soon` solid + `--gt-ink-on-accent` words, (0,3,0) in the
   compared block) must NOT reach this badge — completed to solid orange,
   ink-on-accent words are the light page's own near-white, ~2.2:1 on the
   solid soon BY ARITHMETIC (flat fill against flat glyph; the state is
   suppressed here before it can render, so there is nothing to sample).
   So on the square the badge holds still and
   the decorative hover is the glow alone; the tile's badge keeps its
   brighten. A colour-only fork would have taken the brighten's solid
   background with this rule's ink words — the worst pair of the four. */
.got-gamecard.got-gamecard--square.got-gamecard--soon .got-gamecard__badge {
  background: var(--gt-tint-soon);
  color: var(--gt-ink);
}
/* ONE LINE ON THE SQUARE'S BADGE. The game tile never renders narrow enough
   to wrap COMING SOON (its floor is one full-width phone column); the
   square's 2-col floor is a ~130px card, where the English badge would wrap
   and a soon card would stand taller than its row — the exact rhythm break
   V-18b measured (217.77 vs 201.58) and pinned, and on the FLOWED panel a
   taller body grows the whole card rather than hiding in an overlay.
   Scoped to the square rather than added to the shared badge rule: the
   tile cannot reach the state, and a rule on it would be a guard for a
   state nothing can reach. */
.got-gamecard--square .got-gamecard__badge {
  white-space: nowrap;
}
/* ------------------- THE PRODUCT CARD LEAVES THE ARTWORK — V-21 B2, 28 Aug

   Omar's reason, MEASURED at 1536 before this was written: a gift-card
   cover is a LIGHT product scan carrying its own typography — a white
   header strip, a flag, "$10 WALLET TOP-UP", the "PlayStation.Store"
   lockup and a line of body copy. The overlay title sat directly on that
   lockup and the money run bled into the art's own copy. The game tile's
   overlay works because game covers are dark, textless key art graded for
   it; a gift card is the opposite, so the words come OFF the picture:

     ┌───────────┐
     │  artwork  │   the media box in its own frame — nothing overlaid:
     │           │   no title, no veil, no gradient wash
     ├───────────┤   a real hairline edge, not a gradient veil
     │ Title     │   on the card surface, up to two clamped lines
     │ $11.50 [] │   the money run, with the sold-out pill as the exception
     └───────────┘

   Since V-25 B2 that anatomy is the SHARED FRAMED BLOCK above — the square
   wears it too — and what remains under this heading is the PRODUCT's own
   half: the catalogue title voice, the money run and the pill seats. The
   card is still ONE `<a>`, one Tab stop, and every hover-skill state is
   the TILE'S OWN rules reached by class — the 6px lift, the artwork
   zoom+lit (clipped by the media box), the edge ringing the WHOLE card,
   the sheen (which passes UNDER the panel words — the body's base
   `z-index: 5` applies to a static flex item), the press, the system's
   three-declaration ring, the touch settle and the reduce block. The
   markup omits the veil span and the capsule (this card's anatomy has no
   CTA label — the price row is the body), and the scanline layer stays
   card-wide: at 1.4% white under `overlay` it is imperceptible on the
   panel and keeping it keeps the hover grammar whole.

   ROW RHYTHM IS RESERVED, NOT STRETCHED: the title holds two lines and
   the meta row holds the pill's height (each derived below), so every
   product card in a row is the same height BY CONSTRUCTION — including
   /design's `<li>`-wrapped specimens, which grid-stretch cannot equalise.

   THE PRODUCT CARD'S TITLE: UP TO TWO LINES, CLAMPED — V-18c's BUG-482 fork
   (the names may NOT be shortened: `orders.service_name` is a snapshot, and
   renaming the catalogue strips the brand off historic receipts).

   🔴 THE TILE'S TITLE TREATMENT DOES NOT SURVIVE THE PORT, MEASURED. At
   the tile's 18px/700/UPPERCASE, "PS PLUS ESSENTIAL" is ~195px — wider
   than the title box on the category page's 180px track (156px) — so the
   greedy wrap breaks after "PS PLUS" and the name needs THREE lines: a
   two-line clamp at the tile treatment CLIPPED the name at EVERY width
   (rendered, 1536 and 390, both languages), which is BUG-482 reopening
   inside its own port. The fork therefore keeps V-18c's fix metrics —
   `--gt-text-sm` at `--gt-leading-snug`, MIXED CASE.

   🔴 THE RESIDUAL BAND SURVIVES THE PORT, RE-MEASURED — and the first cut
   of this sentence said it "closes", from an arithmetic comparison of two
   title boxes measured with different insets; the rendered bisect refuted
   it, which is this file's own run-the-arm rule. V-18d's measurement (on
   the square grid): "PS Plus Essential — 1 Month" clipped at layout
   viewport ≤ 320 and was whole from 321 up, below the cw-375 acceptance
   floor. V-20 B4 moved the products to the 4/3/2 product grid (gap 20,
   so the 2-col floor's track is a few px narrower than the square
   grid's); the band RE-BISECTED on the new geometry at one-pixel steps:
   "PS Plus Essential — 1 Month" clips at layout viewport ≤ 328 (2-col
   track ≤ 134px) and every name is whole from 329 up — still below the
   cw-375 acceptance floor, same tracker disposition.

   Dropping uppercase here is not a breach of the tile's rule:
   uppercase is the POSTER WORDMARK's own exception (hover-skill §2), the
   card head everywhere else keeps mixed case, and a product's catalogue
   name is a name, not a wordmark. The SQUARE keeps the wordmark voice —
   its six names are ours to keep short — which is the two-registers
   ruling carried onto one shared anatomy: the poster wordmark shouts,
   the catalogue name reads.

   🔴 ACCEPTED by the round owner, 27 Aug 2026 — the mixed case STAYS. Do
   not "fix" this back to the tile's uppercase treatment: reverting would
   reopen BUG-482 at every width (the measurement above — the names cannot
   fit), and the design review judged the two title voices as one system
   with two registers. A future change here is a new decision for the
   owner, not a consistency cleanup.

   🔴 THE TWO-LINE RESERVATION IS BACK, BECAUSE THE BODY IS IN FLOW AGAIN —
   the same reversal that killed it, run in reverse. V-18d deleted it
   ("this body is absolute and bottom-anchored: a second line grows UPWARD,
   costs no card height"), and that argument was true of the overlay and is
   false of a flowed panel: here a second title line GROWS THE CARD, so
   without the slot a one-line card stands shorter than its two-line
   neighbour and the price rows stagger. The value is the fork's own
   arithmetic — two lines of `--gt-text-sm` at `--gt-leading-snug` — never
   the number 37.8, so retuning the type moves the slot with it. The words
   sit on the SURFACE: `--gt-ink`, no text-shadow (a shadow under
   ink-on-surface is smear, not separation), and the wordmark's lean never
   reaches here — the partial withholds `__title--lean` on the product
   branch, because the skew is the poster voice and this is the catalogue
   voice (the V-18d two-registers ruling, one step further).
   `white-space: normal` un-does the base nowrap (a -webkit-box full of
   nowrap text never wraps); `overflow: hidden` is inherited from the base
   rule. */
.got-gamecard--product .got-gamecard__title {
  white-space: normal;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2;
  line-clamp: 2;
  font-size: var(--gt-text-sm);
  line-height: var(--gt-leading-snug);
  text-transform: none;
  min-block-size: calc(2 * var(--gt-text-sm) * var(--gt-leading-snug));
  color: var(--gt-ink);
  text-shadow: none;
}
/* THE PRODUCT CARD'S META ROW: the tile's `__meta` was ALREADY "a two-item
   row with one item in it" — `space-between` reserving the second seat
   since V-06. The money run takes the first seat (`__count`, dir="ltr" +
   <bdi> — a flex ITEM is content-sized on the inline axis, so the LTR box
   has no width to be misaligned in; BUG-339's family, Range-re-measured on
   /ar in the round report) and the SOLD-OUT pill takes the reserved second
   seat — V-21 B1 made the pill the exception, not the norm: an on-sale
   product renders no pill element at all, so the row is a price alone on
   most cards. The pill's WORDS are the decided 14 Aug surface and never
   truncate — `flex: none` is the store note-row's rule, moved here with
   its element.

   🔴 ON THE SURFACE SINCE V-21 B2, AND THE PRICE GROWS UP TO SAY SO. The
   row reads `--gt-ink` at `--gt-text-sm` with the on-artwork ink and the
   caption shadow switched off — the price is the card's second fact, not
   an 11px caption — and since V-22 B2 the money run inside it takes the
   scale's money size at bold (its own rule below carries the argument;
   the ROW stays text-sm so the pill and any future second item keep the
   panel's size). The pill wears
   the ORDINARY theme-scoped `.got-pill--muted`: the V-18d on-artwork pill
   family (`--gt-tint-on-warning` and its three siblings, plus their four
   rules here) is DELETED, not kept — pinned dark-palette ink on a surface
   THE THEME PAINTS is the V-18d Critical in reverse, a fixed amber on a
   light card. StoreSectionsTest holds the absence.

   THE ROW RESERVES THE PILL'S HEIGHT — the soon badge's own move, one row
   over: the pill (padding-block 2×space-1 + one --gt-text-2xs line at
   --gt-leading-snug) is taller than the price's text line, so without the
   slot a pilled card's BOX is taller than a pill-less neighbour's. The
   value is the pill's arithmetic, never a number.

   🔴 AND WHERE THAT IS VISIBLE IS NARROWER THAN THIS COMMENT FIRST SAID —
   measured by the batch's own tester, not read. It claimed the stagger
   breaks the row on the category page. IT DOES NOT: there the card IS the
   grid item and the grid stretches (`align-items: normal`), so with this
   slot deleted and a pill injected, all four cards still measured 495.55.
   It is load-bearing on /design's <li>-WRAPPED pair, where the stretch
   reaches the <li> and not the card — 6.65px there, with a control.

   THE RULE STAYS, and the measurement is the reason rather than despite
   it: what currently hides the defect on the category page is an
   ANCESTOR'S `align-items`, and a reservation held by somebody else's
   layout property is a guarantee that leaves the day that layout changes.

   THE 1.37px BASELINE RESIDUAL THAT WAS STATED HERE IS DEAD — V-22 B2.
   It existed because pill and price shared the row's baseline; the pill
   sits `align-self: center` against the xl money line now (the rule above,
   with its measurement), and the pilled card's price starts at the same y
   as its pill-less neighbour's — re-measured on /design's pair, identical
   to the hundredth.

   🔴 A LONG MONEY RUN BESIDE THE PILL still HAS NO OVERFLOW ANSWER —
   stated as the open question it is, not designed here. Neither seat may
   shrink (the pill by this rule, the money run because a truncated price
   is a wrong price); the geometry under the question moved in V-21 B2
   (the row sits on the panel) and again in V-22 B2 (the run grew to xl),
   so the /design long-run specimen is the surface that re-measures it —
   the V-22 B2 report carries the current reading, and the xl run raised
   the width at which the specimen starts clipping. Today every live store
   product is flat-priced ("$11.50"-shaped, measured under the run's own
   tabular numerals at the narrowest band this round could drive, no
   overflow in either language); the TRACKER ROW owns the question. */
.got-gamecard__meta .got-pill {
  flex: none;
}
/* 🔴 THE PILL LEAVES THE BASELINE ON THE PRODUCT ROW — V-22 B2, and the
   price's own growth is what forced the question. With the money run at
   `--gt-text-xl` (the inversion below) the price line is TALLER than the
   pill, and on a `baseline` row the two heights no longer add up the same
   way on every card: measured on /design's <li>-wrapped pair — the one
   surface grid-stretch cannot equalise — the pilled card came out 502.58
   against its neighbour's 499.48, the V-21 B2 stagger reopened by this
   batch's own type change (the pill's box hung 3.09px below the price's
   line box, because baseline alignment adds the two boxes' different
   under-baseline depths).

   `align-self: center` seats the pill against the money line's middle
   instead: the row's height is the price line's own on every card, pilled
   or not (re-measured after this rule: both 499.48), and V-21 B2's stated
   residual — the pilled card's price sitting 1.37px lower on the baseline
   row — dies with the baseline coupling. The meta row's `min-block-size`
   reservation above is NOT deleted with it: it is the pill's own height and
   goes back to being load-bearing the day the pill is retuned taller than
   the price line. Scoped to `--product` because only the product row can
   carry a pill beside an xl money run; the square's meta keeps the base
   baseline behaviour untouched. */
.got-gamecard--product .got-gamecard__meta .got-pill {
  align-self: center;
}
.got-gamecard--product .got-gamecard__meta {
  min-block-size: calc(2 * var(--gt-space-1) + var(--gt-text-2xs) * var(--gt-leading-snug));
  font-size: var(--gt-text-sm);
  color: var(--gt-ink);
  text-shadow: none;
}
/* 🔴 THE PRICE IS THE FOOTER'S HEADLINE — V-22 B2, 29 Aug, Omar's direction.

   Until this batch the title and the price were BOTH 14px and the price was
   the LIGHTER of the two (600 against the title's 700) — a money amount
   subordinate to a boilerplate name, on a card whose page (the category
   grid) has already said the platform and whose label (V-22 B1) already says
   the region and the face value. The two facts a shopper is choosing between
   are the value and the price, so the price takes the type scale's own money
   size — `--gt-text-xl` is DOCUMENTED as "section headings, money totals",
   which is why no new size is invented — at bold. Nothing else in the footer
   may out-weigh it.

   The transition eases the ONE colour the hover rules below change. That is
   the capsule's own documented exception to "transform and opacity only":
   a colour easing on a control's clothing, not geometry on the card — and
   reduced motion collapses it through the duration tokens like every other
   colour ease in this system. */
.got-gamecard--product .got-gamecard__count {
  font-size: var(--gt-text-xl);
  font-weight: var(--gt-weight-bold);
  transition: color var(--gt-duration-base) var(--gt-ease-standard);
}
/* THE PRESENTATION LABEL'S OWN VOICE — the other half of the inversion. The
   V-22 B1 label (`<region> · <face value>`) rendered inside the title's rule
   and so wore the title's 14px/700 ink: identity dressed as a headline, ABOVE
   a price that was smaller and lighter than it. It is metadata, and it takes
   the metadata treatment: `--gt-text-xs` ("dense UI, secondary metadata"),
   medium, `--gt-ink-muted` — the muted role's own token, AA on every surface
   by the figures on its definition (re-sampled rendered on this panel for
   this batch: 6.51:1 dark / 6.37:1 light), so quiet never costs legibility.

   SCOPED TO `--product` like every rule in this section, though the label
   only renders there today: the span sits inside `__title`, whose base rule
   is (0,1,0), so this (0,2,0) wins wherever markup grows next.

   🔴 THE TITLE'S TWO-LINE RESERVATION IS DELIBERATELY UNTOUCHED. The grid is
   MIXED — gift cards and PS Plus subscriptions in one row — and the
   `min-block-size` on the product title is what puts a labelled card's price
   on the same line as a two-line neighbour's (BUG-482: subscription names
   wrap, never ellipsize). Shrinking the slot to the label's one short line
   would tidy the gift card and stagger every mixed row; the label simply
   sits at the top of the slot it inherits. */
.got-gamecard--product .got-gamecard__label {
  font-size: var(--gt-text-xs);
  font-weight: var(--gt-weight-medium);
  color: var(--gt-ink-muted);
  transition: color var(--gt-duration-base) var(--gt-ease-standard);
}
/* HOVER AND FOCUS-VISIBLE REACH THE PANEL — V-22 B2, and the gap it closes
   was measured before it was styled: every state rule this card inherits
   lives on the ARTWORK (lift, zoom+lit, scanlines, edge, sheen) or is DEAD
   here (`__go` — the product renders no capsule), so the panel — 18% of the
   card's face — answered hover with nothing and the card read as static
   below the fold line of its own artwork.

   The response is COLOUR ONLY, the restraint the section's contract demands:
   the price completes to the accent (the same family the edge ring lights
   in the same instant — one reward, two layers) and the label warms from
   muted to full ink. `--gt-accent` is theme-scoped and already serves as
   TEXT on themed surfaces (`.got-pill--active`, `.got-stat--active`);
   measured on this batch against the PIXEL-SAMPLED panel ground beside the
   price (scanline layer flattened by the compositor, both themes, 1536):
   10.50:1 dark, 5.73:1 light — both clear the 4.5 body floor, and 22px bold
   only needs 3.0. The unlabelled title changes nothing: it is
   already full ink, and there is no louder ink to hand it — its card's
   reward is the price's.

   Both selectors together, always — the ring has regressed four times, and
   focus-visible takes the same reward PLUS the system's ring, which the
   card-level rule already wears. `:not(--soon)` is the section's invariant
   stated, not a reachable case: no caller hands a product the soon state
   today, but the partial CAN emit the pair, and the guard is one token.

   NO `(hover:none)` ANSWER IS OWED, and the difference from the capsule is
   the reason: nothing here is revealed. The price and the label are fully
   present and AA at rest in every input mode; this is a reward on top of a
   complete rest state, so a phone loses a flourish, never information.
   Reduced motion keeps colour by the house rule — these are colour, so they
   survive it whole, through durations the tokens collapse. */
.got-gamecard--product:not(.got-gamecard--soon):hover .got-gamecard__count,
.got-gamecard--product:not(.got-gamecard--soon):focus-visible .got-gamecard__count {
  color: var(--gt-accent);
}
.got-gamecard--product:not(.got-gamecard--soon):hover .got-gamecard__label,
.got-gamecard--product:not(.got-gamecard--soon):focus-visible .got-gamecard__label {
  color: var(--gt-ink);
}


/* ------------------------------------------------------ THE STORE GRID ----
   Six squares under the store heading: FIXED COLUMN COUNTS — 6, then 3,
   then 2 — never auto-fill. V-18d deleted the 1140 cap and the 172 floor
   (`--gt-store-grid-max` / `--gt-store-tile-min`, tokens and comments both)
   with the auto-fill grid that read them.

   🔴 OMAR'S REVISED RULE, 27 Aug 2026, verbatim shape: THE GAME TILE STAYS
   DOMINANT BY AREA AND HEIGHT, NOT BY WIDTH — width was a proxy. Measured
   at 1920 on this grid: tile 247.33 × 370.98 = 91,755 px² against the
   square card's 254 × 254 = 64,516 px² — the tile is +42.2% by area and
   +46.0% by height, and the CARD IS WIDER THAN THE TILE THERE BY DESIGN
   (254.00 vs 247.33). Under the old width rule that would read as a
   violation; under the revised rule it is the point.

   THE BREAKPOINTS ARE DERIVED, NOT PICKED, and they read the OUTER viewport
   in em (a media query cannot see the container; these bands were checked
   against the games grid's own track steps, with and without a 15px bar):

     6 → 3 at outer ≤ 1067px (66.6875em). The games grid steps 3→4 tracks
     at content 1020 = outer 1068 (24px gutters), and a 3-col store card in
     the games 4-track band OUT-AREAS the tile by up to 27% — the revised
     rule broken by arithmetic — so six columns hold all the way down to
     the boundary where the games grid is itself 3-track. In the 6-col band
     the minimum sits at OUTER ≈1636, where the games grid reaches six
     tracks — not at the shelf cap (the first cut said "at the shelf cap"
     against its own parenthesis; the round's tester caught the sentence
     disagreeing with itself): area ≥ 1.42×, height ≥ 1.459× there, and
     1.46× at the 1920 checkpoints.

     3 → 2 at outer ≤ 600px (37.5em). Two columns are FORBIDDEN above outer
     807 (games 3-track band, content 760–1019: a 2-col card out-areas the
     tile ~1.6×) and three columns run out of card below ~600: at outer 601
     with a 15px bar the 3-col track was 174.00px, the last width the
     two-line product title and the capsule fit legibly — measured by the
     V-18d tester ON THE CATEGORY PAGE, which rendered this grid until
     V-20 B4 moved its products to `.got-product-grid`; the boundary's
     derivation is that measurement's and it stands for the shelf, whose
     own 3-col band brackets the same track widths. Inside 601–807 both
     counts satisfy dominance; three is kept because it is MORE dominant
     (≥3.3×) and holds the six categories to two rows. At and below 600
     the 2-col card ((C−12)/2) sits beside a games grid that is 2-track or
     1-track: ratio ≥ 1.45 at 600 and rising to ≥6 below content 500.

   BUG-488 IS CURED BY CONSTRUCTION: 6, 3 and 2 all divide six, so the
   homepage's six categories can never orphan a row at ANY width — the
   filed 996–1187 band renders 3+3 below the boundary and one row of six
   above it (rendered checks at 996/1024/1067/1068/1140/1187 in the round
   report; the shipped auto-fill grid showed 5+1 at 1024). SINCE V-20 B4
   THE HOMEPAGE SHELF IS THIS GRID'S ONLY PRODUCTION SURFACE — the
   category page's groups render `.got-product-grid` below, sized for the
   2:3 product card (/design keeps a specimen row of each).

   Scrollbar immunity is SAFE BY CONSTRUCTION AND UNMEASURED, said in
   that order: headless Chrome paints no classic bar (measured at eight
   window sizes — every reading had innerWidth == clientWidth), so no run
   in this round ever rendered a 15px bar. What holds without the
   measurement is the construction: the query reads the outer width, `1fr`
   tracks share whatever content the bar leaves, and 6, 3 and 2 all divide
   six — so a 15px shift shrinks every card a hair and can neither change
   the column count nor orphan a row. The knife-edge boundary tables the
   auto-fill grid needed are gone with it. */
.got-store-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--gt-space-4);
  grid-template-columns: repeat(6, 1fr);
}
@media (max-width: 66.6875em) {
  .got-store-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}
@media (max-width: 37.5em) {
  .got-store-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}


/* ---------------------------------------------------- THE PRODUCT GRID ----
   V-20 B4: the product card left the square (GAP-160 — the 1:1 frame kept
   66.7% of every 1024×1536 cover, and what it threw away was the flag and
   the value, the only two things on the picture) and became the game
   tile's own 2:3, BIGGER — Omar has said twice the product was too small.
   The category page's groups render THIS grid; `.got-store-grid` above is
   the homepage shelf's (and /design's specimen row's) alone.

   FIXED COUNTS — 4, then 3, then 2 — never auto-fill, for the store's own
   reasons: the count is the decision, and a scrollbar can shave every card
   a hair but never change the count or orphan a row. FOUR at full width is
   the count that makes the product card the game tile's own size: the
   page's `.container` holds 1140 of content, so a track is
   (1140 − 3×20)/4 = 270 → a 270×405 card, MEASURED beside the games
   catalogue's own tile at 272×408 (5 tracks, viewport 1536) and 247×371
   (6 tracks, 1920) — the V-20 B4 report's rendered readings. Six portrait
   tracks would be a 173px card, NARROWER than the 180px square this batch
   exists to outgrow; five is 212, still under the tile; four is the first
   count that reaches it. (Sixteen gift cards fill four rows of four
   exactly today — an observation, not a mechanism; the count is content.)

   THE BREAKPOINTS ARE DERIVED FROM THE TILE'S OWN FLOOR (--gt-tile-min,
   240px), read on the OUTER viewport in em exactly as the store grid's
   are (a media query cannot see the container): n tracks hold while a
   track ≥ the floor, i.e. content ≥ 240n + 20(n−1), and `.container`'s
   content is outer − 2×clamp(20px, 3vw, 40px):

     4 → 3 at outer ≤ 1085px (67.8125em). Four tracks want 1020 of
     content = outer 1085.1 at the 3vw gutter; at 1086 the track is
     240.2px, at 1085 three tracks give 326.
     3 → 2 at outer ≤ 808px (50.5em). Three want 760 = outer 808.5 —
     within half a pixel of the games grid's own 2↔3 boundary (808, by
     its fixed-gutter arithmetic), so the two catalogues step together
     there. TWO is the floor count, not one: the store's phone answer is
     2-up (the square grid's own, kept across the frame change), and at
     390 the track is 165×247.5 with the two-line clamp holding the
     names — the residual clip band is re-bisected on the title fork's
     comment above.

   A 15px bar in the 4-col band's first ~20px shaves the track to ~236 —
   under the floor by 4px, visible only as a hair-narrower card; the
   count cannot move, which is the same trade the store grid documents. */
.got-product-grid {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  gap: var(--gt-tile-gap);
  grid-template-columns: repeat(4, 1fr);
}
@media (max-width: 67.8125em) {
  .got-product-grid {
    grid-template-columns: repeat(3, 1fr);
  }
}
@media (max-width: 50.5em) {
  .got-product-grid {
    grid-template-columns: repeat(2, 1fr);
  }
}


/* ------------------------------------------------- THE MEDIA CARD (hover-skill)

   A catalogue tile: a 2:3 poster with the game's name ON it. The pattern is
   `~/.claude/skills/hover-skill/SKILL.md` (mirrored in the docs folder's
   04_Brand); this is its only implementation and the two are kept the same
   bytes on purpose — the skill's §6 IS this block, and `GameCardTest` compares
   them. (Since V-18d the store card wears these classes too, and since
   V-25 B2 BOTH store frames are flex columns framing their artwork in a
   media box in flow — 2:3 for the product, 1:1 for the category square —
   with the words BELOW it on the card surface: no veil, no overlay, and
   the capsule only on the square. All of it lives in THE STORE CARD
   section above, OUTSIDE this compared block, so the tile's contract
   stays one copy here.)

   🔴 IT MOVED BACK ON 22 AUG (V-06), AND THE ROUND BEFORE IT MOVED THE OTHER
   WAY. This is the same component's third shape in a week, so both earlier
   arguments are kept rather than overwritten — a comment that only records the
   winner reads, to whoever comes next, as though nobody had thought about it.

     · BEFORE V-02: a 3:4 tile with the name, the description and the call to
       action printed over the picture behind an ink veil.
     · V-02: a 2:3 poster with the words on the card UNDERNEATH it. The
       arguments were good ones — the whole poster is visible, the words sit on
       a colour whose contrast can be stated, and nothing has to be revealed on
       hover.
     · V-06: the words go back onto the artwork. WHAT DECIDED IT WAS THE FOLD,
       not taste. A body under the frame is ~80px of card per tile that carries
       no picture, and on a 1366x735 laptop that was the difference between the
       first row of the catalogue finishing above the fold and being cut in
       half. The tile is the artwork now; the words live in it.

   The three costs V-02 named are real and are paid deliberately here:

     · the foot of the poster is under a scrim again — the veil is back to
       three stops reaching .96 at the bottom edge;
     · the words need a text-shadow, and their contrast depends on the pixels
       behind them, so the figure this block can state is the WORST CASE (a
       pure white frame): title 14.22:1 at the foot, meta 4.71:1 at the veil's
       74% stop, both arithmetic on a flattened composite, both to be
       pixel-sampled by the verifier rather than believed from here;
     · the call to action is REVEALED on hover again — which is why the
       `@media (hover: none)` block below is back, and why its deletion note
       from V-05 is rewritten rather than left contradicting the code.

   ONE `<a>`, SIX LAYERS, and there is no `__frame` any more:

     0  __media   the artwork, object-fit:cover — or, with no artwork, the
                  system's gradient rested and lit EXACTLY like a photo, with
                  the short name as a type mark (18 Aug: one material, one
                  lighting model; a live game without art must not read as a
                  placeholder)
     1  __veil    the ink gradient the words are read against
     2  __grid    scanline texture, mix-blend-mode:overlay
     3  __sheen   one-shot light sweep on hover
     4  __edge    gradient border, drawn with a mask so it can fade
     5  __body    title, meta and the capsule — absolutely positioned at the
                  foot of the artwork

   🔴 `__frame` IS DELETED RATHER THAN LEFT AS A WRAPPER. It existed to hold
   the 2:3 while a body sat UNDER it, which is precisely the arrangement that
   went away: the card IS the picture, so the card carries the ratio. A wrapper
   with nothing to separate is a box every future reader has to work out the
   purpose of. `__foot` went with it for the same reason — it was one row that
   made a live tile's words line up with a soon tile's, and the overlay lines
   them up by construction because every tile is the same shape and every body
   is anchored to the same edge.

   The whole card is a single link. Not a div with a click handler, not a card
   holding three links: one tab stop, Enter works, a screen reader announces it
   once. The layers are empty spans and are aria-hidden in the markup, and the
   capsule is a span — a button inside a link is a control inside a control.

   🔴 THE STATE TABLE, and every line of it is to be measured in both
   languages:

     idle           artwork at --gt-media-rest — the picture as shot, full
                    saturation — edge off, capsule at its resting weight
                    (--gt-media-cta-rest, V-13: present, quiet, unmoving)
     hover          lift 6px, artwork to 1.06 and --gt-media-lit, edge on,
                    scanlines up, sheen sweeps once, capsule completes to
                    full and its arrow nudges along the reading direction
     focus-visible  IDENTICAL to hover, plus the system's ring — the same
                    three declarations every control wears, band included.
                    The ring is never traded for the hover reward; it has
                    regressed four times in this project
     active         scale(--gt-press-scale) for one instant, and the artwork
                    eases back to 1.0325 — the midpoint of rest and hover, so
                    a press can never zoom FURTHER IN than a hover
     touch          the capsule is simply THERE, and the artwork settles to
                    --gt-media-touch so it still reads over the picture. See
                    the (hover:none) block, which is back and says why
     reduced motion every MOVEMENT removed, every COLOUR change kept

   🔴 THE ELEVATION IS LOST ON `:focus-visible`, AND THIS COMMENT USED TO
   CLAIM IT WAS NOT. It said the card "sits at --gt-level-elevated and STAYS
   there". Measured: at rest and on hover the shadow is unchanged and the
   border holds at rgb(37,44,64) — both correct — but on focus-visible the
   computed box-shadow is `rgb(13,15,23) 0 0 0 2px` and the elevation is gone,
   because `--gt-ring-shadow` is a WHOLE-VALUE box-shadow that overwrites the
   base rather than adding to it.

   IT IS LEFT AS IT IS, AND THAT IS A CHOICE WITH A REASON. Composing the ring
   with the elevation HERE would take one line and would be a PRIVATE ANSWER
   to a question that is almost certainly systemic: `--gt-ring-shadow` is the
   system's, worn verbatim by every control in this file, and any of them that
   also carries a level loses it the same way. This block's own rule is that
   the ring is three declarations, never restated. So the behaviour stands,
   the comment now says what actually happens, and the question — should the
   ring's band compose with a control's elevation, everywhere — is raised with
   whoever owns the ring rather than answered on one card. It is cosmetic: the
   band is painted exactly where the shadow was.

   🔴 TRANSFORM AND OPACITY ONLY — AND THE REFERENCE DISAGREES, SO THIS IS A
   DECISION AND NOT AN OVERSIGHT. The V-06 study transitions `box-shadow` and
   `border-color` alongside the lift, and its shadow carries a -18px spread.
   Omar ruled on that exact disagreement on 17 Aug (resolution #8) and again
   on 22 Aug when V-06's reference raised it a second time: "a standing
   decision outranks a mock built yesterday". So NEITHER the throw nor the
   transparent border ships — not eased and not instant — and the resolution
   is quoted verbatim at the hover rule below rather than summarised. Nothing
   here transitions width, height, an inset, a border colour or a box-shadow.
   The card sits at `--gt-level-elevated` and stays there while HOVERED
   (measured; on focus-visible the ring's band replaces it — see above), and
   the lift is carried by `transform`.

   The one place a colour is eased is the capsule's fill, border and words,
   which is what `--gt-transition-control` does for every control in this
   system; that is clothing on a label, not geometry on a card, and it is
   argued where it is written.

   Durations are the system's: press -> instant, filter and edge -> base, lift
   -> base, artwork zoom -> slow, sweep -> sweep. The system's ease is
   `--gt-ease-standard`; the reference's own `cubic-bezier(.22,.61,.36,1)` is
   NOT adopted, because a second ease for one component is a second answer to
   a question the system already answers everywhere else. Reduced motion
   collapses the durations through the tokens, and the two things a duration
   cannot reach — the transforms and the sweep — are stopped where they are
   defined, below, the way the loading ring is. */
.got-gamecard {
  /* Where the artwork's subject sits: the DEFAULT here, and the partial writes
     the file's own value from GameArtwork::FOCUS onto the <img> as the same
     property, which wins. */
  --gt-media-focus: center 30%;
  position: relative;
  /* THE CARD IS THE PICTURE, so the card carries the ratio. 2:3 is the shape
     the artwork is cut to — as of V-19a (27 Aug) ALL TEN bundled covers are
     1024x1536, measured, every one exactly 2:3, so a poster is shown whole
     rather than cropped a second time by the tile. The mixed set this comment
     used to name (736x1104 beside 1024x1536, and five files that were neither)
     is gone; one shape now, which is what makes the crop a no-op rather than a
     coincidence. It is written here rather than as a token because it is a
     fact about the ASSET, not a position in a scale, and the day the assets
     change shape this is the one line that moves.

     `block` and not `flex`: there is nothing to lay out any more. The artwork
     fills the box and the body is absolute inside it, so nothing on this card
     can grow it, and every tile in a row is the same size whatever its title
     is. What still needs reserving is the body's INTERNAL rhythm — a tile
     with no count and a badge instead of a capsule must put its title at the
     same height as its neighbours — and that is two `min-block-size`
     declarations, on `__meta` and on the soon tile's badge, each argued where
     it is written. The CARD's height reserves itself. */
  display: block;
  aspect-ratio: 2 / 3;
  overflow: hidden;
  isolation: isolate;
  border-radius: var(--gt-radius-lg);
  background: var(--gt-surface-sunken);
  border: var(--gt-border-hair) solid var(--gt-line);
  box-shadow: var(--gt-level-elevated);
  color: var(--gt-ink);
  text-decoration: none;
  transition: transform var(--gt-duration-base) var(--gt-ease-standard);
  will-change: transform;

  /* The sweep travels from the reading edge. `translateX()` is physical —
     there is no logical transform — so the distance is a custom property
     flipped by the [dir] rule below, exactly as the toast does. This, the
     arrow's nudge and the arrow glyph are the three [dir] exceptions on this
     component, and all three are properties with no logical form. */
  --gt-sweep-to: 420%;
}
[dir="rtl"] .got-gamecard { --gt-sweep-to: -420%; }

/* 0 · the artwork. Absolutely positioned inside an aspect-ratio box, so the
   card's size never depends on the image: no layout shift while it loads,
   whatever the file's dimensions. `object-position` decides whether a
   portrait frames the face or the chest and CANNOT be reasoned out — every
   value in GameArtwork::FOCUS was chosen by rendering and looking, and the
   list is empty today because no bundled file is taller than the frame.
   (That sentence was FALSE from V-13 Batch 6 until V-19a: fortnite.jpg was
   1080x1920 and carried an entry the whole time this comment denied it. It is
   true again because V-19a made every cover 2:3 — and it is asserted by
   GameCardTest, which is the only reason it can be trusted here. That
   assertion reads THIS rule's ratio only; .got-gamecard--square renders the
   same image at 1:1 and is outside it. Since V-20 B4 the square is the
   category tile alone, whose uploads are square art, and the PRODUCT card
   shows its 2:3 covers uncropped — in this frame then, and since V-21 B2
   in its own 2:3 media box, the same number decided in THE STORE CARD
   section — which is what retired GAP-160's live harm.)

   The END PADDING is the type mark's, and only the type mark's: the image
   below is absolutely positioned against this box and ignores it entirely.
   It lifts a lettered tile's mark clear of the band the body occupies, which
   is the same job the mark's old upper-third placement did before V-02 put
   the words under the picture. */
.got-gamecard__media {
  position: absolute;
  inset: 0;
  z-index: 0;
  overflow: hidden;
  display: grid;
  place-items: center;
  padding-block-end: var(--gt-space-11);
}
/* The image is positioned against the card, not laid out as its grid item.
   As a grid item its intrinsic ratio grew the auto track — 380px tall in a
   285px frame, centred, cropped at 50% instead of at the object-position,
   face gone (measured, 17 Aug). Absolute, 100% IS the box, whatever wraps
   the <img> — a <picture> or nothing. */
.got-gamecard__img {
  position: absolute;
  inset: 0;
  inline-size: 100%;
  block-size: 100%;
  object-fit: cover;
  object-position: var(--gt-media-focus);
  display: block;
  filter: var(--gt-media-rest);
  transform: scale(var(--gt-media-zoom-rest));
  transition:
    transform var(--gt-duration-slow) var(--gt-ease-standard),
    filter    var(--gt-duration-base) var(--gt-ease-standard);
}
/* No artwork at all: the game's short name as a type mark, in the picture
   area above the words.

   🔴 IT IS THE LIGHT ON-ARTWORK INK AGAIN — 23 AUG — AND THAT IS F-1'S OWN
   RULE OBEYED, NOT F-1 UNDONE. READ BOTH HALVES OF THIS BLOCK TOGETHER.

   F-1's rule (22 Aug, found by the Card verifier) is that an on-artwork colour
   does not follow the page: it follows THE GROUND IT IS MEASURED ON. Batch 6
   had argued that the veil makes every card a dark context, which is true of
   the TITLE and the META at ~90% of the frame where the veil is .74 to .96, and
   was FALSE of this element at 43%, where the veil alpha is about .16 over
   `--gt-media-placeholder`. Measured ground under the mark, mark hidden:
   rgb(33,142,217) — a BRIGHT context in both themes — so F-1 gave it the dark
   on-artwork ink, `--gt-on-accent-ink`, and took it from 2.05 to 5.43.

   🔴 OMAR'S ROW-RHYTHM RULE (23 Aug) MOVED THE GROUND, AND THE TWO
   REQUIREMENTS PROVABLY DO NOT OVERLAP WITH THE DARK INK. The fallback's mean
   luminance now has to sit inside the range of the real tiles — the derivation
   is on `--gt-media-placeholder` — and the ground under this mark falls with
   it. The two were measured against each other on the rendered page, one alpha
   at a time, with an artwork tile as a control that did not move one count:

     scrim alpha       0     .10    .20    .30    .35    .40    .50    .60
     tile mean      .2446  .1994  .1608  .1257  .1113  .0971  .0716  .0519
     mark, dark ink  5.60   4.70   3.98   3.29   3.00   2.71   2.22   1.84

   The dark ink reaches the 3.0 large-text floor at about .35, where the tile
   still means .111 — 1.28x the BRIGHTEST real tile (.08681), outside the range
   at its very top. NO ALPHA SATISFIES BOTH, and it is not a near miss to be
   tuned around: the gap is a quarter of the whole range wide.

   Shaping the gradient instead of flattening it does not rescue it either, and
   this next sentence is ARITHMETIC ON THE MEASUREMENTS ABOVE rather than a
   reading of its own, which is why it is labelled: at .35 the top 55% of the
   frame means .169 and the mark's own ground .113, so even with the bottom 45%
   driven to pure black the whole-box mean is .55 x .169 = .093 — still above
   .08681. A bright band behind the mark over a black foot does not get there.
   So the ground is dark now, and F-1's rule points at the light ink.

   `--gt-ink` is STILL the wrong answer, for F-1's original reason: it follows
   the theme, so it would read correctly on one page and by accident on the
   other. `--gt-on-ink` reads `brand.on-ink` and is absent from the light block,
   which is why the two themes below are identical rather than merely close.

   MEASURED ON THE RENDERED GLYPH against the rendered background at the same
   location — top decile of a with/without-text diff so antialiasing is
   excluded, control asserts the mask is non-empty — AA large text floor 3.0:1.
   Both widths and both themes, because a comment quoting one number reads as
   proof the rule was checked. Re-run end to end on this tree on 23 Aug, so the
   middle column is what shipped yesterday and the right column is what ships:

                        F-1's dark ink on the   this light ink on the
                        BRIGHT placeholder      DARKENED placeholder
     live type tile
       dark  @1536 / @390    5.60 / 5.11            6.41 / 6.11
       light @1536 / @390    5.60 / 5.11            6.41 / 6.11
     coming-soon type tile — /design's specimen, the only surface that renders
     the combination at all
       dark  @1536           4.32                   5.10
       light @1536           4.32                   5.10

   Every reading rises: a light ink on a darkened ground has further to travel
   than a dark one had. The two themes read identically BECAUSE the colour does
   not follow the theme, which is the mechanism being asserted rather than a
   coincidence. (F-1's table gave 5.43 and 4.15 where this run gives 5.60 and
   4.32 on the same shipped bytes — the same instrument at a slightly different
   mask cut. Both columns above come from THIS run, so they are comparable to
   each other; neither is comparable to a figure measured on another day.)

   THE @390 COLUMN NEEDED THE VEIL FIX AS WELL AS F-1'S INK — history, and
   the figures in it are F-1's, not this round's. With the phone's
   own veil override still in place this token measured 2.86 there — a real
   improvement on 1.80 and still under the floor — because that override made
   the ground under the mark luminance .1074 against .2468 at 1536, dark enough
   to flip which polarity wins. Deleting it (see the token block) is what takes
   @390 to 5.11. Two changes, one defect; neither is sufficient alone.

   AND THE WORDS WERE RE-MEASURED ON THE STANDARD VEIL IN THE SAME RUN, because
   a fix that clears the mark by thinning the veil the title is read against is
   not a fix. Before → after, same instrument: title @390 15.14 → 15.00, title
   @1536 14.67 → 14.67, meta @390 9.16 → 9.15, meta @1536 9.16 → 9.16. The
   phone veil was buying the title 0.14 of contrast and costing the mark 3.31.

   The text-shadow stays `--gt-media-text-shadow` — the ink halo — and with the
   glyph light again it is the OPPOSITE polarity to it once more, so it is back
   to separating rather than only softening. It does not enter the figures above
   either way: the halo is part of the text layer, so hiding the text hides it
   too. */
.got-gamecard__initials {
  font-size: var(--gt-text-3xl);
  font-weight: var(--gt-weight-heavy);
  color: var(--gt-on-ink);
  text-shadow: var(--gt-media-text-shadow);
}

/* 1 · the veil, 2 · the scanlines, 3 · the sheen, 4 · the edge. All
   pointer-events:none so the link underneath takes every click. */
.got-gamecard__veil {
  position: absolute;
  inset: 0;
  z-index: 1;
  pointer-events: none;
  background: var(--gt-media-veil);
}
.got-gamecard__grid {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  background-image: var(--gt-media-scanline);
  mix-blend-mode: overlay;
  /* No transition: `background-image` is not an interpolable property, and
     the reference gets its .14 -> .22 by easing the LAYER'S opacity — which
     this component may not do, because SiteTokensTest forbids a system class
     from dimming itself with `opacity` (BUG-241: opacity fades text and fill
     together, so a measured contrast stops being the figure on screen). The
     alpha is folded into the gradient stop instead and the change is
     instant. It is a texture at 1.4% white; nobody can see it arrive. */
}
.got-gamecard__sheen {
  position: absolute;
  inset-block: -60%;
  inset-inline-start: -70%;
  inline-size: 42%;
  z-index: 3;
  pointer-events: none;
  opacity: 0;
  transform: rotate(14deg) translateX(0);
  background: var(--gt-media-sheen);
}
/* The edge rings the CARD, because the card is the picture now — there is no
   longer a body underneath for a lit line to sit on. */
.got-gamecard__edge {
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  padding: var(--gt-hairline);
  border-radius: var(--gt-radius-lg);
  background: var(--gt-gradient-brand);
  -webkit-mask: var(--gt-mask-ring);
          mask: var(--gt-mask-ring);
  -webkit-mask-composite: xor;
          mask-composite: exclude;
  opacity: 0;
  transition: opacity var(--gt-duration-base) var(--gt-ease-standard);
}

/* 5 · the body. ON the artwork, anchored to its foot.

   `inset-inline` and `inset-block-end`, never `left`/`bottom`: it is a
   full-width block in a box that is RTL on /ar, and CLAUDE.md has paid twice
   for a `dir="ltr"` block sitting at the wrong edge. Every child is aligned
   by the flex column, so the only thing this rule decides is which edge of
   the picture the words sit on — the bottom one, in both directions. */
.got-gamecard__body {
  position: absolute;
  inset-inline: 0;
  inset-block-end: 0;
  z-index: 5;
  display: flex;
  flex-direction: column;
  gap: var(--gt-space-1);
  padding: var(--gt-space-4);
}
/* 🔴 THE TITLE IS UPPERCASE, AND THAT IS A ONE-LANGUAGE DECORATION — SAID
   OUT LOUD RATHER THAN LEFT TO BE FOUND.

   The system reserves uppercase for the 11px micro-label precisely because
   Arabic has no case, so an uppercase title is a treatment that exists on the
   English page and is a no-op on the Arabic one. That is checked, not
   assumed: `text-transform: uppercase` cannot change an Arabic glyph, so /ar
   loses nothing and breaks nothing — it simply does not receive the gesture.
   V-06 takes it anyway, because on a poster the name is a wordmark rather
   than a heading, and the asymmetry is a cost the round accepted with its
   eyes open. If Omar would rather the two pages match, deleting this one
   declaration is the whole change.

   NO TRACKING. The reference writes `letter-spacing: .01em`. It is not here:
   CLAUDE.md's standing decision (20 Aug) is no tracking before launch, and
   the measurement behind it is that Chrome applies letter-spacing only at
   non-joining boundaries, so one value moves Latin ~40% and Arabic ~4% and
   no single number serves both. At .01em the Latin gain would be 0.16px at
   this size — invisible — which makes it a rule that buys nothing and puts a
   language-specific value in the system.

   THE LEAN IS A CLASS, not this rule: `__title--lean`, set by the partial
   when the words are Latin. And it carries NO `transform-origin`. The
   reference writes `transform-origin: left`, which is physical and has no
   logical keyword — `transform-origin` takes no `inline-start`. The default
   centre origin is the one value that is correct in both directions with no
   [dir] rule at all, and it also keeps a skewed title inside its own clipped
   box, which a left origin does not. */
.got-gamecard__title {
  display: block;
  margin: 0;
  font-size: var(--gt-text-tile);
  font-weight: var(--gt-weight-bold);
  line-height: var(--gt-leading-tile);
  text-transform: uppercase;
  color: var(--gt-on-ink);
  text-shadow: var(--gt-media-caption-shadow);
  /* One line. A wrapped title on an overlay pushes the capsule down over the
     picture, and a two-line reservation would put a permanent empty line
     under every short name. */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* The meta row. Secondary BY WEIGHT AND COLOUR, never by being hidden: it is
   the only place the tile says how much is under it.

   🔴 IT USED TO BE THE GAME'S DESCRIPTION AND THE DESCRIPTION IS GONE. Two
   clamped lines of prose do not fit on an overlay, and a clamp over artwork
   would have needed the veil to reach halfway up the poster. What is left is
   the count — the one fact a shopper is choosing on. The reference also shows
   a `from $X` beside it; there is no minimum price in the shelf query and
   adding one is a controller change, which this round's scope forbids, so the
   row is built as a two-item row with one item in it and the second half is
   reported as deferred rather than invented.

   ONE LINE IS RESERVED so a coming-soon tile — which has no count — keeps its
   badge on the same line as its neighbours' capsules. `space-between` is what
   the second item will land in the day there is one. */
.got-gamecard__meta {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--gt-space-3);
  min-block-size: calc(var(--gt-text-3xs) * var(--gt-leading-snug));
  font-size: var(--gt-text-3xs);
  line-height: var(--gt-leading-snug);
  color: var(--gt-media-meta);
  font-variant-numeric: tabular-nums;
  text-shadow: var(--gt-media-caption-shadow);
}
/* Measured at a 214px tile before V-02: "7 services" wrapped to two lines
   beside the pill. It has a row of its own now and still may not break. */
.got-gamecard__count {
  white-space: nowrap;
}
/* 🔴 THE CAPSULE — present at rest, completed on hover and on focus
   (V-13 Batch 5, Omar's decision, 24 Aug).

   The go word is a label, not a control: the whole card is the control, so it
   is a span, it takes no focus and it needs no 44px of its own.

   IT HAS BEEN A FILLED PILL, THEN ACCENT TEXT, A HIDDEN CAPSULE, AND NOW A
   RESTING ONE — and the argument each time was about how many of them are on
   screen at once. A filled pill was right while the CTA was revealed on hover
   (one at a time); V-02 made it always-visible and a row of saturated cyan
   pills out-weighed the artwork, so it became text; V-06 hid it on the
   picture. V-13 brings it back AT REST at --gt-media-cta-rest — Omar chose
   the hover lab's "touch" option for every pointer — and what keeps the old
   row-of-pills problem away is the WEIGHT: a .72 bordered tint reads as an
   affordance, not a row of buttons, and hover still owns the completed
   state. Hover REWARDS rather than reveals.

   ITS SPACE WAS ALWAYS RESERVED AND NOW ITS BODY IS TOO: nothing on this
   card moves when the pointer arrives and nothing reflows underneath it.
   What changes on hover is opacity and colour only — the rise died with the
   reveal (token deleted, not orphaned) — and on a touch screen the
   (hover:none) block hands the COMPLETED state, because a phone has no
   hover to complete it with.

   `align-self` and not a margin: the capsule is content-width at the reading
   edge of the body, in both directions, with no [dir] rule. The reference
   gates all this behind `.frame[data-cta="capsule"]`, one of four prototype
   treatments; production ships the capsule unconditionally, so there is no
   attribute selector here and the bar, underline and glass variants are not
   ported at all. */
.got-gamecard__go {
  align-self: flex-start;
  display: inline-flex;
  align-items: center;
  gap: var(--gt-space-2);
  margin-block-start: var(--gt-space-2);
  padding-block: var(--gt-space-2);
  padding-inline: var(--gt-space-4);
  border: var(--gt-border-hair) solid var(--gt-media-cta-line);
  border-radius: var(--gt-radius-pill);
  background: var(--gt-media-cta-fill);
  color: var(--gt-on-accent);
  font-size: var(--gt-text-xs);
  font-weight: var(--gt-weight-semibold);
  line-height: 1;
  white-space: nowrap;
  /* V-13 Batch 5: visible at rest, quieter than hovered — no translate, no
     sweep, just presence. The rise and its `transform` transition went with
     the reveal; what remains to ease is the opacity step to 1 and the
     colour inversion, both of which are hover's REWARD. */
  opacity: var(--gt-media-cta-rest);
  /* The three colours ease as well as the opacity, and that is not a breach
     of "transform and opacity only": that rule is about the CARD's geometry
     — a shadow spread, a border, an inset — and this system already eases
     every control's colours through `--gt-transition-control`. The capsule
     is a control's clothing. */
  transition:
    opacity          var(--gt-duration-base) var(--gt-ease-standard),
    background-color var(--gt-duration-fast) var(--gt-ease-standard),
    border-color     var(--gt-duration-fast) var(--gt-ease-standard),
    color            var(--gt-duration-fast) var(--gt-ease-standard);
}
/* THERE IS DELIBERATELY NO `.got-gamecard--soon .got-gamecard__go` BELT
   HERE, and the first cut of this batch had one. A coming-soon tile never
   renders the capsule at all — media-tile.blade.php's @if renders the badge
   in its slot — so a CSS rule hiding it is unreachable by construction, and
   DesignPageTest's reachability instrument refused it on its first run
   (states that exist in CSS and cannot exist on the page are how unchecked
   states accumulate; that test carries no exemption list on purpose). The
   guard is the MARKUP brace, and CardRestStateTest pins it RENDERED, in
   both languages, with a falsification arm that mutates the blade. */
/* The arrow, drawn by CSS with the direction carried by a [dir] rule (as
   .arrow::after does in site.css — BUG-119): U+2192 is not Bidi_Mirrored,
   so a glyph typed into the markup would point back INTO the card on the
   Arabic page.

   Its hover nudge is a `translateX`, which is physical, so the distance is a
   custom property the [dir] rule flips — the same mechanism as the sweep's,
   and it is set on the capsule rather than the pseudo-element because custom
   properties inherit and one rule is easier to keep true than two. */
.got-gamecard__go::after {
  content: "\2192";
  display: inline-block;
  transition: transform var(--gt-duration-base) var(--gt-ease-standard);
}
[dir="rtl"] .got-gamecard__go::after {
  content: "\2190";
}
[dir="rtl"] .got-gamecard__go {
  --gt-media-arrow-nudge: var(--gt-media-arrow-nudge-rtl);
}

/* -------- THE TYPE TILE — a tile with no artwork (18 Aug) ----------------------
   Six of the seven home tiles had no photo and painted a flat sunken surface
   with muted initials, so a live game read exactly like a store-card bucket.
   `--type` (the partial adds it when it renders __initials) paints the
   system's gradient in the picture box and puts the SAME rest and lit filters
   on it that a photo gets on its image — one material, one lighting model;
   the difference between a photo tile and a type tile is only what is in the
   box. Guarded with :not(--soon) so the coming-soon tile keeps its own dim (a
   soon tile with no artwork wears BOTH classes).

   🔴 IT IS ALSO THE FALLBACK'S WHOLE CONTRACT. A game with no artwork holds
   the SAME 2:3 card, the same veil, the same body and the same states as one
   with a photograph, so the day a cover is uploaded nothing structural
   changes — it is a data change and a deploy is not involved. And it reads as
   "this game is here", never as "this image failed to load": a broken image
   is a grey box with a torn-page icon, and this is the brand's own gradient
   with the game's mark on it. */
.got-gamecard--type .got-gamecard__media {
  background: var(--gt-media-placeholder);
  transition: filter var(--gt-duration-base) var(--gt-ease-standard);
}
.got-gamecard--type:not(.got-gamecard--soon) .got-gamecard__media {
  filter: var(--gt-media-rest);
}
/* The same on-artwork ink the base rule takes, and the declaration is kept
   rather than deleted so the type tile states its own colour beside the
   gradient it paints — they are one decision and they are read together, which
   is exactly why both had to move on 23 Aug when the gradient's brightness
   became a measured rule. */
.got-gamecard--type .got-gamecard__initials {
  color: var(--gt-on-ink);
}
/* The lean — the wordmark's skewX — is a CLASS the partial sets when the
   words are Latin (Locale::directionOfText): no skewX on Arabic script,
   Latin keeps it. It reaches the title and the type mark, here and on the
   featured panel, which is why it keeps its name. */
.got-gamecard__title--lean,
.got-gamecard__initials--lean {
  transform: skewX(-5deg);
}

/* -------- THE NON-INTERACTIVE VARIANT — coming soon (18 Aug) ------------------
   A game that is on its way: visible, not sellable. The tile is a <div> (the
   partial decides), so it takes no focus; this block makes sure it takes no
   STATE either — every hover/focus/active rule below is written
   `.got-gamecard:not(.got-gamecard--soon)…`, the invariant rather than a list
   of undone properties, and ComingSoonGameTest asserts no unguarded state
   selector exists. Dimmed, the system's gradient where there is no artwork, a
   micro-label badge where the capsule would be, no sheen, no edge, no
   capsule. It is the documented exception to "the whole card is one link"
   (hover-skill §3a).

   🔴 IT STAYS A <div> AND KEEPS ITS BADGE, AGAINST THE REFERENCE. The V-06
   study renders the coming-soon card as `<a href="#">` with a "Notify me"
   capsule. There is no notify route in this product and building one is a
   controller change this round's scope forbids, so that capsule would be a
   Tab stop leading to a 404 wearing a promise — which is the exact defect the
   non-interactive variant was created to prevent on 18 Aug. Production keeps
   the <div>, keeps the badge, and offers no call to action at all. */
.got-gamecard--soon {
  cursor: default;
}
/* The dim sits on the picture box, so artwork and the placeholder gradient
   are dimmed alike by one rule — and it never lifts, because no state can
   reach this tile. The image's own REST FILTER is switched OFF underneath it,
   or a cover would be composed under the soon dim twice — the shape the
   verifier's arithmetic caught on 18 Aug. (0,2,0) beats the (0,1,0) img rules
   whatever the media query, which is what keeps the touch block below off it
   as well. */
.got-gamecard--soon .got-gamecard__media {
  background: var(--gt-media-placeholder);
  filter: var(--gt-media-dim-soon);
}
.got-gamecard--soon .got-gamecard__img {
  filter: none;
}
/* A coming-soon tile with no artwork paints the same gradient under a
   brightness(.80) and the orange wash, so it moves with it: since 23 Aug that
   is a DARK ground under a further dim rather than a dimmed bright one, and it
   takes the same light on-artwork ink for the same reason. Measured on
   /design's specimen — the only place in the product that renders this
   combination — with the same instrument: 5.10 at 1536 in both themes, against
   4.32 on the bright gradient. Same polarity as the tile beside it, same token,
   one decision. */
.got-gamecard--soon .got-gamecard__initials {
  color: var(--gt-on-ink);
}
/* THE ORANGE (Omar, 19 Aug): the soon tile's wash is the soon colour, not
   the accent — the tint that says "on its way" over artwork or the
   placeholder alike. It is the only wash left on this component: the live
   tile's was deleted in V-06 so nothing tints the ground its words are
   measured against. */
.got-gamecard--soon .got-gamecard__veil {
  background: var(--gt-media-veil-soon);
}
/* The badge takes the capsule's place in the body: same edge, same rhythm,
   so a soon tile's rows line up with a live one's.

   🔴 THE `margin-inline-start: 0` THAT STOOD HERE IS DELETED WITH ITS
   REASON — V-18d, 27 Aug. It existed to neutralise the shared badge rule's
   `margin-inline-start: auto`, which was written for a flex ROW and — in
   this flex COLUMN — pushed the badge along the inline axis by whatever the
   title left (measured on /games at 1536: restoring the auto moved the
   badge x 791 → 949.05, used margin 158.047px; V-18a Batch 5's whole
   finding). The previous version of this comment ended by naming the auto
   "a candidate for deletion by whoever owns the store card" — V-18d is that
   owner: the store card became this tile, every surface that renders the
   badge is now a flex COLUMN, and the auto was deleted from the shared rule
   below rather than re-neutralised per caller. `align-self: flex-start` is
   what places the badge at the body's inline start, in both directions,
   with no margin to fight. */
.got-gamecard--soon .got-gamecard__badge {
  align-self: flex-start;
  margin-block-start: var(--gt-space-2);
  /* 🔴 AND IT IS THE CAPSULE'S HEIGHT, DERIVED RATHER THAN PICKED. Measured
     on the rendered grid at 1536: the badge came out 24px against the
     capsule's 27, so a coming-soon tile's whole body was 100px against its
     neighbours' 103 and its title sat three pixels lower across the row. Three
     pixels is small and it is exactly the defect this slot exists to prevent —
     the 18 Aug version of it was 82px and was found by looking; this one was
     found by measuring, which is the only reason it is here.

     The value is the capsule's own arithmetic and not the number 27: two
     `--gt-space-2` of block padding, one line of `--gt-text-xs` at
     `line-height: 1`, and two hairlines of border. Retune the capsule and the
     badge follows it; write 27px and it does not. */
  min-block-size: calc(2 * var(--gt-space-2) + var(--gt-text-xs) + 2 * var(--gt-hairline));
}
/* THE DECORATIVE HOVER — the ONE state a coming-soon tile answers with, and
   it is stated this narrowly on purpose (hover-skill §3a): on hover the tile
   shows a soft orange edge glow and the badge brightens. NO lift, NO zoom, NO
   capsule, NO sheen, cursor stays default, the tile stays a <div> with no Tab
   stop and nothing to 404 into. It acknowledges the customer without
   pretending to be clickable. Drawn by a pseudo-element on the CARD — it was
   on `__frame` until V-06 and moved back when the frame was deleted — and
   animated by OPACITY only, through the system's durations, so reduced motion
   collapses it. `:focus-within` is written for symmetry with the skill's rule
   and can never fire here: nothing inside can take focus.

   INSET, because the card clips its overflow and an outer glow would never be
   seen — measured 19 Aug: only the 1px ring survived. It sits at z-index 4,
   where the edge would be on a live tile, so it glows the PICTURE and does
   not wash orange over the title and the badge above it. */
.got-gamecard--soon::after {
  content: "";
  position: absolute;
  inset: 0;
  z-index: 4;
  box-shadow: var(--gt-media-glow-soon);
  opacity: 0;
  pointer-events: none;
  transition: opacity var(--gt-duration-base) var(--gt-ease-standard);
}
/* Under (hover:hover) only: on a touch screen a tap left Chrome's sticky
   :hover on the tile — glow lit, badge solid — until the next tap somewhere
   else (the Phase A verifier, 19 Aug). A decoration that answers the pointer
   has nothing to say to a finger. */
@media (hover:hover) {
  .got-gamecard--soon:hover::after {
    opacity: 1;
  }
  .got-gamecard--soon:hover .got-gamecard__badge {
    background: var(--gt-soon);
    color: var(--gt-ink-on-accent);
  }
}
.got-gamecard--soon:focus-within::after {
  opacity: 1;
}
.got-gamecard--soon:focus-within .got-gamecard__badge {
  background: var(--gt-soon);
  color: var(--gt-ink-on-accent);
}
/* The badge: the micro-label treatment — the one uppercase step the system
   allows (Arabic has no case; there the words carry it). Since V-18d the
   store card IS this tile, so "shared with the store card" is sharing BY
   CLASS rather than by a second reader of one rule — and the
   `margin-inline-start: auto` that stood here is DELETED, not inherited:
   it was written for a flex row, every body that renders this badge is a
   flex column, and in a column an inline auto margin is either inert or
   the V-18a Batch 5 defect (the badge pushed by whatever the title
   leaves). Placement belongs to `align-self` in the soon rule above. */
.got-gamecard__badge {
  display: inline-flex;
  align-items: center;
  padding-block: var(--gt-space-1);
  padding-inline: var(--gt-space-3);
  border-radius: var(--gt-radius-pill);
  /* THE ORANGE: the badge is the soon colour on its tint (Omar, 19 Aug); on
     the tile's decorative hover it fills solid and the words go to ink. */
  background: var(--gt-tint-soon);
  color: var(--gt-soon);
  font-size: var(--gt-text-3xs);
  font-weight: var(--gt-weight-semibold);
  letter-spacing: var(--gt-tracking-label);
  text-transform: uppercase;
  line-height: var(--gt-leading-snug);
  transition: var(--gt-transition-control);
}

/* -------- hover AND focus-visible, always together, always the same ------
   🔴 THE LIFT IS CARRIED BY `transform` AND BY NOTHING ELSE. The V-06 study
   also writes `border-color: transparent` and a
   `0 22px 48px -18px rgba(31,161,255,.55)` throw on this rule. Neither ships,
   and the reason is Omar's resolution #8 of 17 Aug, quoted rather than
   paraphrased:

     "The rule wins over its own sample. The edge glow is an opacity fade on
      its own layer; the hairline border under it never changes; no lift
      shadow."

   Read literally that is three instructions and all three are obeyed here:
   the edge is `.got-gamecard__edge`, an opacity fade on its own layer; the
   hairline stays `var(--gt-line)` in every state (measured rgb(37,44,64) at
   rest, hovered and focused alike); and this rule adds no lift shadow, eased
   or instant.

   🔴 IT USED TO GO ON "the card sits at --gt-level-elevated and stays
   there", WHICH IS FALSE FOR HALF OF THIS RULE'S OWN SELECTOR — on
   `:focus-visible` the ring's band replaces the elevation outright. That
   sentence was corrected in the block comment at the top of this component
   and this SECOND COPY of it was left standing, which is the more useful half
   of the lesson: a claim written in two places gets corrected in one. It is
   not restated here. The measurement, the reason it is left alone and the
   question it raises for the ring system are in that one place. */
.got-gamecard:not(.got-gamecard--soon):hover,
.got-gamecard:not(.got-gamecard--soon):focus-visible {
  transform: translateY(calc(-1 * var(--gt-media-lift)));
}
.got-gamecard:not(.got-gamecard--soon):hover .got-gamecard__img,
.got-gamecard:not(.got-gamecard--soon):focus-visible .got-gamecard__img {
  transform: scale(var(--gt-media-zoom));
  filter: var(--gt-media-lit);
}
.got-gamecard:not(.got-gamecard--soon):hover .got-gamecard__grid,
.got-gamecard:not(.got-gamecard--soon):focus-visible .got-gamecard__grid {
  background-image: var(--gt-media-scanline-lit);
}
/* The type frame takes its colour back exactly as a photo does. */
.got-gamecard--type:not(.got-gamecard--soon):hover .got-gamecard__media,
.got-gamecard--type:not(.got-gamecard--soon):focus-visible .got-gamecard__media {
  filter: var(--gt-media-lit);
}
.got-gamecard:not(.got-gamecard--soon):hover .got-gamecard__edge,
.got-gamecard:not(.got-gamecard--soon):focus-visible .got-gamecard__edge {
  opacity: 1;
}
.got-gamecard:not(.got-gamecard--soon):hover .got-gamecard__sheen,
.got-gamecard:not(.got-gamecard--soon):focus-visible .got-gamecard__sheen {
  animation: got-sweep var(--gt-duration-sweep) var(--gt-ease-standard) 1;
}
.got-gamecard:not(.got-gamecard--soon):hover .got-gamecard__go,
.got-gamecard:not(.got-gamecard--soon):focus-visible .got-gamecard__go {
  opacity: 1;
  background: var(--gt-on-accent);
  border-color: var(--gt-on-accent);
  color: var(--gt-on-accent-ink);
}
.got-gamecard:not(.got-gamecard--soon):hover .got-gamecard__go::after,
.got-gamecard:not(.got-gamecard--soon):focus-visible .got-gamecard__go::after {
  transform: translateX(var(--gt-media-arrow-nudge));
}
@keyframes got-sweep {
  0%   { opacity: 0; transform: rotate(14deg) translateX(0); }
  16%  { opacity: 1; }
  100% { opacity: 0; transform: rotate(14deg) translateX(var(--gt-sweep-to)); }
}

/* The ring: the system's, verbatim. Hover and focus give the same reward
   AND the ring stays — "the hover effect is enough" is how the ring was
   lost four times before this skill was written.

   The reference's own ring is `outline: 3px solid var(--cyan); outline-offset:
   4px` — a 3px ring with no band, which is precisely the shape the skill
   forbids: on a card sitting next to an accent-filled neighbour it measures
   1.00:1 against it. The system's two-tone ring is what ships, and the
   `box-shadow` here is the BAND, which is the only box-shadow any state on
   this card writes (see resolution #8 above). */
.got-gamecard:not(.got-gamecard--soon):focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  box-shadow: var(--gt-ring-shadow);
}

/* -------- press: after hover in source, because a pressed card is a hovered
   card and both selectors are (0,2,0) — source order decides. -------------- */
.got-gamecard:not(.got-gamecard--soon):active {
  transform: translateY(calc(-1 * var(--gt-media-lift) / 3)) scale(var(--gt-press-scale));
  transition-duration: var(--gt-duration-instant);
}
.got-gamecard:not(.got-gamecard--soon):active .got-gamecard__img {
  transform: scale(var(--gt-media-zoom-press));
  transition-duration: var(--gt-duration-instant);
}

/* -------- touch: THE BLOCK IS BACK, AND WHY IT WENT AWAY IS PART OF THE
   REASON IT RETURNED (V-06) ------------------------------------------------

   V-05 deleted it and wrote, correctly for the component it was describing:
   "rest is the picture in every input mode, so a touch-specific value could
   only make a phone worse than a laptop… there was never a hidden CTA to
   reveal either, which is the point of moving the body off the picture."
   Both halves of that were true of the V-02 card and neither is true of this
   one. The body went back ON the picture and V-06 hid the capsule until
   hover — and a hover-only affordance does not exist on a phone (skill
   §5.6). V-13 Batch 5 then made the capsule rest-visible everywhere, which
   CHANGED this block's content again; the reconciliation below says what
   remains touch-specific.

   So the block returns with the OPPOSITE content to the one that was
   deleted. It does not hold the artwork back for touch; it hands a touch
   screen the REVEALED state, and settles the artwork a whisper so the
   always-visible capsule reads over it.

   V-13 Batch 5 RECONCILED rather than deleted this block: the base rule now
   shows the capsule AT REST on every pointer, so what remains touch-specific
   is exactly two things — FULL opacity (a phone has no hover to complete
   the reward, so it gets the completed state) and the settled artwork. The
   `transform:none` that used to cancel the rise went with the rise itself.

   The prose above was rewritten in the same pass rather than left saying the
   block is gone. When a token dies the prose that names it is part of the
   change, and so is the prose that celebrated its deletion. */
@media (hover: none) {
  .got-gamecard__go {
    opacity: 1;
  }
  .got-gamecard__img {
    filter: var(--gt-media-touch);
  }
  /* :not(--soon) is load-bearing here and nowhere else in this block. The
     soon dim is (0,2,0) on `.got-gamecard--soon .got-gamecard__media` and
     this would be (0,2,0) too, LATER in source — so without the guard a
     coming-soon tile would lose its dim on every phone. */
  .got-gamecard--type:not(.got-gamecard--soon) .got-gamecard__media {
    filter: var(--gt-media-touch);
  }
}

/* -------- reduced motion: less movement, not less information --------------
   The durations already collapsed through the tokens. What a duration cannot
   reach is stopped here: the transforms (a 1ms lift is still a jump) and the
   sweep (a 1ms sweep is still a flash). The colour, the veil, the edge and
   the capsule's own fade all still happen — the state is communicated,
   calmly. */
@media (prefers-reduced-motion: reduce) {
  .got-gamecard:not(.got-gamecard--soon):hover,
  .got-gamecard:not(.got-gamecard--soon):focus-visible,
  .got-gamecard:not(.got-gamecard--soon):active {
    transform: none;
  }
  /* 🔴 THIS RULE IS THE ONLY THING STOPPING THE HOVER ZOOM REACHING A
     REDUCED-MOTION USER, AND A COMMENT HERE ONCE SAID THE OPPOSITE. It
     claimed that with rest at exactly 1 the rule "can no longer be told apart
     from its own absence by any instrument here", and invited the reader to
     treat it as a statement of intent rather than a load-bearing declaration.

     MEASURED by V-05's verifier, three arms, reduced motion forced on and
     transitions killed — the computed transform of `.got-gamecard__img` while
     hovered:

       A  the rule as shipped          matrix(1, 0, 0, 1, 0, 0)
       B  the declaration REMOVED      matrix(1.03, 0, 0, 1.03, 0, 0)   <-- !
       C  `transform: none` instead    none

     B is the hover zoom going straight through to somebody who asked for less
     movement — exactly what this block exists to prevent. Those readings were
     taken when rest was 1 and hover 1.03; V-06 moved them to 1.005 and 1.06,
     which makes arm B a LARGER violation and arm A a value that is no longer
     even the identity matrix. So the rule is now distinguishable from
     `transform: none` as well, which the note below said it was not — and it
     is written as `scale(var(--gt-media-zoom-rest))` for the reason it always
     was: it states the invariant that under reduced motion the artwork sits
     at its RESTING size, whatever that value later becomes.

     KEEP THE CORRECTION. Writing "no instrument can tell" about a rule nobody
     had run the arms on is this project's own named defect, and it was
     written here about the one rule in the batch that most needed measuring.
     It would have read, to the next author, as permission to delete it. */
  .got-gamecard:not(.got-gamecard--soon):hover .got-gamecard__img,
  .got-gamecard:not(.got-gamecard--soon):focus-visible .got-gamecard__img,
  .got-gamecard:not(.got-gamecard--soon):active .got-gamecard__img {
    transform: scale(var(--gt-media-zoom-rest));
  }
  /* The capsule no longer moves at all — V-13 Batch 5 made it visible at
     rest with no translate, so there is no rise for this block to stop and
     the rule that stopped it is deleted rather than left as a fallback
     nothing can reach. What still has to be stopped is the arrow's nudge,
     at the two selectors that start it. */
  .got-gamecard:not(.got-gamecard--soon):hover .got-gamecard__go::after,
  .got-gamecard:not(.got-gamecard--soon):focus-visible .got-gamecard__go::after {
    transform: none;
  }
  /* The same two selectors that START the sweep, so this is (0,3,0) against
     (0,3,0) and later in source. Written as the bare `.got-gamecard__sheen`
     it LOST — measured under emulated reduced motion: `got-sweep 0.001s`,
     the flash this block exists to prevent. */
  .got-gamecard:not(.got-gamecard--soon):hover .got-gamecard__sheen,
  .got-gamecard:not(.got-gamecard--soon):focus-visible .got-gamecard__sheen {
    animation: none;
  }
}


/* ==========================================================================
   3 · THE LEGACY NAMES

   Not a third section of the system — a BRIDGE, and one with an end date.

   Eight templates that have not been migrated yet say `.got-auth__status`,
   `.got-auth__errors`, `.got-field__hint` and `.got-account__panel`. Those
   four rules used to be defined in site.css. Deleting them there and pointing
   the names at the system HERE means:

     · the eight pages are unchanged in markup and instantly consistent with
       the system — the account pages get the card, the auth pages get the
       message box, all four get the corrected RTL padding for free;
     · there is exactly ONE definition of each treatment, which is the whole
       point. Leaving them in site.css would have been the two-systems
       problem with extra steps.

   🔴 EACH LINE HERE IS DELETED WHEN THE LAST TEMPLATE SAYING IT IS MIGRATED,
   and the list is short on purpose. A bridge that grows is a second system
   wearing a apology. `database/check_site_css.php --legacy` prints who still
   says each name.

   THE OUTER MARGIN IS THE ONLY THING THAT IS NOT A PURE ALIAS. The old
   `.got-auth__status` carried `margin-top:1rem` and the system's
   `.got-message` deliberately carries none — spacing between a component and
   what precedes it belongs to the page, or the component cannot be placed.
   The margin is kept HERE, on the legacy name only, so the eight pages do not
   shift while they wait. It does not travel to `.got-message`.
   ========================================================================== */

.got-auth__status,
.got-auth__errors {
  padding-block: var(--gt-space-4);
  padding-inline: var(--gt-space-6) var(--gt-space-5);
  border-radius: var(--gt-radius-md);
  border-inline-start: var(--gt-border-bar) solid var(--gt-line-strong);
  color: var(--gt-ink);
  font-size: var(--gt-text-xs);
  line-height: var(--gt-leading-normal);

  /* Legacy only. See above. */
  margin-block-start: var(--gt-space-5);
}
.got-auth__status {
  background: var(--gt-tint-success);
  border-inline-start-color: var(--gt-success);
}
.got-auth__errors {
  background: var(--gt-tint-danger);
  border-inline-start-color: var(--gt-danger);
}
.got-auth__errors li + li {
  margin-block-start: var(--gt-space-2);
}

/* The help sentence under a field, under its older name. This is also where
   the 14 Aug hint correction lands permanently: it was `--dim` at 3.66:1 in
   its original rule and was overridden to `--muted` two hundred lines later
   in the same file, which is how the comment describing it came to be wrong.
   One definition, 6.53:1, no override to disagree with. */
.got-field__hint {
  display: block;
  margin-block-start: var(--gt-space-2);
  font-size: var(--gt-text-2xs);
  line-height: var(--gt-leading-normal);   /* prose (19 Aug) */
  color: var(--gt-ink-muted);
}

/* The account pages' panel IS the system's panel card. The declarations were
   already identical — surface, hairline, 14px radius, 1.5rem padding — so the
   only thing migrating changes is that the panels now catch the light like
   every other object in the system. `.got-account__scroll` negative-margins
   against this padding and is unaffected: --gt-space-7 IS 1.5rem. */
.got-account__panel {
  background: var(--gt-surface-raised);
  border: var(--gt-border-hair) solid var(--gt-line);
  border-radius: var(--gt-radius-lg);
  padding: var(--gt-space-7);
  box-shadow: var(--gt-level-base);
}


/* The wallet's figures, under their older names. Moved out of site.css on
   17 Aug (the client-area round) so the ledger row and the balance hero —
   system components — do not sit on a page whose figures a legacy sheet
   styles. `StoreCreditScreensTest` pins the MARKUP of both
   (`class="got-credit__balance" dir="ltr"><bdi>`), so the names stay; only
   where they are decided moved.

   Bidi, carried over and not re-derived: an isolated LTR figure that WRAPS is
   an LTR run in two pieces the algorithm may reorder, so both stay nowrap.
   The two `[dir="rtl"] text-align` rules that used to sit beside them are
   GONE — the balance is a flex item of `.got-stat--hero` and the amount a
   flex item of `.got-ledger__money`, both shrunk to content and placed by
   the container's direction, so the figure has no width to be misaligned
   inside. Measured on /ar/account/wallet in the round's report. */
/* 🔴 MOVED OUT OF `site.css` BY V-40. These were V-39's, and they were legal
   there while nothing in the system styled `got-credit__form` — V-40 gave that
   class a `::before` in this file, which made every one of these a legacy
   sheet redefining a system class with no page context. `SiteTokensTest` named
   all three. The component is the system's now, whole. */
/* ================= THE TOP-UP PAGE — STUDY D + COLUMN B (V-39) ==========

   Omar chose Study D's structure with Study B's information column
   (`04_Brand/topup-final.html`, 7 Sep 2026). The form asks one question — how
   much, and how — and the column beside it answers the ones a customer has
   before they send money to an address.

   A GRID and no breakpoint of its own: `auto-fit` with a `minmax` floor puts
   the column beside the form when there is room for both and under it when
   there is not, so there is no media query to fall out of step with the
   container. The floor is in `rem` because it is about the MEASURE of the
   text in the column, not about a device.

   The accent hairline is the one the wallet's capsule and the home hero
   already carry, read from the same tokens — a fourth private copy of a blue
   is how a palette becomes four palettes. */
.got-credit__split{
  display:grid; gap:var(--gt-space-6);
  grid-template-columns:repeat(auto-fit, minmax(20rem, 1fr));
  align-items:start;
}
/* The form is the page's subject, so it takes the wider track wherever the
   grid has two. */
.got-credit__form{ grid-column:span 1; }

/* 🔴 ONE LIT TOP EDGE, ON EVERY CARD OF BOTH HALVES OF THE FLOW — V-41c.

   Omar, with the two pages side by side: "when you move to the second page it
   is not the same thing", "the colours are different", "make them as if they
   were one page". He chose Study A from `04_Brand/topup-flow-styles.html`,
   whose whole proposition is that ONE signature repeated on every card is
   what makes two pages read as one.

   IT REPLACED A RULE ON `.got-credit__form` RATHER THAN JOINING IT. That
   selector lit exactly one card on one page, and the second page had no
   equivalent; a copy of it for the transfer page's cards would have been the
   two-systems problem this file has paid for repeatedly. The edge is defined
   once, here, and the pages differ only in how many cards they render.

   🔴 AND IT IS SELECTED THROUGH TWO CLASSES THIS FILE OWNS, WHICH IS NOT A
   STYLE CHOICE — IT IS WHAT THE SHEET BOUNDARY ALLOWS. The first version of
   this rule read `.got-credit__page .got-order__panel`, and `SiteTokensTest`
   turned it down in exactly the words the rule exists for: *"site.css
   overrides .got-order__panel with no page context — these load AFTER the
   system, so they WIN and the system is decoration."* `.got-order__panel` is
   the legacy sheet's class; naming it here builds a rule that a later
   stylesheet silently outranks. MEASURED, by the suite, not reasoned about
   afterwards.

   So the cards in this flow carry `got-credit__card` of their own, and
   `TopUpFlowContinuityTest` holds the other half of that bargain: every
   `.got-order__panel` inside the flow's templates must wear it, or a card
   added later joins the pages without the signature and nothing complains.

   `.got-credit__page` is on the two top-up containers and the design
   specimen, and nowhere else — the blast radius is the two pages Omar was
   looking at.

   The geometry moved with it: `position` and `overflow` were on the form
   because its own `::before` needed them, and they belong wherever the
   pseudo-element is drawn.

   A horizontal gradient does not mirror, so RTL reads the reversed token —
   never a second rule with its own colours. */
.got-credit__page .got-credit__card{ position:relative; overflow:hidden; }
.got-credit__page .got-credit__card::before{
  content:""; position:absolute; inset-block-start:0; inset-inline:0;
  block-size:var(--gt-border-bar);
  background:var(--gt-gradient-edge);
}
[dir="rtl"] .got-credit__page .got-credit__card::before{
  background:var(--gt-gradient-edge-rtl);
}
/* The column is quieter than the form beside it: same panel, one step down in
   ink, and it never competes for the eye with the control the page exists
   for. */
.got-credit__facts{ background:var(--gt-surface-sunken); }
/* The facts column's own note rhythm lives in site.css beside the method row,
   for the same ownership reason: `.got-order__note` is a legacy-sheet class
   and naming it here would make site.css's own rule for it an offence. */

/* ================= THE TOP-UP PAGE'S CONTROLS — V-40, 7 Sep 2026 ==========

   Omar approved `04_Brand/topup-final.html` ("D + B"), V-39 shipped something
   visibly different, and he said «فيه فرق كبير بين المتوقع والمتطبق». These
   are the differences that needed no words.

   🔴 THEY LIVE HERE AND NOT IN `site.css`, AND THE SUITE SAID SO. The first
   cut put them in the legacy sheet: `SiteTokensTest` refused
   `.got-credit__amount .got-field__hint` (a system class redefined with no
   page context), `SystemCoverageTest` refused an interactive control the
   system did not claim, and `ClientAreaUxTest` refused the literal
   `.got-credit__balance{` appearing in site.css at all. All three were right
   — a new component belongs to the system.

   🔴 EVERY INTERACTIVE SURFACE CARRIES THE HOUSE STATES, IN THE COMMIT THAT
   INTRODUCES IT. The ring is the system's three declarations, the press is
   `--gt-press-scale`, and the transitions name colour and transform only.
   Nothing animates a width, an inset or a shadow.
   ======================================================================== */

/* The heading and "Balance now" on one line, the figure at the END side and
   one step down. It was large and inside the form card, competing with the
   amount the customer is about to type. */
.got-credit__head {
  display: flex;
  align-items: flex-end;
  justify-content: space-between;
  gap: var(--gt-space-5) var(--gt-space-7);
  flex-wrap: wrap;
  /* 🔴 ONE STEP DOWN THE PAGE — V-42b2 Batch 3. MEASURED before: the gaps
     between the four blocks above the cards were 8 · 20 · 24 · 0, which is
     three different answers and then no answer at all — the notice sat hard
     against the cards. It was `--gt-space-6` (20px) here; the rail below
     already used `--gt-space-7` (24px), and the notice now does too, so the
     page steps 8 · 24 · 24 · 24. The 8 is the back link's own tight pairing
     with the title it belongs to and is deliberately not part of the run. */
  margin-block-end: var(--gt-space-7);
}
.got-credit__headfig {
  text-align: end;
}

/* 🔴 THE FILE CONTROL, WITH OUR OWN WORDS ON IT — V-41d, 8 Sep 2026.

   Omar, on /ar, marked "Choose File / No file chosen" in red among the
   untranslated strings. It was not untranslated: a bare `<input type="file">`
   is drawn by the BROWSER and labelled in the BROWSER's language, so one
   control reads English on his machine and French on a French customer's. No
   translation key could ever have reached it.

   IT IS IN THIS FILE BECAUSE THE SUITE PUT IT HERE. Written first in
   `site.css` beside the native input's own rules, it failed two guards at
   once: `SystemCoverageTest` ("a control shipped that the browser is drawing
   — give it a system class") and `TypeRegisterTest`, which counted two
   hand-picked sizes in the legacy sheet. Both were right, and both are the
   same objection said twice: an interactive control belongs to the design
   system, on the system's scale.

   🔴 THE NATIVE CONTROL IS HIDDEN ONLY UNDER `is-enhanced`, WHICH THE SCRIPT
   ADDS. `public/js/file-field.js` is what makes this control appear; without
   it the browser's own button is still there and still works. That is the
   whole reason `site.css` keeps its `::file-selector-button` rules, and
   `TopUpArabicAndFileFieldTest` fails any rule that hides the input
   unconditionally — on a page where the screenshot IS the proof that money
   was sent, a control nobody can see is worse than an untranslated one.

   🔴 AND THERE IS NO SECOND HIDING RECIPE HERE. The script hides the native
   input by giving it `got-sr-only` — the system's own visually-hidden
   utility, whose docblock beside `--gt-hairline` already argues the exact
   point this control needed: a 1px focusable box rather than `display:none`,
   because `display:none` and `visibility:hidden` both make an element
   unfocusable and Chrome then refuses the whole submit with "an invalid form
   control is not focusable" — a `required` field the browser cannot point at
   breaks the form rather than the styling.

   The first cut wrote that recipe again, here, and `SiteTokensTest` caught
   it by its two raw `1px` values. It was right twice over: the literals were
   the symptom, and a second copy of the visually-hidden recipe was the
   defect. Nothing in this block hides the input at all now.

   It sits inside the `.got-field` label, so a click anywhere on the label
   opens the picker exactly as it did before. */
.got-file{
  display: inline-flex;
  align-items: center;
  gap: var(--gt-space-4);
  position: relative;
  max-inline-size: 100%;
}
/* Neither half is rendered until the script has run, so nothing that belongs
   to the enhanced control can be seen beside the native one. */
.got-file__button,
.got-file__name{ display: none; }
.got-file.is-enhanced .got-file__button,
.got-file.is-enhanced .got-file__name{ display: inline-block; }
.got-file__button{
  flex: none;
  padding-inline: var(--gt-space-4);
  padding-block: var(--gt-space-2);
  border: var(--gt-border-hair) solid var(--gt-line);
  border-radius: var(--gt-radius-sm);
  background: var(--gt-surface-overlay);
  color: var(--gt-ink);
  font-size: var(--gt-text-2xs);
  font-weight: var(--gt-weight-semibold);
  line-height: var(--gt-leading-snug);
  cursor: pointer;
  transition: border-color var(--gt-duration-base) var(--gt-ease-standard);
}
.got-file:hover .got-file__button{ border-color: var(--gt-line-strong); }
/* The ring follows the INPUT's focus, because the input is the control — the
   button is its face and can never be tabbed to on its own. */
/* THE RING ALONE, and the border is deliberately left where it is. The first
   cut also set `border-color: var(--gt-accent)` here; `FocusVisibilityTest`
   turned it down — the ring can be that colour, so a focused control would
   draw the same stroke twice with the gap between them, which reads as a
   fault rather than as focus. */
.got-file.is-enhanced .got-file__input:focus-visible ~ .got-file__button{
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
}
.got-file__name{
  min-inline-size: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
  color: var(--gt-ink-muted);
  font-size: var(--gt-text-2xs);
  line-height: var(--gt-leading-snug);
}

/* 🔴 THE CARD'S TOP EDGE MOVED UP THIS FILE IN V-41c AND WIDENED TO THE WHOLE
   FLOW — see the `got-credit__card` rule above. What stood here
   was this page's private copy of the ramp; it is deleted rather than left
   beside its replacement, because a dead rule with a true-sounding comment is
   the thing the next round quotes. */

/* THE AMOUNT IS ONE CONTROL: a symbol, the figure, a unit. The BOX carries
   the border; the input inside it is bare. */
.got-credit__money {
  display: flex;
  align-items: center;
  gap: var(--gt-space-4);
  padding-inline: var(--gt-space-6);
  padding-block: var(--gt-space-4);
  border: var(--gt-border-hair) solid var(--gt-line);
  border-radius: var(--gt-radius-lg);
  background: var(--gt-surface-sunken);
  transition: border-color var(--gt-duration-base) var(--gt-ease-standard);
}
.got-credit__money:hover {
  border-color: var(--gt-line-strong);
}
.got-credit__sym,
.got-credit__unit {
  flex: none;
  color: var(--gt-ink-muted);
  font-weight: var(--gt-weight-semibold);
  font-variant-numeric: tabular-nums;
}
.got-credit__unit {
  font-size: var(--gt-text-2xs);
  letter-spacing: var(--gt-tracking-label);
}
/* 🔴 THE INPUT KEEPS ITS OWN RING AND THE FIRST CUT TOOK IT AWAY.
   That version put the ring on the wrapper with `:focus-within` and wrote
   `outline: none` on the input — and `FocusVisibilityTest` failed twice, in
   the two arms whose whole subject is a control that loses its indicator by
   suppression. It was right: a keyboard user tabs to the INPUT, and a ring
   drawn on an ancestor is a different element's outline. The wrapper keeps
   its hover; the ring stays where focus lands. */
/* 🔴 THE SELECTOR IS A COMPOUND AND THAT IS THE WHOLE FIX — BUG-711.
   The first cut wrote these on `.got-credit__input` alone, at (0,1,0), and
   MEASURED on the rendered page every declaration had lost to
   `.got-field input` at (0,1,1): 16px not 28, the sunken fill not
   transparent, a 1px #66739A border not none, a 10px radius, 8/12 padding
   and a 44px floor. The control this round exists to build was a second
   bordered box inside a wrapper that already had a border — two boxes where
   the study has one — and NOTHING could see it: the ring still drew (from
   `.got-field input:focus-visible`), nothing overflowed, nothing clipped, and
   the `/design` specimen wraps the input in the same `.got-field` so it was
   wrong identically. Only measuring the study beside the page said so.

   `.got-field .got-credit__input` is (0,2,0) and beats (0,1,1) on
   specificity, so no source order decides it — which is the difference
   between this and the same repair written as a move. `:focus-visible`
   likewise: (0,3,0) against `.got-field input:focus-visible`'s (0,2,1),
   rather than a tie that the next append re-orders.

   The 44px floor STAYS. The input is the tap target; the label around it
   grows to fit the larger type. */
.got-field .got-credit__input {
  flex: 1 1 auto;
  min-inline-size: 0;
  inline-size: auto;
  border: 0;
  background: transparent;
  padding: 0;
  border-radius: 0;
  /* 28px, the ramp's step. The study measures 29.6 — 1.6px off a token, and
     a hand-picked size to close that gap is the defect V-39's own report
     named: 29 literals in the legacy sheet. */
  font-size: var(--gt-text-2xl);
  font-weight: var(--gt-weight-bold);
  font-variant-numeric: tabular-nums;
  color: var(--gt-ink);
}
.got-field .got-credit__input:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  box-shadow: var(--gt-ring-shadow);
}
/* This page's hint is a whole sentence and `.got-field__hint` is `nowrap` for
   the short ones it was written for. MEASURED clipped at 538px of text in a
   510px box, mid-word, on the line that states the minimum. */
.got-credit__amount .got-field__hint {
  white-space: normal;
}

/* 🔴 THE TWO QUESTIONS ARE THE FORM CARD'S SECTIONS — V-42b2 Batch 3.

   MEASURED on the rendered page: the "How it works" card carries an 18px/700
   title and the FORM card carries none — its two questions ("How much would
   you like to add?" and "How would you like to pay?") rendered at 13px/600,
   the same eyebrow the little hints use, so the card that does the work read
   as the flatter of the two.

   🔴 NO NEW COPY WAS INVENTED. The brief asked for one clear title per card;
   the honest way to get one without writing a sentence nobody approved is to
   let the questions the card already asks BE its sections. They step up to
   `--gt-text-md` semibold in full ink — one step under the other card's
   title, which is right: they are sections of a card, not a card's name.

   THE ELEMENTS DO NOT CHANGE. `.got-field__label` stays a `<span>` inside its
   `<label>`, so the input keeps the label it is wired to; promoting it to an
   `<h2>` would have broken that for a screen reader to gain a heading the
   page outline does not want.

   IN system.css AND NOT IN site.css, and the guard is what said so: every
   class in this selector is one the system owns, so written in the legacy
   sheet it is a redefinition with no page context —
   `SiteTokensTest::test_nothing_the_system_owns_is_redefined_by_the_legacy_
   stylesheet` refused it there on the first run. */
.got-credit__page .got-credit__amount > .got-field__label,
.got-credit__page .got-credit__paylabel {
  font-size: var(--gt-text-md);
  color: var(--gt-ink);
  margin-block-end: var(--gt-space-4);
}

/* THE PROGRESS RAIL — V-40, difference 8. Two segments; the lit one carries
   the accent and a gradient hairline, the other a plain line. A list, because
   it is one; `aria-current="step"` in the markup says which. */
.got-credit__rail {
  list-style: none;
  display: flex;
  align-items: center;
  gap: var(--gt-space-3) var(--gt-space-5);
  margin: 0 0 var(--gt-space-7);
  padding: 0;
  /* MEASURED at 390: two segments and their connectors wanted 353px in a
     350px box — a 3px overflow, which is a horizontal scrollbar on a phone.
     They wrap instead, and the connector's floor came down with them. */
  flex-wrap: wrap;
}
.got-credit__seg {
  display: flex;
  align-items: center;
  gap: var(--gt-space-3);
  flex: 1 1 auto;
  min-inline-size: 0;
  font-size: var(--gt-text-2xs);
  font-weight: var(--gt-weight-semibold);
  color: var(--gt-ink-muted);
}
.got-credit__seg::after {
  content: "";
  flex: 1 1 auto;
  min-inline-size: var(--gt-space-5);
  block-size: var(--gt-border-hair);
  background: var(--gt-line);
}
/* 🔴 THE THIRD STATE — V-41b. The rail had exactly two, `--seg` and
   `--seg--on`, because until now it only ever appeared on the page where
   segment 1 is current. On the transfer page segment 1 is FINISHED, and
   "finished" is not "not current".

   BOTH CANDIDATES WERE RENDERED BEFORE THIS ONE WAS KEPT:

     A (shipped)  ink `--gt-ink`, connector solid accent
     B (rejected) ink `--gt-ink-muted`, connector accent at .55

   B was rejected on the measurement, not on taste: its done ink came out
   `rgb(147,160,188)`, which is EXACTLY a pending segment's colour — the two
   states differed only in the connector, and on page two there is no pending
   segment on screen to compare against, so a customer had no reference at
   all. The brief's requirement was "legible as finished rather than pending",
   and B is legible as pending.

   A reads as three distinct things at a glance: done is the brightest ink but
   not the accent, current is the accent and bold, pending is muted. The
   connector behind a done segment is SOLID accent — the stretch you have
   already walked is filled in; the stretch ahead is not. */
.got-credit__seg--done {
  color: var(--gt-ink);
}
.got-credit__seg--done::after {
  background: var(--gt-accent);
}
.got-credit__seg--on {
  color: var(--gt-accent);
  font-weight: var(--gt-weight-bold);
}
.got-credit__seg--on::after {
  background: var(--gt-gradient-edge);
}
[dir="rtl"] .got-credit__seg--on::after {
  background: var(--gt-gradient-edge-rtl);
}
/* 🔴 THE LAST SEGMENT'S TRAILING LINE IS NEVER LIT — V-41b, and it is a
   semantic fix rather than a tidy-up.

   `.got-credit__seg::after` draws a connector after EVERY segment, including
   the last, where it runs off toward the edge with nothing to connect to. On
   page one that was invisible: the last segment is the pending one, so its
   trailing line was already the plain hairline. On page two the last segment
   is the CURRENT one — so it took the accent gradient and the rail ended in a
   lit line pointing at a step three that does not exist. MEASURED on the
   render before this rule was written.

   `:last-child` is safe here, and this project has been bitten by a
   positional selector counting a child that was not a row (V-37). It is safe
   because the partial that owns this markup emits exactly two `<li>` and
   nothing else — the rail has no head row, no empty state and no decoration
   inside the `<ol>`.

   Specificity: (0,2,1) against `--on::after`'s (0,1,1), so it wins on its own
   rather than on source order. Page one is unchanged by it — its last
   connector was this colour already. */
.got-credit__seg:last-child::after {
  background: var(--gt-line);
}
.got-credit__segno {
  flex: none;
  font-variant-numeric: tabular-nums;
}
.got-credit__segname {
  flex: none;
}

/* THE INFORMATION COLUMN — V-40, difference 9. A heading and four titled
   facts, each with a small mark. */
.got-credit__factstitle {
  font-size: var(--gt-text-lg);
  font-weight: var(--gt-weight-heavy);
  line-height: var(--gt-leading-tight);
  color: var(--gt-ink);
  margin-block-end: var(--gt-space-6);
}
.got-credit__factlist {
  list-style: none;
  margin: 0;
  padding: 0;
  display: flex;
  flex-direction: column;
  gap: var(--gt-space-6);
}
.got-credit__fact {
  display: flex;
  align-items: flex-start;
  gap: var(--gt-space-4);
}
/* 🔴 THE FACT ICONS — V-41. Four different glyphs where V-40 drew one ring
   four times. The SIZE lives here and not on the `<svg>`: a bare element sized
   in the document is a size no stylesheet can retune, which is the line
   `AuthUxTest` already holds for the reveal eyes.

   16px, not the study's 17. 17 is on no step of the space scale and a
   hand-picked size is the defect this project has filed most often; the 1px
   is invisible beside a 14px sentence and the scale is worth more than it.

   `currentColor` in the markup means the TONE is one declaration here. The
   first three take the accent — they are what the customer gets. The fourth
   takes the muted ink, because "it stays yours" states a LIMIT rather than a
   benefit, and drawing a limit as loudly as a benefit is the page telling the
   customer the wrong thing twice. */
.got-credit__facticon {
  flex: none;
  inline-size: var(--gt-space-5);
  block-size: var(--gt-space-5);
  margin-block-start: var(--gt-space-1);
  color: var(--gt-accent);
}
.got-credit__facticon--quiet {
  color: var(--gt-ink-muted);
}
.got-credit__facthead {
  display: block;
  font-size: var(--gt-text-sm);
  font-weight: var(--gt-weight-bold);
  color: var(--gt-ink);
}
.got-credit__factbody {
  display: block;
  margin-block-start: var(--gt-space-1);
  font-size: var(--gt-text-xs);
  line-height: var(--gt-leading-normal);
  color: var(--gt-ink-muted);
}
/* The wrong-network warning, under a hairline. It is the most consequential
   sentence in the flow, so it is quiet rather than loud — a warning that
   shouts on every visit stops being read. */
.got-credit__warning {
  margin-block-start: var(--gt-space-7);
  padding-block-start: var(--gt-space-6);
  border-block-start: var(--gt-border-hair) solid var(--gt-line);
  font-size: var(--gt-text-xs);
  line-height: var(--gt-leading-normal);
  color: var(--gt-ink-muted);
}

/* The eyebrow over the method list. It wears `.got-field__label`, which is
   the treatment the amount field's own label already uses — this adds ONLY
   the space above it, because the label sits directly under the amount
   field's hint and would otherwise read as part of that sentence. A second
   full eyebrow definition is how one type scale becomes two. */
.got-credit__paylabel {
  margin-block-start: var(--gt-space-7);
}

/* THE QUICK AMOUNTS. Toggle buttons, wrapping, never submits — a submit
   carrying `name="amount"` would fire on Enter and send the wrong figure. */
.got-credit__quick {
  display: flex;
  flex-wrap: wrap;
  gap: var(--gt-space-3);
  margin-block-start: var(--gt-space-4);
}
.got-credit__chip {
  font-family: inherit;
  font-size: var(--gt-text-2xs);
  font-weight: var(--gt-weight-semibold);
  font-variant-numeric: tabular-nums;
  padding-block: var(--gt-space-2);
  padding-inline: var(--gt-space-5);
  border: var(--gt-border-hair) solid var(--gt-line);
  border-radius: var(--gt-radius-pill);
  background: transparent;
  color: var(--gt-ink-muted);
  cursor: pointer;
  min-block-size: var(--gt-tap-min);
  transition: color var(--gt-duration-base) var(--gt-ease-standard),
              border-color var(--gt-duration-base) var(--gt-ease-standard),
              background-color var(--gt-duration-base) var(--gt-ease-standard),
              transform var(--gt-duration-instant) var(--gt-ease-standard);
}
.got-credit__chip:hover {
  color: var(--gt-ink);
  border-color: var(--gt-line-strong);
}
.got-credit__chip:focus-visible {
  outline: var(--gt-ring-width) solid var(--gt-ring-color);
  outline-offset: var(--gt-ring-gap);
  box-shadow: var(--gt-ring-shadow);
}
.got-credit__chip:active {
  transform: scale(var(--gt-press-scale));
}
/* The chosen one. `aria-pressed` is the state a screen reader hears and the
   class follows it — one source of truth, set in one place by the script. */
/* 🔴 THE CHOSEN CHIP DOES NOT PAINT ITS BORDER IN THE RING'S COLOUR.
   The first cut set `border-color: var(--gt-accent)`, and `FocusVisibilityTest`
   caught it on the /design specimen: focused, the ring, its band and the
   control's own edge were all one accent and the control drew its state twice.
   That is BUG-247, whose fix cost a whole measurement round. The chosen state
   is carried by the TINT and the INK — both of which survive a focus ring
   landing on top of them — and the edge stays the neutral strong line. */
.got-credit__chip.is-on {
  color: var(--gt-accent);
  border-color: var(--gt-line-strong);
  background: var(--gt-tint-accent);
}
@media (prefers-reduced-motion: reduce) {
  .got-credit__chip:active {
    transform: none;
  }
}

/* The full-width primary is `.got-btn--block`, which system.css already defines at
   `inline-size:100%` — written as a modifier precisely so a caller does not
   restate the width. Nothing is minted for it here. */

.got-credit__label {
  font-size: var(--gt-text-3xs);
  font-weight: var(--gt-weight-semibold);
  letter-spacing: var(--gt-tracking-label);
  text-transform: uppercase;
  color: var(--gt-ink-muted);
}
/* 🔴 THE FIGURE STEPS DOWN A LEVEL — V-38, Study D, and it is the point of
   the study rather than a tweak. The balance was `--gt-text-3xl` (36px), the
   step the system reserves for "the one hero line on a page that has one",
   because until now the balance WAS the page. Study D inverts that: a wallet
   is mostly a history, so the history becomes the page and the balance becomes
   a figure in a strip across the top. MEASURED off the study: 27.2px, which is
   `--gt-text-2xl` (28px) to within half a pixel.

   `ClientAreaUxTest` pinned the 3xl and was re-pointed in the same commit,
   with the reason beside it. It is still pinned — the step is a decision, and
   a decision that nothing can redden is a preference. */
/* 🔴 V-42d B3: `--gt-text-xl`, ONE step on all three money pages. The wallet
   showed the same figure at 2xl (Study D's measured 27.2px) while the top-up
   and transfer pages showed it at xl through a child rule on
   `.got-credit__headfig` — the same number, two sizes, one screen apart, which
   is the difference Omar named on 7 Sep ("look how the font is different").
   Where one page differs from the other two, the two win; the override is
   gone rather than shadowed, so there is one home for the step again. */
.got-credit__balance {
  font-family: var(--gt-family-numeric);
  font-size: var(--gt-text-xl);
  font-weight: var(--gt-weight-bold);
  line-height: var(--gt-leading-tight);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
  color: var(--gt-ink);
}
/* A ZERO BALANCE IS STILL THE TRUTH, SO IT IS PRINTED — QUIETLY. Study D:
   the loudest thing on an empty wallet must not be a zero. The condition is
   decided in the Blade — no non-zero digit anywhere in the formatted figure,
   which is true of a zero balance in every locale and currency format this
   page can render.

   🔴 THE ATTRIBUTE IS ON THE CAPSULE AND THE RULE REACHES THE FIGURE BY
   DESCENT. Putting it on the figure broke six byte pins in one run: three test
   files read `class="got-credit__balance" dir="ltr"><bdi>` as a single
   literal, so nothing may sit between them. */
.got-credit__capsule[data-zero="true"] .got-credit__balance {
  color: var(--gt-ink-muted);
}

/* ------------------------------------------------ THE BALANCE CAPSULE (V-38)

   Study D's top strip. The hero stopped being a full-width card and became a
   compact capsule beside the page heading: eyebrow, figure, and the page's one
   action, on a soft overlay ground with a hairline and a pill radius.

   🔴 IT IS STILL `.got-stat--hero` AND THAT IS DELIBERATE. The element was
   restyled, not replaced: `ClientAreaUxTest` requires exactly one
   `.got-stat--hero` on this page with the balance and the top-up link inside
   it, `WalletCompositionTest` pins its cap and its `space-between`, and
   `WalletLivelinessTest` reads its `.got-stat__actions`. A new class beside a
   deleted one would have been four guards re-pointed to describe the same
   object under a different name. */
.got-stat--hero.got-credit__capsule {
  inline-size: auto;
  max-inline-size: none;
  flex-direction: row;
  flex-wrap: wrap;
  align-items: center;
  justify-content: flex-start;
  gap: var(--gt-space-5);
  padding-block: var(--gt-space-4);
  padding-inline: var(--gt-space-6);
  border-radius: var(--gt-radius-pill);
  background: var(--gt-surface-overlay);
  border: var(--gt-border-hair) solid var(--gt-line);
  box-shadow: none;
}
/* The figure sits under its own eyebrow and the action sits beside both, so
   the body stops trying to be a column of three. */
.got-credit__capsule .got-stat__body {
  gap: 0;
}
.got-credit__direction {
  display: block;
  font-size: var(--gt-text-3xs);
  font-weight: var(--gt-weight-semibold);
  letter-spacing: var(--gt-tracking-label);
  text-transform: uppercase;
  color: var(--gt-ink-muted);
}
.got-credit__amount {
  display: block;
  white-space: nowrap;
  font-family: var(--gt-family-numeric);
  font-variant-numeric: tabular-nums;
  font-weight: var(--gt-weight-semibold);
}

/* =====================================================================
   THE CRYPTO PAY PAGE — the facts row, the countdown strip and the QR
   (V-33 Batch 1b, 4 Sep 2026)
   =====================================================================

   The page's own Step 1 / Step 2 furniture stays in site.css under
   `.got-pay__*` (the legacy sheet's migration debt, named in
   TypeRegisterTest::SIZED_BY_HAND). What this batch ADDED is here, in the
   system, on tokens, and none of these class names is written in site.css
   (SiteTokensTest holds that).

   THE FACTS ROW. Reference · amount · network, the three things a customer
   checks against their exchange before reading anything else. `auto-fit`
   over a 9rem floor: one row of three on a desktop; at 390px it is two
   and one, because the alternative — three 7rem columns — wraps
   `Tron (TRC-20)` at its space, and a chain name split down the middle is
   the one value on this page a customer compares character by character.
   MEASURED at 390×844 with touch emulation (`matchMedia('(hover:none)')`
   true beside the reading), both languages, all three clock states: the
   grid resolves to two 144px tracks, the facts sit on two rows, and the
   network stays on one line. At 1440 it is one row of three 352px tracks. */
.got-pay__facts {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: var(--gt-space-4) var(--gt-space-6);
  margin: 0 0 var(--gt-space-4);
  padding: var(--gt-space-5) var(--gt-space-6);
  border: var(--gt-border-hair) solid var(--gt-line);
  border-radius: var(--gt-radius-lg);
  background: var(--gt-surface-raised);
  box-shadow: var(--gt-level-base);
}
.got-pay__fact {
  min-inline-size: 0;
}
.got-pay__fact dt {
  font-size: var(--gt-text-3xs);
  font-weight: var(--gt-weight-semibold);
  letter-spacing: var(--gt-tracking-label);
  text-transform: uppercase;
  color: var(--gt-ink-muted);
  margin-block-end: var(--gt-space-1);
}
/* No sizing on the bare `dd` — the system may not decide the width of an
   element by position (SiteTokensTest, BUG-235); the column's own
   `min-inline-size: 0` on `.got-pay__fact` is what lets a long value shrink. */
.got-pay__fact dd {
  margin: 0;
}
/* The reference and the network lines are the legacy `.got-pay__refcode` /
   `.got-pay__network` paragraphs, moved up here unchanged because a test pins
   their markup byte for byte; the only thing the row takes off them is the
   trailing margin the card used to need. */
.got-pay__fact dd p {
  margin-block-end: 0;
}
.got-pay__factvalue {
  font-size: var(--gt-text-lg);
  font-weight: var(--gt-weight-bold);
  line-height: var(--gt-leading-tight);
}
.got-pay__refnote {
  margin-block-end: var(--gt-space-6);
}

/* THE COUNTDOWN STRIP — ruling 3 (on this page, above both steps, visible
   without scrolling) and ruling 9 (neutral → amber under 5:00 → red under
   1:00; the client never cancels).

   🔴 `position: static` IS WRITTEN, NOT LEFT TO THE DEFAULT, because it is
   the claim: the strip scrolls with the page. TRAP 0 — nothing on this site
   is fixed or stuck to the bottom of a phone, and V-25's toast is why.
   CryptoPayPageTest reads the winning rule's last `position` and every rule
   that paints a `.got-countdown*` element for `fixed`/`sticky`.

   THE TONE IS A STATE LAYER OVER THE RAISED SURFACE, the same technique
   `.got-btn--secondary:hover` uses: `background-color` stays
   `--gt-surface-raised` and the tint goes on as a `background-image`, so the
   composite under the words is exactly the pair the system measured for its
   messages — `--gt-ink` on a 12% tint over raised, 11.25:1 warning, 12.36:1
   danger (the figures on `--gt-tint-*`). The words never take the status
   colour; the edge and the tint carry the meaning, which is the system's
   standing rule for every message.

   MOTION: `--gt-transition-control` eases the edge and the tint between
   tones. Its durations are tokens, so the reduced-motion band at the top
   of this file collapses them to 1ms; there is no `animation` on the strip
   or on the digits — a pulsing number on the page where a person is copying
   a wallet address is a distraction, not urgency (ui-ux-pro-max
   `excessive-motion`: one or two key elements per view, and the tone
   change is the element). */
.got-countdown {
  position: static;
  display: block;
  margin: 0 0 var(--gt-space-6);
  padding: var(--gt-space-4) var(--gt-space-5);
  border: var(--gt-border-hair) solid var(--gt-line-strong);
  border-inline-start: var(--gt-border-bar) solid var(--gt-accent);
  border-radius: var(--gt-radius-md);
  background-color: var(--gt-surface-raised);
  color: var(--gt-ink);
  font-size: var(--gt-text-sm);
  line-height: var(--gt-leading-normal);
  box-shadow: var(--gt-level-base);
  transition: var(--gt-transition-control);
}
/* `m:ss` in tabular figures — `number-tabular`: a timer that breathes as a
   `1` becomes a `0` moves the sentence around it every second. The numeric
   family alias is the system's own name for "the face the figures use".

   🔴 IT IS A READOUT NOW, NOT A BOLD WORD — V-35 B3, Omar's D4 ("the strip is
   plain for a control that CANCELS the order"). It was 18px bold set inline
   in a 14px sentence, which is the treatment this system gives an emphasised
   phrase; the thing it names is a clock that ends the order. A recessed chip
   — the sunken surface the system already uses for wells and inputs, the
   small radius, one step up in size — says "this is a number being counted
   down" before the sentence around it is read.

   IT STILL TAKES NO STATUS COLOUR AND NO MOTION, WHICH IS THE PART NOT TO
   UNDO. The strip's own rule states the house position: the words never take
   the status colour — the edge and the tint carry the meaning — and there is
   no `animation` here or on the strip, because a pulsing number on the page
   where a person is copying a wallet address is a distraction, not urgency
   (ui-ux-pro-max `excessive-motion`). The chip is a shape, not a signal, and
   it is identical in all three tones.

   `display: inline-block` is what lets the padding paint: padding on a bare
   inline run is drawn but does not reserve line space, so on the line where
   the sentence wraps it would have overlapped the line above it. The block
   padding is `--gt-space-1`, small enough that the chip does not grow the
   strip's line box beyond the sentence's own leading.

   🔴 AND THE FILL ALONE IS NOT A CHIP — F6, V-35 B3 fix pass, 6 Sep 2026.
   The paragraph above claimed a recessed shape; MEASURED on the rendered
   strip, the sunken fill against the raised surface it sits on is
   **1.03:1 in dark** (rgb(20,24,36) on rgb(23,27,40)) and 1.23:1 in light.
   That is not a boundary — in dark it is nothing at all, so the composition
   this comment described existed only in the author's stylesheet. It is the
   same mistake the QR's edge made two blocks down and had corrected there,
   made again one component over.

   The shape is drawn with a hairline in `--gt-line-strong` — the strip's own
   edge token, so the chip is bounded by the same line weight and colour the
   strip is. No new colour: the palette is `config/brand.php`'s and nothing
   here names one. The fill stays, because with a boundary in front of it it
   is what makes the shape read as RECESSED rather than as a box drawn round
   the figures.

   PIXEL-SAMPLED, not computed — the tone is a `background-image` over the
   raised surface, so `getComputedStyle` cannot answer this question at all
   and the reading has to come off the rendered page. A 1px cut across the
   chip's leading edge, 1440x900, /en, 6 Sep 2026:

     dark  neutral  #171B28 x12 | #66739A x1 | #141824 …   3.66:1
     dark  warn     #322C25 …   | #66739A x1 | #141824 …   2.94:1
     dark  danger   #32212C …   | #66739A x1 | #141824 …   3.23:1
     light neutral  #FFFFFF x12 | #6B7A96 x1 | #E2E8F2 …   4.33:1
     light warn     #F1EBE0 x12 | #6B7A96 x1 | #E2E8F2 …   3.65:1
     light danger   #F6E5E5 x12 | #6B7A96 x1 | #E2E8F2 …   3.56:1

   One stroke, one pixel, in every tone — against 1.03:1 and 1.23:1 for the
   fill alone, which is what was there before and is why nothing was visible
   in dark. FIVE OF THE SIX CLEAR 3:1 AND THE WARN TONE IN DARK DOES NOT, at
   2.94, and that is stated rather than rounded up: the amber tint lifts the
   strip toward the line. It is accepted rather than fixed because this chip
   is not an indicator — it identifies no control and carries no state; the
   MEANING is the strip's own edge and tint (measured in the tone rules
   above) and the figures inside it, which stay `--gt-ink` at 14.3:1 in every
   tone. Reaching 3:1 in that one tone would mean either a colour the palette
   does not have or the chip taking the status colour, which is the one thing
   the paragraph above says it must not do. */
.got-countdown__digits {
  font-family: var(--gt-family-numeric);
  font-variant-numeric: tabular-nums;
  font-size: var(--gt-text-xl);
  font-weight: var(--gt-weight-bold);
  white-space: nowrap;
  display: inline-block;
  padding-block: var(--gt-space-1);
  padding-inline: var(--gt-space-2);
  border: var(--gt-border-hair) solid var(--gt-line-strong);
  border-radius: var(--gt-radius-xs);
  background: var(--gt-surface-sunken);
}
/* THE TONE ON THE WHOLE EDGE, NOT ONLY THE BAR — V-35 B3. The hairline round
   the other three sides carries the tone too, so the strip reads as amber and
   then red at a glance rather than only along one edge. `border-color` is
   written before `border-inline-start-color` so the bar still gets the
   longhand's word for its own weight.

   🔴 AND IT GOES THROUGH `--gt-edge-color`, WHICH IS NOT DECORATION — the
   first version of this block wrote `border-color: var(--gt-warning)` and
   `FocusVisibilityTest::test_a_state_that_recolours_the_border_recolours_the_
   ring` went red on it by name, in both tones, first run. It was right and
   this batch was wrong: a status colour that reaches a border WITHOUT the
   token is an edge the ring can never learn, which is the whole of BUG-245
   and BUG-247. So the tone is declared once and both strokes are defined as
   it, exactly as `.got-field--error` and `.got-btn--destructive` do.

   `--gt-ring-color` is declared for the same reason those two declare it, and
   the honest note is that NOTHING INSIDE THE STRIP TAKES FOCUS TODAY — it is
   one `<p>` with a `<span>` of digits in it, so there is no ring on this
   element to observe and I am not claiming one was measured. The declaration
   is what makes the invariant hold for whatever is put in the strip next,
   which is the property this mechanism exists for: a sixth state is two lines
   and nothing else.

   Nothing here touches `color`: the sentence and the digits stay `--gt-ink`
   in all three tones, so the composite the system measured (11.25:1 warning,
   12.36:1 danger, on the tint over raised) is unchanged. */
.got-countdown--warn {
  --gt-edge-color: var(--gt-warning);
  --gt-ring-color: var(--gt-edge-color);
  border-color: var(--gt-edge-color);
  border-inline-start-color: var(--gt-edge-color);
  background-image: linear-gradient(var(--gt-tint-warning), var(--gt-tint-warning));
}
.got-countdown--danger {
  --gt-edge-color: var(--gt-danger);
  --gt-ring-color: var(--gt-edge-color);
  border-color: var(--gt-edge-color);
  border-inline-start-color: var(--gt-edge-color);
  background-image: linear-gradient(var(--gt-tint-danger), var(--gt-tint-danger));
}

/* THE QR — the deposit address as a picture, rendered by App\Support\QrSvg.

   RESERVED SPACE: an explicit inline-size AND block-size on the figure
   (hover-skill §5.4 — a surface that resizes when its content arrives is
   layout shift), and the SVG fills it. 11rem is the size at which a phone
   camera across a desk reads a version-3 code off a laptop screen; it is a
   layout fact and it is in rem so it scales with the reader's text.

   🔴 THE GROUND DOES NOT FOLLOW THE THEME. A QR wants dark modules on a
   light ground whatever the page is doing, so the figure paints
   `--gt-on-ink` (the light on-artwork ink, pinned in both themes) under
   `--gt-on-accent-ink` (the dark one) — the two theme-independent inks the
   emitter already defines for exactly this need, and no new colour. The
   modules take `currentColor`: the SVG carries no `fill` attribute, so the
   colour is decided here and nowhere else. */
/* 🔴 V-35 B3, Omar's D4 — TWO CHANGES, AND THE MARGIN IS THE THIRD.

   IT SITS BESIDE THE ADDRESS NOW, not above it (the row is site.css's
   `.got-pay__transfer`), so the block margin that separated it from the label
   underneath has nothing left to separate and is gone: the row's own `gap`
   places it. Leaving it would have been a second answer to one space.

   IT IS 13rem RATHER THAN 11. 11rem was chosen as the size a phone camera
   reads a version-3 code off a laptop screen across a desk, and that floor is
   unchanged and still cleared. What moved is the composition: standing beside
   a 51.6px field instead of above it, an 11rem square read as a small white
   tile rather than as the thing you point a phone at. It is still a layout
   fact and still in rem, so it scales with the reader's text.

   AND IT HAS AN EDGE NOW. The ground is `--gt-on-ink` — a near-white pinned
   in BOTH themes, because a QR wants dark modules on a light ground whatever
   the page is doing (below). On a LIGHT page that puts a near-white square on
   a near-white card: MEASURED on the rendered page at 1440, the QR's ground
   is rgb(229,231,235) on a card of rgb(255,255,255) — 1.24:1, which is no
   boundary at all. The dark page has the opposite problem and no problem: the
   same ground on rgb(23,27,40) is 13.86:1.

   🔴 IT IS `--gt-line-strong` AND NOT `--gt-line`, AND THE FIRST VERSION OF
   THIS RULE HAD IT WRONG. `--gt-line` measured 1.29:1 against the light card
   and about 1.04:1 against the QR's own ground — a hairline written to fix an
   invisible boundary, itself invisible, with a comment claiming it worked.
   `--gt-line-strong` measures 4.33:1 on the light card and 3.66:1 on the dark
   one, which clears the 3:1 a non-text boundary is held to, and it is the
   same token the address field beside it draws its own edge with. Nothing
   here names a colour. */
.got-pay__qr {
  display: block;
  box-sizing: border-box;
  inline-size: 13rem;
  block-size: 13rem;
  margin: 0;
  padding: var(--gt-space-3);
  border: var(--gt-border-hair) solid var(--gt-line-strong);
  border-radius: var(--gt-radius-md);
  background: var(--gt-on-ink);
  color: var(--gt-on-accent-ink);
}
svg.got-pay__qrsvg {
  display: block;
  inline-size: 100%;
  block-size: 100%;
}
path.got-pay__qrsvg {
  fill: currentColor;
}

/* The unit beside the figure in the amount box — `11.50 USDT` (ruling 11).
   Smaller and muted so the figure stays the thing a person reads across a
   room; the box itself is site.css's `.got-pay__amount`, untouched.

   🔴 V-35 B3, Omar's D4: IT WAS FIGHTING THE FIGURE. Measured at 1440 before
   this change, the pair rendered 28px/700 beside 16px/600 — near enough in
   size to read as a second value and far enough in weight to read as an
   accident, in a 654px box holding 127.6px of ink. Two things settle it and
   neither is a colour change: the box is `fit-content` now (site.css's
   `.got-pay__field`), so the pair is a tight unit rather than two marks
   adrift in a wide panel; and the unit is re-cast as a UNIT — one step
   smaller, the label tracking, the label weight — so it reads as the thing
   after the number rather than as a word competing with it. Its colour and
   its family are untouched: `--gt-ink-muted` on the box's own fill was
   already measured, and the family is the amount box's monospace, inherited,
   which is what keeps the two on one rhythm.

   THE SPACE BEFORE IT IS IN THE MARKUP, NOT HERE. `CryptoPayPageTest` pins
   the two `<span>`s with exactly one space between them; a margin here would
   be a second, invisible answer to the same gap. */
.got-pay__unit {
  font-size: var(--gt-text-sm);
  font-weight: var(--gt-weight-bold);
  letter-spacing: var(--gt-tracking-label);
  color: var(--gt-ink-muted);
}
.got-pay__next {
  margin-block-start: var(--gt-space-6);
}
