/* The MaMills landing page, both languages. This file used to be a 607-line
   <style> block inlined in index.html and copied verbatim into hu/index.html —
   the largest single piece of duplication in the repository, and the one most
   likely to drift, because a change had to be made twice by hand.

   The repo rule "no build step, no framework" is intact: this is one static
   file served as-is. Being external also lets the CSP drop 'unsafe-inline'
   for styles. Font URLs are relative to THIS file, not to the page. */

  /* ---- self-hosted faces: the page makes zero external requests ----
     Both files are subsets: Google Fonts' latin range plus Ő ő Ű ű and the
     double acute, added 2026-09-06 because Hungarian cannot be set without
     them and a missing glyph is drawn by the fallback face, mid-word. Any
     re-cut has to keep them; tests/fonts.spec.ts checks, DESIGN.md has the
     recipe. */
  @font-face{
    font-family:'Inter'; font-style:normal; font-weight:100 900; font-display:swap;
    src:url('fonts/Inter-var.woff2') format('woff2');
  }
  @font-face{
    font-family:'Libre Baskerville'; font-style:normal; font-weight:400; font-display:swap;
    src:url('fonts/LibreBaskerville-400.woff2') format('woff2');
  }

  /* ---- metric-matched fallbacks -------------------------------------------
     The bug these fix, together with the `swap` above: this serif used to be
     `font-display:optional`, which gives a font a ~100ms block period and NO
     swap period. Miss that window and the browser renders the whole page in
     the fallback and holds the downloaded file for the *next* navigation - so
     the serif appeared on some loads and not others, and the headings and
     numerals changed size every few reloads. Measured at the boundary it was
     one load in three; past 150ms of font latency it never appeared at all,
     which no amount of preloading can fix. `swap` makes the final state the
     same on every load.

     What `swap` costs is a repaint when the font arrives, and these two faces
     are what stop that repaint from moving the page: the fallback is given the
     webfont's own vertical metrics, so line boxes are identical and nothing
     below shifts. Measured, the document is the same height either way, to the
     pixel. Preloading the file (both landing pages) keeps the swap early.

     Ratios measured in the DOM at 100px by tests/fonts.spec.ts - not with
     canvas TextMetrics, which will silently measure the default serif if the
     webfont is not fully loaded into the canvas context and gave a ratio that
     was inverted:
       Libre Baskerville  width 1697.91  ascent 97  descent 27
       Georgia            width 1480.72
       Times New Roman    width 1355.19
       Inter              width 1575.78  ascent 97  descent 24
       Arial / Helvetica  width 1495.17
     size-adjust      = webfont width / fallback width
     ascent-override  = webfont ascent / size-adjust   (and likewise descent)

     Two serif faces because the adjustment is per fallback font: Georgia is on
     macOS and Windows, Times covers most of the rest. Where neither exists the
     face fails to match and the plain stack below it applies, unadjusted -
     the behaviour this whole block replaces. */
  @font-face{
    font-family:'Libre Baskerville Fallback';
    src:local('Georgia');
    /* No size-adjust on purpose. Measured element by element on this page,
       Georgia is already within 2.5% of Libre Baskerville for lowercase prose
       - the twelve headings run 0.98 to 1.03 - and the only real width
       outliers are DIGITS, because Georgia sets old-style figures: "01" is
       11% narrower, "76" is 7%. Scaling the face to fix four numerals would
       push every heading 6% wide, which is the more visible error.
       The heights are the part that is uniformly wrong - Libre's line boxes
       run 7 to 13% taller than Georgia's - and that is what these overrides
       fix. Vertical is where a font swap shows as "the text changed size",
       because it moves everything below it. */
    ascent-override:97%; descent-override:27%; line-gap-override:0%;
  }
  @font-face{
    font-family:'Libre Baskerville Fallback Times';
    src:local('Times New Roman'), local('Liberation Serif'), local('Tinos');
    /* Times is the long tail - Linux and Android, where Georgia is absent. It
       is genuinely narrower than Georgia, so this one does carry a width
       adjustment, derived from the same measurements. */
    size-adjust:109.26%; ascent-override:88.78%; descent-override:24.71%; line-gap-override:0%;
  }
  @font-face{
    font-family:'Inter Fallback';
    /* The same two-platform problem the serif has, and the same answer. Segoe
       UI, Helvetica Neue and Arial cover Windows and macOS; none of the three
       exists by name on Linux or ChromeOS, where the face used to fail to
       match and the unadjusted stack below applied. Liberation Sans and Arimo
       are metric-compatible with Arial, so the one adjustment derived from
       Arial is correct for all of them - unlike the default there, DejaVu
       Sans, which is wider and is deliberately not listed. */
    src:local('Segoe UI'), local('Helvetica Neue'), local('Arial'),
        local('Liberation Sans'), local('Arimo');
    size-adjust:105.39%; ascent-override:92.04%; descent-override:22.77%; line-gap-override:0%;
  }
  /* Fraunces used to be declared here for the wordmark. The wordmark is
     mamills-wordmark.png — the <h1> is three images — so the face was
     downloaded and never used by a single rule. The file stays in assets/ as
     the source of that lettering; the declaration is gone. */

  :root{
    --paper:#FDF6E3;          /* page background (cream) */
    /* Two neutral grounds and one dark one. That is the whole set.
       The page used to carry four more — paper with 7 to 20% of a game colour
       mixed in, one per band — and they were the loudest thing on it: an 18%
       red wash behind the rules and a 20% gold behind the ways to play, each
       several hundred square centimetres of saturated ground standing behind
       an argument that had not been made yet. They also set the page's ink
       budget. --brass-ink is the tightest ink here, and it was cut darker
       twice for no other reason than to survive those tints; a 20% ground
       costs about 0.9 of contrast ratio, so every band was quietly taxing
       every word printed on it.
       The palette earns more as a mark than as a ground, which is what the
       --accent system below spends it on. These two neutrals are 1.06:1 apart
       — enough to step, far too little to shout — and they alternate down the
       page so the bands still have a rhythm without any of them raising its
       voice. */
    --band-warm:#F7EDD4;      /* the alternate cream: chairs, ways, court, dedication */
    /* Four inks. Six tokens (--head, --serif-ink, --card-ink, --stat and two
       more) bought three perceptible levels: --ink and --head differed by ~2
       L*, --card-ink and --stat by ~4. The names below are what the page now
       uses; the old ones are gone. */
    --ink:#221105;            /* body copy AND every heading */
    --muted:#5A4A33;          /* sub-lines, captions, secondary card copy */
    --faint:#6B5B45;          /* notes and the smallest print only */
    --btn:#3A1D0C;            /* CTA, skip link, play button */
    --stamp-red:#9E2D24;      /* the "Soon" stamp; 6.82:1 on --paper */
    --on-dark:#FFF9EC;
    --brass:#B08A4A;          /* step numbers, display sizes only: 2.74:1 */
    /* Small-text brass. #8A6A2E cleared 4.5:1 on --paper (4.66) but not on
       the tinted bands (4.31 warm, 4.21 cool), which is exactly where the step
       numbers sit — axe-core caught it at 4.3.
       Darkened again when the band tints deepened: #88632B measured 3.99 on
       the 18% red ground, so it set the ceiling on how much colour the page
       could carry. #7C5926 is the same brass one step down in lightness and
       clears every ground the page has — 5.87 paper, 5.44 warm, 4.66 red 18%,
       5.28 gold 20%. It is the binding ink, so it is what a deeper tint
       breaks first. */
    --brass-ink:#7C5926;
    --hair-a:#E7DBBE; --hair-b:#E2D6BC; --hair-c:#E9E1CD; --hair-d:#E0D4B8;
    /* The focus ring is the game's Wong blue, the same #017AB9 the
       colour-blind safe palette uses in the screenshots below. It was the last
       colour on the page not sampled from the game. Clears 3:1 on every ground
       the site has - 4.34 on paper, 3.92 at worst on a tint, and 3.69 on the
       dark band, where the old ring managed 3.44. */
    --focus:#017AB9;
    --band-dark:#2A1608;      /* the page's only dark ground */
    /* --brass-lit (#C9A24D) is gone: the dark band's accents are the game's
       own gold now, which clears 9.18:1 there against the lightened brass's
       6.0. One colour for the heading, the links and the CTA. */

    /* ---- the game's own palettes -------------------------------------------
       Sampled from the screenshots this page already shows: the classic red
       and blue, the violet pair, Gold & Silver, and the colour-blind safe blue.
       At full strength none of them is text on cream - red is 3.90:1, blue
       3.48, gold 1.74 - so they are used as edges, rules and figure borders,
       where the 3:1 non-text line applies, and never as body copy. */
    --game-red:#D74C3F;       /* 3.90 on paper, 4.11 on the dark band */
    --game-blue:#5387C0;      /* 3.48 on paper */
    --game-violet:#9874E0;    /* 3.29 on paper */
    --game-access:#017AB9;    /* 4.34 on paper - the Wong blue */
    --game-gold:#E0B93A;      /* 1.74 on paper, 9.18 on the dark band */
    /* A --game-*-ink is the game's colour darkened until it clears its bar on
       the TINTED grounds, not merely on paper: a 7% tint costs about 0.35 of
       ratio, so a value that measures 4.5 on cream measures about 4.15 on the
       band it has to sit on. Every one of these carries the ground it was
       checked against. */
    --game-red-ink:#AC2F24;   /* 6.11 on paper, 4.85 on the 18% red band.
                                 Re-derived when that band deepened: #C43629
                                 cleared 4.56 on the old 7% ground and 3.95 on
                                 the new one. An -ink is only valid for the
                                 ground it was cut against, so moving a ground
                                 means re-cutting every ink that lands on it. */
    --game-blue-ink:#41699A;  /* 5.24 on paper, 4.74 on the worst tinted ground */
    --game-gold-ink:#9A7F28;  /* the gold darkened until it clears 3:1 on the
                                 tinted grounds too, not only on plain cream:
                                 #A88A2B managed 3.07 on paper but 2.84 on the
                                 gold band it has to sit on */

  }
  *{box-sizing:border-box;}
  /* Selection is the one piece of colour that costs no space on the page: it
     exists only while someone is reading closely. Warm rather than blue, so it
     reads as this page rather than as the browser default.
     BOTH halves are set on purpose. With a background alone the selected text
     keeps whatever ink it already had, and this page has inks from --ink at
     16.9:1 down to --faint - over the red that lands as low as 1.7:1, so
     selecting a caption would erase it. The pinned pair is 4.98:1 wherever it
     appears, including on the dark band. */
  /* Longhand on purpose: a `background:` shorthand carrying a var() reads back
     from the CSSOM as an empty string, so the rule cannot be inspected - and
     the shorthand would reset background-image along with it. */
  ::selection{background-color:var(--game-red-ink); color:var(--paper);}
  /* WCAG 2.4.11 Focus Not Obscured. The dock is fixed to the bottom, so when
     the browser scrolls a newly focused element into view it lands underneath
     it — tabbing into the footer links used to put them completely behind the
     dock. scroll-padding reserves the dock's height for every scroll-into-view
     the browser performs, focus-driven ones included. It matches the
     padding-bottom that body.has-dock reserves at the end of the document. */
  html{scroll-padding-bottom:calc(4.75rem + env(safe-area-inset-bottom));}
  body{
    margin:0; background:var(--paper); color:var(--ink);
    font-family:'Inter', 'Inter Fallback', ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  }
  /* Fixed 920px design composition, scaled as one rigid unit on narrower
     viewports. tan(atan2()) computes the unitless 100vw / 920px ratio (wider
     browser support than plain length/length division); min(1, …) makes it a
     no-op at 920px and above. Browsers without CSS zoom fall back to the
     width=920 viewport meta and scale the page themselves.
     Full-bleed bands inside .page use width:100% rather than 100vw on
     purpose: vw is viewport-relative and would then be multiplied by zoom
     again, leaving cream gutters down both sides of every band. */
  /* Between 820px and 920px the composition is scaled down as one rigid unit;
     below 820px the breakpoint at the bottom of this file takes over and the
     page becomes a single column at zoom 1. The viewport meta is
     width=device-width, so both of those actually apply on a phone. */
  /* overflow-x:clip, not hidden: clip does not make .page a scroll container,
     so nothing inside it can be scrolled sideways by script or by focus, and
     document.scrollWidth stays at the viewport. What it clips is the hero
     plate's spill past the 920 column (see .frame) - the windmill's sail and
     a few feathers - which would otherwise put a horizontal scrollbar on any
     window narrower than about 970px. It is a safety net, not a layout tool:
     the plate is sized so its drawing ends at the window at every width, and
     tests/media-fit.spec.ts fails the page if the clip ever takes ink. The
     mascot is outside .page. */
  .page{zoom:min(1, tan(atan2(100vw, 920px))); overflow-x:clip;}
  .col{width:920px; margin:0 auto; position:relative;}

  /* ---- the loose feathers ------------------------------------------------
     Seven single feathers blown clear of the hero frame, falling away from it
     across the whole page (2026-09-09: "like a burst radius but fewer feathers
     and randomly"). The ones still ON the frame are drawn into
     assets/hero-deco.png; these are cut from the same master by
     tools/scatter_feathers.py --cutouts, so they are the same hand.

     WHY THEY ARE MOSTLY IN THE GUTTER. The 920px column's right half IS the
     frame - the plate's ink runs to x 1210 on a 1440px window, past the column
     itself - so there is no room beside it inside the column. The only strips
     this page can promise are empty, whatever the copy grows into, are the two
     gutters, the 52px gap under .features, and the dark band either side of its
     centred text. "It should not cover anything" was the ask, so that is where
     they are, and every position was checked against the rendered boxes of the
     hero copy, the callouts, the band and the footer, in both languages.

     Two sets, because the gutter is not always there:
       .pf      is inside the 920px column and shows at every width - the three
                under the callouts, on the band and at the foot, so the burst
                does not vanish on a narrower screen.
       .pf-far  is the two beside the hero, out in the gutter, which only exists
                above 1200px. Below that they are display:none rather than
                sliced in half by .page's own overflow-x:clip - a feather cut in
                half by the window edge is a bug this site has had twice, see
                .frame.

     Each is positioned against .col, .features, .band-close or footer rather
     than against the page, so the Hungarian page - where the copy runs longer
     and every absolute page offset would be wrong - places them from its own
     layout. Widths are 22 to 34px, roughly the 0.72 the plate itself draws at,
     smaller the further out they go. Nothing here is content: the containers
     are aria-hidden, every alt is empty, pointer-events:none. */
  /* The container is absolutely positioned, not just its images: .features is
     a two-column grid and .ctarow a flex row, and an in-flow <div> in either
     becomes a grid item - which pushed "Accessible by design" onto a second row
     and made .features 106px taller. Out of flow, it cannot. */
  .feathers{position:absolute; inset:0; pointer-events:none;}
  .pf, .pf-far{position:absolute; display:block; pointer-events:none; z-index:0;}
  @media (max-width:1199px){ .pf-far{display:none;} }

  /* Seven. Three out past the hero - including one in the upper right corner,
     which was the one direction the burst had missed - one under the callouts,
     two on the dark band, one at the foot of the page. Far apart, different
     sizes, no two at the same angle. */
  .pf-1{left:-86px;  top:96px;  width:30px; transform:rotate(-24deg);}
  .pf-2{right:-78px; top:14px;  width:26px; transform:rotate(33deg);}
  .pf-3{right:-92px; top:392px; width:32px; transform:rotate(-37deg);}
  .pf-4{left:118px;  top:74px;  width:28px; transform:rotate(19deg);}
  .pf-5{right:86px;  top:118px; width:22px; transform:rotate(28deg);}
  .pf-6{left:104px;  top:286px; width:24px; transform:rotate(-14deg);}
  .pf-7{left:302px;  top:-36px; width:22px; transform:rotate(7deg);}

  /* ---- the "Soon" stamp, gone -------------------------------------------
     From 2026-09-09 to 2026-09-12 the Steam page was not live, every Wishlist
     control was a disabled <button> and a red .soon stamp hung off its corner.
     The store opened on 2026-09-12; the controls are links again and the
     stamp's rules went with it. DESIGN.md keeps the record. */

  a.skip{
    position:absolute; left:50%; top:8px; transform:translate(-50%,-160%); z-index:20;
    background:var(--btn); color:var(--on-dark);
    font:700 13px/1 'Inter', 'Inter Fallback', sans-serif; padding:11px 18px;
    border-radius:999px; text-decoration:none; transition:transform .12s;
  }
  a.skip:focus{transform:translate(-50%,0);}
  a:focus-visible, [tabindex]:focus-visible, button:focus-visible{
    outline:3px solid var(--focus); outline-offset:3px; border-radius:4px;
  }
  /* The ring flips lightness with its ground, which is how a ring is supposed
     to behave: #017AB9 is 3.69 on the dark ground - legal, but the hardest
     colour on the page to see, and now on two surfaces rather than one.
     #3A9BD9 is the same hue at 5.65. Only the value changes; the width, the
     offset and the hue are constant, so this is one colour per ground rather
     than two colours for one state. */
  .band-close{--focus:#3A9BD9;}

  /* ---- studio lockup: still used by the footer mark ---- */
  .lockup{display:flex; align-items:center; justify-content:center; gap:14px;}
  .lockup img{width:26px; height:26px; flex:none; display:block;}
  .lockup img.mirror{transform:scaleX(-1);}
  /* The footer's copy of the lockup is an <h2> since 2026-09-11 - the one
     heading on the page that names the studio - and keeps the mark's own
     size rather than a heading's. */
  .lockup b, .lockup h2{
    display:block; margin:0; font-weight:700; font-size:15px; line-height:1.06;
    color:var(--ink); text-align:center;
  }
  /* Who and what, under the mark: a step up from the 11px legal lines below
     so it reads as the studio speaking rather than as a notice. The measure
     is in px, not ch, on purpose: `ch` is the width of "0" in the face that
     actually renders, and it came out 492px in Inter against 457px in the
     size-adjusted fallback - three lines in one, four in the other, 20px of
     drift the moment the webfont fails (tests/fonts.spec.ts caught it on
     2026-09-11). The text is 1358px long in both faces; at 520px it wraps
     to the same three lines in each, and it takes 460px or 680px to change
     that count, so both edges are 60px away. Re-measure if the copy changes. */
  .studioline{
    max-width:520px; margin:14px auto 0; text-align:center;
    font-size:13px; line-height:20px; color:#3E2B14;
  }
  /* The game's name in the studio sentence links to its page (2026-09-14): the
     page's own ink, underlined, not the browser's blue. */
  .studioline a{color:#26170A; text-decoration:underline; text-underline-offset:2px;}

  /* ---- the header ----------------------------------------------------------
     Two columns above 820px: the words take a left edge, the game takes the
     right half. What this replaced was eight centred blocks — a duck lockup,
     "proudly presents" at 19px, a wordmark flanked by two board marks, and
     five more centred lines — where nothing led and the board appeared twice
     within 200px. The bookend marks are gone; the wordmark stands alone. ---- */
  .hero{display:flex; flex-direction:column; align-items:center; padding-top:20px;}
  /* Words left, frame right. The text column is cut to the width the
     wordmark actually needs so the frame's cell can take 600 of the 920; the
     plate itself overruns that cell now, see .frame. It is not narrower than
     this: at 280px the
     serif lead wraps to a different line count in the fallback face, and
     tests/fonts.spec.ts fails the page for reflowing when a webfont is lost. */
  /* start, not center: the words are anchored to the top of the page, and the
     plate is placed against that top rather than against a centred column.
     Identical to `center` while the text column is the taller of the two,
     which it is at every width - but only `start` stays true if the plate
     ever grows past it. */
  .herogrid{
    display:grid; grid-template-columns:300px 600px; gap:20px;
    align-items:start; width:920px;
  }
  .herotext{text-align:left;}
  .studio{
    display:flex; align-items:center; gap:8px; margin:0 0 18px;
    font-size:13px; font-weight:700; color:var(--ink);
  }
  .studio img{display:block;}
  .studio span{font-weight:400; color:var(--muted);}
  h1{
    position:relative; z-index:3; margin:0 0 18px; line-height:0;
    display:block;
  }
  h1 .wordmark{height:64px; width:auto; display:block;}
  /* Stretched to the row so the absolutely placed plate below measures from
     the row's top, whatever height the text column has in either language.
     The plate is out of flow and cannot hold the row open itself, so if it is
     ever moved far enough down to end below the text column, this cell needs a
     min-height or the two feature callouts will sit under the windmill. As
     placed it ends at 429 against the column's 474, so it does not.
     tests/media-fit.spec.ts is what says so. */
  .heromedia{position:relative; align-self:stretch;}

  /* The frame artwork and the trailer are siblings. The trailer is placed in
     percentages of the frame box so the two can never drift: measured from
     hero-deco.png at its native 700x388 the opening is x 145-521, y 122-329
     (376x207) — it sits 17px LEFT of the artwork's horizontal centre, so the
     x offset is NOT (700-376)/2. The artwork's opening is keyed transparent,
     so the video below shows through it. */
  /* The plate used to be pulled up under the title - margin-top:-48px, then
     top:-116px - and both are gone. Kept as a warning, because the number the
     first of them carried was measured wrong: the feather was read at x
     525-558 in hero-deco.png's own 700px-wide coordinate space and compared
     against the 920px column as if the two were the same ruler. There is no
     "72px of headroom" invariant, and nothing here is safe to nudge from the
     comment alone. Re-measure both boxes before changing any of it. */
  /* The plate is at most 840x465.62 - its own 1160x643 ratio, so the
     percentages below still land on the drawn frame, and it narrows with the
     window below 1103px so the windmill stays on screen, see the clamp on
     --plate-w - and it sits out of flow, absolutely
     placed against the stretched .heromedia, so the row keeps the text
     column's height. It is bigger than its 600px cell on purpose (2026-09-06:
     "make the trailer bigger, feathers may overlap other elements"), and the
     two offsets say exactly what it overlaps. left:-60px puts the plate's
     left edge at x=260 of the 920 column: the ducks start 15% in, at 386,
     clear of the 300px text column, and only the bottom-left feathers (1.5 to
     3.3% in, at 273-306) cross into it, at heights where the platform and
     language lines have already ended and above the first button; on the
     right the windmill's sail ends at 1011, 91px past the column, and .page
     clips that spill at the viewport rather than growing a scrollbar. Until
     2026-09-09 the plate was 840px at every width above 820 and the clip took
     the sail from about 1100px down and the whole mill by 920px; the width is
     a clamp now, so the spill ends at the window instead of inside the
     drawing. The opening is 455x252 at the full size, against 379x210 in the
     700px frame it grew back from on 2026-09-08.

     Four feather clusters used to float out to x 1010 and beyond on the
     right, which at 840 put them under the window's edge on anything narrower
     than about 1090px - a feather sliced in half by the viewport. They were
     re-composited into the drawing itself (see DESIGN.md), so the sail is now
     the rightmost ink and the plate spills no further than it did at 700.

     The vertical offset lines the DRAWN FRAME'S TOP LINE up with the bottom of
     the wordmark (2026-09-06: "align the trailer, the frame's border should
     start after the height of the MaMills header text"). Neither number is a
     nudge. 100px is where the wordmark ends, measured from .heromedia's top:
     the 18px studio line, its 18px margin, and the 64px wordmark - the same in
     both languages. 136.83px is where the drawn line starts inside the plate:
     on the 1160x643 master the frame's top line runs y 189-202, 14px thick,
     and 189/643 of 465.62 is 136.86. The difference puts the plate's own box
     17px above the page, which is transparent paper - the topmost ink is 43px
     down - and the duck heads start at 22.7%, at page y 89.

     It was top:-116px until this change (the plate 96px up, both duck heads
     cropped against the top of the window) and top:100px for one revision,
     which aligned the plate's transparent BOX with the wordmark and so hung
     the drawn frame 137px lower than asked. Measure the line, not the box. */
  /* The plate's own geometry, named once, so a second hand-copied number is
     not a second thing to get wrong the next time the plate is resized.
     --plate-line is where the drawn frame's top line sits inside the art
     (189/643 of the height), which is what the plate is hung from. */
  /* The height and the line follow the width now, instead of being three
     hand-copied numbers: 1160/643 is the art's ratio, so h = w / 1.804, and
     the line is 189/643 = 29.394% of that height. At the full 840px they are
     the 465.62 and 136.86 that used to be written out here.
     WHY THE WIDTH IS FLUID. The plate is anchored 60px left of its grid cell
     and the cell sits at x 320 of the centred 920 column, so the drawing's
     right edge lands at (100vw - 920)/2 + 320 - 60 + 0.89483 * w, where
     0.89483 is where the rightmost ink sits in the art (x 1037 of 1160,
     measured off the alpha channel; the leftmost is x 66, or 5.69%). Past
     that the windmill runs into .page's overflow-x:clip and loses its sail -
     from about 1100px down, and by 920px the whole mill is gone. Solving that
     edge for the window gives w <= 55.88vw + 223.5px; the 215 is that minus
     8px, so the sail's tip ends about 7px inside the window rather than on
     it. The 728px floor is the 820-919px zoom band, where .page scales a
     layout that is always 920px wide, so no vw expression can describe it:
     0.5588 * 920 + 215 = 729. The 840px ceiling is the size the plate was
     drawn for and keeps above 1119px. */
  .heromedia{
    --plate-w:clamp(728px, 55.88vw + 215px, 840px); --plate-left:-60px;
    --plate-h:calc(var(--plate-w) / 1.804); --plate-line:calc(var(--plate-h) * .29394);
    --plate-top:calc(100px - var(--plate-line));
    /* The opening's centre, the same 47.59% .playaff is placed on. The art is
       wider on the right, so this is 17px left of the plate's own centre. */
    --hole-centre:.4759;
  }
  .frame{position:absolute; left:var(--plate-left); top:var(--plate-top); width:var(--plate-w); height:var(--plate-h); z-index:2;}
  /* ---- the screen inside the duck frame ---------------------------------
     The picture fills the frame. On the 1160x643 master the drawn frame's
     inner edge encloses x 241-862, y 203-544 (622x342, 1.82:1), and that
     interior is keyed fully transparent; only the fringe of the drawing -
     the mallard's foot top-left, the white duck's feet top-right, the
     duckling and the windmill along the bottom, a few feathers - reaches
     back over it, 1.9% of the opening in all, at most 44px in from the left,
     18 from the right, 40 from the top and 37 from the bottom on the master.

     The video takes that interior plus a 3px master bleed on every side
     (238-865 x 200-547, 628x348), so its edge runs under the frame's own
     13px line and the line's anti-aliased inner edge never shows paper. The
     drawn frame is the bezel, so there is no ring and no radius: a rounded
     corner in a square opening is a cream notch. object-fit:cover crops 1.5%
     of the 16:9 picture to the 1.80:1 hole, inside the 3% tests/media-fit
     allows and documents. The fringe sits on the picture's edges by design -
     that is what the art was keyed for - and .deco keeps pointer-events:none
     so it never shadows the controls beneath it.

     Until 2026-09-06 the video was inset 11% inside the hole, behind a dark
     bezel, to keep the fringe off the HUD panels at the edges of the 1280x720
     capture; the frame then read as a picture floating in a cream gap, and
     the fit was asked for instead. Re-measure the alpha before changing any
     of this; the artwork is the source of the numbers, not taste. */
  .frame video{
    position:absolute; left:20.52%; top:31.10%; width:54.14%; height:54.12%;
    object-fit:cover; display:block; background:#5A402A; z-index:0;
  }
  /* The drawing is the top layer of the frame, above the picture and above the
     play affordance. Every layer inside .frame now names its z-index rather
     than leaning on document order: the poster and the video are 0, .playaff
     is 2, and the plate is 3, so the mallard's foot, the white duck's feet and
     the windmill - the fringe that reaches back over the opening - stay drawn
     over the picture whatever the video element does to its own compositing
     layer while it plays. It costs nothing: the opening is keyed fully
     transparent and .deco is pointer-events:none, so the button under it is
     neither hidden nor shadowed. tests/media-fit.spec.ts holds the order. */
  .frame .deco{position:absolute; inset:0; width:100%; height:100%; z-index:3; display:block; pointer-events:none;}
  /* The poster is markup, not the <video poster> attribute: that attribute is
     always eager, takes no <picture> and no loading="lazy", and was serving
     140KB of JPEG — more than the fonts — one of it a 4x oversample painted
     into a 312x167 hole. As markup it gets WebP, lazy-loading below the fold,
     and a fetchpriority. site.js removes it when the video starts. */
  /* Same rectangle as the video, to the decimal: the poster stands in for it
     until playback starts, so any drift here would show as the picture jumping
     when the trailer begins. */
  .frame .poster{position:absolute; left:20.52%; top:31.10%; width:54.14%; height:54.12%; z-index:0;}
  .frame .poster img{width:100%; height:100%; object-fit:cover; display:block;}
  .poster[hidden]{display:none;}
  .playaff{
    position:absolute; left:47.59%; top:58.17%; transform:translate(-50%,-50%); z-index:2;
    display:flex; align-items:center; justify-content:center;
    background:none; border:0; padding:0; cursor:pointer; font:inherit; color:inherit;
  }
  .playaff .knob{
    width:44px; height:44px; border-radius:50%; background:var(--btn);
    display:flex; align-items:center; justify-content:center;
    box-shadow:0 2px 6px rgba(26,26,31,.45);
  }
  .playaff .knob::after{
    content:""; width:0; height:0; margin-left:4px;
    border-left:13px solid var(--on-dark);
    border-top:8px solid transparent; border-bottom:8px solid transparent;
  }
  /* The class above sets display:flex, which outranks the user-agent rule for
     [hidden]; without this the affordance stays on screen while the trailer
     plays. */
  .playaff[hidden]{display:none;}
  .playaff .knob{transition:transform .15s ease, box-shadow .15s ease;}
  .playaff:hover .knob{transform:scale(1.06);}
  /* Over video, one ring can never be guaranteed 3:1. A light ring inside a
     dark one always clears it, whatever frame is underneath. */
  .playaff:focus-visible{
    outline:3px solid var(--on-dark); outline-offset:4px; border-radius:8px;
    box-shadow:0 0 0 7px var(--btn);
  }

  /* WCAG 1.2.2 / 1.2.3: both videos carry a caption track, and this is the
     text alternative — what the video shows, for anyone who will not or cannot
     play it.

     It used to be a hairline-ruled disclosure sitting under the frame, opening
     with a bare "+ 46 seconds", which read as a stray note rather than as part
     of the player. It is the same native <details> — no script, no new focus
     behaviour — but the summary is now a chip ON the video and the description
     opens over the frame. The chip sits top-right because with JavaScript off
     the <video> keeps its native controls, and those live along the bottom.

     The offsets are the frame's own percentages, the same ones .frame video is
     placed with: 20.52% + 54.14% = 74.66% is the right edge of the screen, so
     the chip is inset from 25.34%. The extra 10px keeps it off the frame line
     and the white duck's feet, which reach 9px into the top-right corner at
     600px. Pixels alone would drift the moment the frame goes fluid below 820px. */
  .mediaalt{margin:0;}
  .mediaalt > summary{
    position:absolute; z-index:4; list-style:none; cursor:pointer;
    display:inline-flex; align-items:center; gap:6px;
    height:28px; padding:0 11px; border-radius:999px;
    background:rgba(26,22,8,.78); color:var(--on-dark);
    font-size:12px; font-weight:700; line-height:1; white-space:nowrap;
  }
  .mediaalt > summary::-webkit-details-marker{display:none;}
  /* An information mark, not a "+": the control opens a description, and the
     disclosure triangle it replaces said nothing about what is inside.
     It was a CSS circle holding the letter "i" until 2026-09-10 - a 1.5px
     border straddling two device pixels, and a serif glyph centred by hand.
     Two 18px symbols from the sprite instead: whole-pixel geometry, one shape
     per state, and no font in the loop. */
  .altmark{width:18px; height:18px; flex:none; display:block;}
  .mediaalt[open] .altmark-i,
  .mediaalt:not([open]) .altmark-x{display:none;}
  .mediaalt > summary:hover{background:rgba(26,22,8,.92);}
  .frame .mediaalt > summary{right:calc(25.34% + 10px); top:calc(31.10% + 10px);}
  /* While the description is open the panel covers the play knob, and a
     covered control must not keep a place in the Tab order (2.4.11, 2.4.7):
     visibility takes it out of both the paint and the focus order. */
  .frame:has(> .mediaalt[open]) > .playaff{visibility:hidden;}
  /* Over video no single ring can be guaranteed 3:1, the same problem
     .playaff has, so it takes the same two-ring answer. */
  .mediaalt > summary:focus-visible{
    outline:3px solid var(--on-dark); outline-offset:2px; border-radius:999px;
    box-shadow:0 0 0 6px var(--btn);
  }
  /* The panel covers the opening, not the whole plate: the plate is 840px
     wide and runs past the page edge on the right and 28px into the text
     column on the left, so "the whole frame" would be a dark slab hanging off
     both. The opening is
     455x252 - more than the 520x288 the whole-frame panel used to have once
     its padding is taken off - and the chip keeps its closed position in the
     opening's top-right corner; the 48px top padding is the room it needs. */
  .altbody{
    position:absolute; inset:31.10% 25.34% 14.78% 20.52%; z-index:3; overflow:auto;
    padding:48px 18px 16px 16px; border-radius:4px;
    background:rgba(26,22,8,.96); color:var(--on-dark);
  }
  /* The description is the text alternative for the whole teaser (1.2.3), so
     it runs to several paragraphs and the panel scrolls. Paragraphs need the
     gap: without it the beats of the video read as one block of prose. */
  .altbody p{margin:0; font-size:12px; line-height:18px; color:rgba(255,249,236,.92);}
  .altbody p + p{margin-top:8px;}
  /* A scrollable region has to be reachable by keyboard (2.1.1); the tabindex
     in the markup does that, and this is its focus ring. */
  .altbody:focus-visible{outline:3px solid var(--on-dark); outline-offset:-3px;}

  /* ---- primary call to action, used in the hero and reusable elsewhere ---- */
  /* The width was fixed at 244px, which silently clipped the Hungarian label
     ("Kívánságlistára a Steamen" is 8 characters longer than "Wishlist on
     Steam"). min-width keeps the English button pixel-identical to the handoff
     and lets any other language grow the button instead of losing its text. */
  .pill{
    /* border:0 and font:inherit are here rather than on a button.pill rule:
       the forced-colours block below needs to put a border back, and a
       type-plus-class selector would outrank it. */
    border:0; font-family:inherit;
    position:relative;
    min-width:244px; height:52px; border-radius:999px;
    display:flex; align-items:center; padding:0 22px 0 14px; gap:17px;
    background:var(--btn); color:var(--on-dark); text-decoration:none;
    font-size:15px; font-weight:700; letter-spacing:.01em; white-space:nowrap;
    box-shadow:0 2px 3px rgba(42,33,24,.35);
    transition:transform .08s, box-shadow .08s;
  }
  .pill:hover{transform:translateY(1px); box-shadow:0 1px 2px rgba(42,33,24,.35);}
  /* The one moment of the game's gold above the fold, and it costs no space at
     rest: the Steam roundel is cream until the pointer is on the button, then
     it is the colour the game gives you for beating the Machine on Expert. The
     mark inside stays --btn, which is 11.4:1 on that gold. Scoped to the hero
     because the closing ask is already a gold pill on the dark band, where the
     roundel is the dark disc and inverting it would erase the mark. */
  .hero .pill:hover .disc{background:var(--game-gold);}
  /* Steam Next Fest, October 19 to 26, 2026 (added 2026-09-19): the one Next
     Fest line on this page, in the developer's words. Take it down after the
     fest; tools/check.sh reminds from October 27. */
  .band-close .band-fest{
    /* Wider than the paragraph above it (--band-sub is 620px): at 1280 the
       sentence fits one line in Libre Baskerville and wrapped to two in the
       fallback face, which moved the page 21px when the webfont failed and
       broke tests/fonts.spec.ts. 46rem holds it on one line in both. */
    max-width:46rem; margin-top:10px; color:var(--on-dark); font-weight:600;
  }
  .pill .disc{
    width:37px; height:37px; border-radius:50%; background:#FCFBF6; flex:none;
    display:flex; align-items:center; justify-content:center;
  }
  .pill .disc svg{width:22px; height:22px; display:block; color:var(--btn);}
  /* In a 360px column the two asks cannot share a line, so they stack and
     match widths: a ragged pair of different-width pills reads as an accident,
     a matched pair reads as a decision. */
  /* z-index:3, the same layer the <h1> takes, because the plate reaches 28px
     into this column at the pill's height: a feather painted across the right
     end of a dark 52px CTA reads as a smudge on the button rather than as
     part of the drawing. Above it, the feather passes behind the pill and the
     button stays a clean shape. The plate is z-index:2 and .deco is
     pointer-events:none, so nothing about the click target changes. */
  .hero .ctarow{
    display:flex; flex-direction:column; align-items:stretch; gap:12px; margin-top:26px;
    position:relative; z-index:3;
  }
  /* Both asks centre their label. The pill used to sit at flex-start while
     the ghost beside it was centred, so the two buttons — identical in width,
     stacked, 12px apart — disagreed about where their text began. */
  .hero .ctarow .pill{width:100%; justify-content:center;}
  .hero .ctarow .ghost{width:100%; justify-content:center;}
  /* The wishlist ask and the game page are the two things a visitor can do in
     the first screen. The secondary one used to be a JS-only button that
     started the video — a second control for a player that already carries its
     own play knob, hidden entirely without JS. It is a link now, so it works
     with nothing running. */
  /* 2px, not a hairline: this button carries no fill, so its edge is the whole
     control. At 1px it read as a disabled twin of the filled pill beside it.
     The ink is the same brown the label is, so the outline is the label's own
     weight drawn round it. box-sizing keeps the 52px height. */
  .ghost{
    display:flex; align-items:center; height:52px; padding:0 21px;
    border:2px solid var(--btn); border-radius:999px;
    color:var(--btn); text-decoration:none; font:700 15px/1 'Inter', 'Inter Fallback', sans-serif;
    white-space:nowrap; background:none; cursor:pointer;
  }
  .ghost:hover{background:var(--btn); color:var(--on-dark);}
  .ghost[hidden]{display:none;}
  /* Where it runs, in the hero rather than under the plate. It carried
     "Coming soon" until 2026-09-10; the platforms moved up from .herospecs
     under the drawing, which is a long way from the eye at the moment it asks
     "can I run this?". A list rather than a dotted run: four items with their
     own marks read as four platforms, and a screen reader counts them.
     The marks are 14px silhouettes in currentColor from the sprite at the top
     of the page, so they take the weight of the text they sit in and cost no
     request - the CSP is default-src 'none'. */
  .herofacts{
    margin:0; padding:0; list-style:none;
    /* The row gap and the 24px line are Valve's clear-space rule, not taste:
       the Steam Deck logomark has to sit in open space at least as wide as its
       own centre circle, which is half the mark's height - 7px at this size.
       12px between rows plus 5px of leading clears it; the 18px column gap and
       the 8px before each label clear it sideways. */
    display:flex; flex-wrap:wrap; gap:12px 18px;
    font-size:15px; line-height:24px; font-weight:700; color:var(--ink);
  }
  .herofacts li{display:inline-flex; align-items:center; gap:8px;}
  /* Optically matched to the 15px label rather than set to it: the Apple and
     Linux silhouettes fill their box, the Windows panes do not, so a shared
     14px box lands them all on the same visual weight. */
  .platmark{width:14px; height:14px; flex:none; display:block;}
  /* ---- feature callouts ---- */
  /* The bottom margin is the hero's, not the next band's: the first band
     opens with a 3px coloured edge, and with no gap here that rule started
     immediately under the accessibility callout's last line while the band's
     own 64px of padding sat empty below it. The air belongs above the rule. */
  /* The callouts run the full 920 column, not the 860 `.inner` the bands use.
     The hero above them starts at the column's own left edge, so an 860px row
     under it sat 30px inboard of everything else and the page had two left
     edges 30px apart — the kind of misalignment a reader feels as untidiness
     without being able to name it. Two equal halves of the column, so the pair
     also splits on the column's centre line. */
  .features{
    width:920px; margin:24px 0 56px; z-index:3; position:relative;
    display:grid; grid-template-columns:1fr 1fr; gap:40px; text-align:left;
  }
  .feature{display:flex; align-items:flex-start; gap:10px;}
  /* One box for both marks — but the box was never the whole problem, and an
     earlier comment here claimed it was. A shared 66x66 with object-fit:contain
     equalises the BOX, not the drawing: the Deck art was near square (content
     371x359) and the accessibility art wide (382x317 inside a 432x328 canvas
     with 43px of dead transparent margin on its right), so contain fitted the
     second one to 58x48 against the first's 64x62 — 28% less optical area, and
     the pair still read as an accident. No CSS can fix that; the aspect ratios
     of the two drawings differ. Both PNGs were re-cut instead: alpha-trimmed,
     scaled to equal optical area (the Deck down 4.6%, so nothing is upscaled)
     and centred on one 400x400 canvas. Each now fills 3.3k square px of the
     box. Re-cut the art, not this rule, if they drift apart again. */
  .feature img{flex:none; display:block; width:66px; height:66px; object-fit:contain;}
  .feature h3{margin:2px 0 3px; font-size:19px; font-weight:800; color:var(--ink); line-height:1.1;}
  .feature p{margin:0; font-size:13px; line-height:17px; color:var(--ink);}

  .lead{
    margin:0 0 10px; text-align:left;
    font-family:'Libre Baskerville', 'Libre Baskerville Fallback', 'Libre Baskerville Fallback Times', ui-serif, Georgia, serif;
    /* 19px, measured, not chosen, and the copy carries all of its own breaks.
       The column is 300px wide, and a line that wraps in one face and not the
       other moves the whole document when the webfont fails to arrive -
       tests/fonts.spec.ts fails on the drift. Until 2026-09-11 the lead was
       20px with one break after "friend -" and left the rest to wrap: that
       put the first line at 297px in Libre Baskerville, and Chromium on the
       Linux runner, which rounds glyph advances differently from the macOS
       build, measured it past 300 and broke it in two - a fourth line, 24px
       of drift, only with the webfont loaded. The other half was as tight
       the other way: 317px in the metric-matched fallback against a 366px
       webfont, so a slightly narrower fallback would have taken it to one
       line. `text-wrap:balance` used to even the lines; with every break
       forced there is nothing left for it to do, so it is gone.
       At 19px the three lines measure 282, 152 and 191px in Libre Baskerville
       and 243, 132 and 166 in the Times fallback: 6% of slack on the longest
       in the worst face, and the same line count in every face on every
       platform. Re-measure whenever this copy changes. It was 20px, 24px,
       27px and 31px for earlier leads. */
    font-size:19px; line-height:1.22; color:var(--ink);
  }
  /* What kind of game this is, directly under what it is — and the one place
     "about five minutes a match" belongs. It used to sit in the facts line,
     between the price and the release quarter, where a reader files it as a
     spec rather than as a promise about how the game feels. The length of a
     match IS the cozy claim; it is not a number about commerce. The line takes
     the game's own tagline so the site and the trailer's end card say the same
     words. */
  /* Who you play, as four short parts rather than one 89-character sentence:
     the same shape as the platform list under it, so the first screen answers
     "who do I play?" and "where does it run?" in the same rhythm. Muted ink and
     a normal weight keep the two apart - the platforms are the bolder line
     because they are the question a reader actually arrives with. */
  .heromodes{
    margin:0 0 16px; padding:0; list-style:none;
    display:flex; flex-wrap:wrap; gap:8px 16px;
    font-size:15px; line-height:22px; color:var(--muted);
  }
  /* A hairline lead-in rather than a bullet: at this size a disc reads as a
     list of chores, and the marks below already carry the visual weight. */
  .heromodes li{display:inline-flex; align-items:center; gap:8px;}
  /* Who you play takes a line each, where you play share one when it fits:
     the two are different questions, and left to itself the flex wrap put
     "Over LAN" up beside the couch and left the Steam option alone underneath.
     Since the Steam option became "over Steam Remote Play Together" (2026-09-13)
     the pair no longer fits on one line at any width, so each takes its own. */
  .heromodes li:nth-child(-n+2){flex-basis:100%;}
  .heromodes li::before{
    content:""; flex:none; width:10px; height:2px; border-radius:1px;
    background:var(--hair-a);
  }

  /* ---- full-bleed bands ---- */
  .band{width:100%;}
  .band h2{
    margin:0; text-align:center; text-wrap:balance;
    font-family:'Libre Baskerville', 'Libre Baskerville Fallback', 'Libre Baskerville Fallback Times', ui-serif, Georgia, serif;
    font-size:24px; line-height:1.2; color:var(--ink);
  }
  /* Visible to screen readers only: a band needs a heading for the document
     outline even where the content already labels itself on screen. */
  .sr-only{
    position:absolute; width:1px; height:1px; margin:-1px; padding:0;
    overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
  }

  .band-sub{
    margin:12px auto 0; max-width:620px; text-align:center;
    font-size:15px; line-height:21px; color:var(--faint);
  }

  /* ---- one accent per band ------------------------------------------------
     The game's five colours are marks, not grounds. A band that carries an
     argument takes exactly one of them and spends it on everything coloured
     inside it: the 3px edge it opens with, the rule under its heading, and
     whatever numeral or border belongs to that section. One colour per band is
     what lets a reader learn what a colour means here — and is precisely what
     four saturated grounds destroyed, by shouting the colour before anything
     had been said with it.

     Brass is the default every band starts from — the page's own binding
     colour, and the right answer for the studio ident, because the game has no
     colour for "who made this" and inventing one would be an accent doing no
     work. The closing ask takes gold, the one band that overrides it.

     Every value here is a --game-*-ink where the mark can be small or sit on a
     tinted card, and the raw colour only where the mark is a 3px rule and the
     3:1 non-text bar applies. tests/palette.spec.ts checks both. */
  .band{--accent:var(--brass-ink);}
  .band-close{--accent:var(--game-gold);}

  /* Every band names its colour once, under its own heading. Short and
     centred, so it reads as a mark on the heading rather than as a divider —
     the band already has a 3px edge for that job. No forced-color-adjust: this
     one IS decoration, so under Windows High Contrast it should become the
     system ink like everything else. Headings hidden from sight (.sr-only)
     are skipped: a decorative
     ::after on one of those is a rule nobody sees on a heading nobody sees. */
  .band h2:not(.sr-only)::after{
    content:""; display:block; width:64px; height:3px; margin:12px auto 0;
    background:var(--accent); border-radius:2px;
  }

  /* The <use> sprite: present in the DOM, never painted. */
  .sprite{position:absolute;}

  /* ---- the player, full screen -----------------------------------------
     The fullscreen UA stylesheet sets position, inset, width and height with
     !important, so a video sized into a hole in the hero frame does land on
     the whole screen. `object-fit:contain` in that same UA rule is NOT
     !important — it is the one declaration author CSS still wins — so the
     `object-fit:cover` the player needs in the page followed it into
     fullscreen and cropped the picture to the shape of the display. Measured
     in Chromium at 1440x900: fullscreen computed object-fit:cover on a 16:9
     source, about 11% of the frame cut off the top and bottom, with the hero
     player's 4px corner radius still rounding the picture's corners on top of
     that. Full screen means the whole frame: contain, square corners, black
     where the display is not 16:9.

     Written as two rules, not one selector list: an engine that does not know
     one of these pseudo-classes drops the entire rule it appears in, so
     listing them together would lose the fix everywhere. */
  .frame video:fullscreen{
    object-fit:contain; border-radius:0; background:#000;
  }
  .frame video:-webkit-full-screen{
    object-fit:contain; border-radius:0; background:#000;
  }
  video::backdrop{background:#000;}

  /* ---- the ask: the page's only dark ground. It sits at the middle of the
     page now, not at the end — after the rules, the ways to play and the four
     numbers, which is the earliest point at which a visitor knows what they
     would be buying, and far enough above the memorial that the two are never
     on screen together. Everything below it (the accessibility work, the Duck
     Court, the studio) answers a reader who did not decide here. ---- */
  .band-close{
    background:var(--band-dark); color:var(--on-dark);
    margin-top:0; padding:64px 0 72px; text-align:center;
    /* positioned so the two feathers that fell onto the band place against it */
    position:relative;
  }
  /* On the dark ground the game's gold is 9.18:1, well past the 4.5 the
     lightened brass was chosen to clear on cream. The band can have the real
     colour, and everything in it uses the same one. */
  .band-close h2{color:var(--game-gold);}
  .band-close .band-sub{color:rgba(255,249,236,.82);}
  .band-close .wheretobuy{
    margin:16px 0 0; font-size:13px; color:rgba(255,249,236,.72);
  }
  .band-close .wheretobuy a{color:var(--game-gold);}
  /* 38px, not 22: set for the "Soon" stamp that hung off this pill until
     2026-09-12, and kept - the ask reads better with the air. */
  .closerow{display:flex; justify-content:center; align-items:center; gap:20px; margin-top:38px;}
  .band-close .pill{
    justify-content:flex-start; background:var(--game-gold); color:#2A1608;
    box-shadow:0 2px 3px rgba(0,0,0,.35);
  }
  .band-close .pill .disc{background:#2A1608;}
  .band-close .pill .disc svg{color:var(--game-gold);}
  .closenote{
    margin:18px auto 0; max-width:560px;
    font-size:13px; line-height:18px; color:rgba(255,249,236,.66);
  }
  .closenote a{color:var(--game-gold);}
  /* The two emoji in the memorial line. Colour glyphs sit on their own
     baseline and run large next to 13px copy, so they are pulled down to the
     text's own optical centre and held a hair off the word they follow. Each
     carries role="img" and a label, because a screen reader that reads the raw
     character says "red heart" mid-sentence. */
  .emoji{
    display:inline-block; margin-left:2px;
    font-size:12px; line-height:1; vertical-align:-1px;
  }

  /* ---- footer mark + footer ---- */
  /* The mark and the link row are one footer landmark now — they used to be
     two siblings, and axe-core counted the mark as content outside any
     landmark. The spacing is unchanged: what was footer margin-top is now the
     footer's own, and the mark keeps the rule above it. */
  footer{width:860px; margin:38px auto 0; padding:0 0 26px; position:relative;}
  .footmark{width:auto; margin:0 0 20px; padding-top:22px; border-top:1px solid var(--hair-c);}
  .footrow{display:flex; align-items:center; justify-content:space-between; gap:18px;}
  .footrow span{font-size:11px; color:#3E2B14;}
  .footlinks{display:flex; gap:26px; font-size:11px;}
  /* padding grows the tap target to the WCAG 2.5.8 24x24 minimum (11px text in
     a page that `zoom` shrinks on narrow screens); the equal negative margin
     cancels it again so the footer lays out pixel-identically */
  .footlinks a{
    color:#26170A; text-decoration:none; white-space:nowrap;
    padding:12px 6px; margin:-12px -6px;
  }
  .footlinks a:hover{text-decoration:underline;}
  /* The trademark line. A step quieter than the copyright above it and given a
     measure of its own: it is a notice, read once if at all, and it must not
     compete with the four links it sits under. The top rule separates it from
     them without adding a fifth thing to look at. */
  .trademarks{
    margin:14px 0 0; padding-top:12px; border-top:1px solid rgba(62,43,20,.18);
    max-width:70ch; font-size:10px; line-height:16px; color:#5A452C;
  }
  .trademarks a{color:#26170A;}

  /* ---- the dock (outside .page, sized in rem) ---- */
  .dock{
    position:fixed; left:0; right:0; bottom:0; z-index:40;
    display:flex; align-items:center; gap:.75rem;
    padding:.5rem 1rem calc(.5rem + env(safe-area-inset-bottom)) 1rem;
    background:rgba(253,246,227,.96);
    /* Brown, not a hairline. The dock is fixed over whatever the visitor is
       reading, so the edge that says where the page stops and the bar begins
       has to be visible against both light grounds; --hair-a was 1.09:1 on the
       dock's own cream and read as nothing. --brass-ink is the page's binding
       colour at 5.87:1 there, still quieter than the dock's own text. Six
       pixels, not one: at 1px the line disappeared against the closing band it
       sits over, so it reads as a drawn edge rather than a rendering seam. */
    border-top:6px solid var(--brass-ink);
    box-shadow:0 -2px 10px rgba(42,33,24,.10);
    transform:translateY(100%);
    /* Lowered, the dock is off-screen but was still in the tab order: three
       controls a keyboard visitor could focus with nothing visible anywhere on
       screen (WCAG 2.4.7, and 2.4.11 since they were entirely hidden).
       visibility:hidden takes them out of the tab order; delaying it by the
       length of the slide keeps the animation. */
    visibility:hidden;
    transition:transform .18s ease, visibility 0s linear .18s;
  }
  .dock.is-up{
    transform:translateY(0); visibility:visible;
    transition:transform .18s ease, visibility 0s;
  }
  .dock[hidden], .dock .dockcta[hidden], .dock .dockbtn[hidden]{display:none;}
  /* Reserve the dock's height while it is up, or it covers the closing band
     and the footer links it is sitting on top of. */
  body.has-dock{padding-bottom:4.5rem;}
  .dockmark{
    display:flex; align-items:center; gap:.4rem;
    font-size:.9rem; font-weight:700; color:var(--ink);
  }
  .dockmark img{display:block;}
  .dock .dockcta, .dock .dockbtn{
    border:0; font-family:inherit;
    display:flex; align-items:center; min-height:2.75rem; padding:0 1rem;
    border-radius:999px; text-decoration:none; white-space:nowrap;
    font:700 .9rem/1 'Inter', 'Inter Fallback', sans-serif; cursor:pointer;
  }
  .dock .dockcta{
    margin-left:auto; background:var(--btn); color:var(--on-dark); border:1px solid var(--btn);
  }
  /* One size for both labels. The button says "Stop the duck" and then "Go
     ducky gooo!", which are 131px and 145px in Inter (130 and 142 in the
     fallback), so the dock used to change width the moment anyone pressed it -
     and the wishlist button beside it slid along with it. 9.5rem holds the
     longer label with room to spare in either face; min() keeps it from
     forcing a scrollbar on a 320px screen, and the label centres rather than
     sitting left in a box it no longer fills. */
  .dock .dockbtn{
    background:none; border:1px solid var(--hair-d); color:var(--btn);
    min-width:min(9.5rem, 100%); justify-content:center;
  }
  /* Also gated: the dock is the phone-primary surface and this border jumps
     from a hairline to full ink, which sticks after a tap on a touch device. */
  @media (hover: hover){
    .dock .dockbtn:hover{border-color:var(--btn);}
  }

  @media (max-width:560px){
    .ctarow{flex-direction:column; gap:12px;}
    .dockmark{display:none;}
    .dock .dockcta{margin-left:0; flex:1; justify-content:center;}
    /* The rail's hairline dividers only make sense on one line. */
}

  /* At 320px — and at any width once a reader applies the WCAG 1.4.12 text
     spacing — the dock's three controls do not fit on one line. They wrap onto
     two rows rather than pushing the dock's duck button off screen, which
     would fail both 1.4.10 and 2.2.2. */
  @media (max-width:420px){
    /* The dock is two rows here (about 118px), so the padding that keeps
       focus clear of it has to match (2.4.11); site.js also nudges focus that
       lands under it into view, for engines that skip scroll-padding on Tab. */
    html{scroll-padding-bottom:calc(8rem + env(safe-area-inset-bottom));}
    .dock{flex-wrap:wrap; gap:.5rem; padding-left:.75rem; padding-right:.75rem;}
    .dock .dockcta{flex:1 0 100%; order:-1;}
    .dock .dockbtn{flex:1; justify-content:center;}
    body.has-dock{padding-bottom:8rem;}
  }

  /* ---- the first screen on a laptop (2026-09-14) -------------------------
     The two feature callouts are part of the pitch, and on a MacBook Pro 14"
     in Chrome with its tabs and bookmarks bar the page area is about
     1346x657: the callouts ended at 638px, under the 66px dock, so only their
     headings showed. These values take 70px out of the hero's vertical rhythm
     so the callouts end at about 568px and clear the dock in a 640px-tall
     window. The callouts sit in equal 22px gaps, below the last button and
     above the brown band that follows, and in that 657px window the band's top
     lands on the dock's top line. Nothing above the wordmark moves inside the hero (studio line,
     its gap, the wordmark), because --plate-top ties the frame's drawn top
     line to the wordmark's bottom at 100px; the hero's own top padding may
     change, since the frame moves with the whole row. Desktop only: the phone
     layout below 820px keeps its spacing and its decor. The platform rows
     keep 8px + 5px of leading, still clear of the Steam Deck mark's 7px
     clear space. ---- */
  @media (min-width:820px){
    .hero{padding-top:8px;}
    h1{margin-bottom:10px;}
    .lead{margin-bottom:6px;}
    .heromodes{row-gap:2px; margin-bottom:10px;}
    .herofacts{row-gap:8px;}
    .hero .ctarow{margin-top:14px; gap:8px;}
    .features{margin:22px 0;}
    /* The feather under the callouts had the old 56px below them to lie in;
       with 22px it reached into the brown band. It moves out into the left
       gutter beside the Steam Deck duck instead. */
    .features .pf-4{left:-58px; top:18px;}
  }
  /* Below 1060px the gutter is too narrow to hold that feather, so it keeps
     its old place under the callouts and the old 56px below them. Laptop
     windows are wider than this; tests/media-fit.spec.ts counts the feather on
     screen at 920 and 1199. */
  @media (min-width:820px) and (max-width:1059px){
    .features{margin-bottom:56px;}
    .features .pf-4{left:118px; top:74px;}
  }

  /* ---- H5: below 820px the fixed composition stops being scaled down and
     becomes a single column instead. `zoom` on .page caps browser zoom at
     viewport/920 and shrinks 12.5px body copy to 5.3px on a 390px phone; both
     stop here. Above 820px the 920px composition is untouched. ---- */
  @media (max-width:819px){
    .page{zoom:1;}
    .col, .inner, .features, .footmark, footer{width:auto; max-width:100%;}
    .col, .inner{padding:0 18px;}
    .footmark, footer{margin-left:18px; margin-right:18px; width:auto;}
    .herogrid{display:block; width:auto;}
    .herotext, .heromedia{width:auto; margin-left:0; margin-right:0;}
    .herotext{text-align:center;}
    .studio{justify-content:center;}
    .lead{text-align:center;}
    .heromodes{justify-content:center; gap:8px 14px;}
    /* The platform list is a flex row, so centring it is a justify, not a
       text-align - and on a 360px column it wraps to two rows of two. */
    .herofacts{justify-content:center; gap:12px 16px;}
    /* The two asks are 425px side by side, which is wider than the phone.
       Specificity, not the media query, is why this has to repeat .hero: the
       generic .ctarow rule further down cannot outrank .hero .ctarow. */
    .hero .ctarow{flex-direction:column; align-items:center; justify-content:center;}
    /* flow-root: the plate below carries a negative top margin, and without
       a formatting root it would collapse through .heromedia and drag the
       cell itself up under the buttons. The cell stays 26px under the text
       (tests/mobile.spec.ts reads its top as "one column"); the margin pulls
       only the plate, which overhangs the cell's top by the empty band. */
    /* 58px, not 26: the plate's own feathers drawn above its top line reached
       the bottom edge of "Check the Game" (2026-09-14). */
    .heromedia{margin-top:58px; display:flow-root;}
    h1{margin-bottom:14px;}
    h1 .wordmark{height:48px; margin:0 auto;}
    /* Back in flow, and 119.34% of the column, which is the widest the plate
       goes with all of its drawing on screen. The art's ink runs from x 66 to
       x 1037 of the 1160 master (5.69% to 89.483%, measured off the alpha
       channel), so 83.793% of the plate's box is drawing and the box has to
       be 1 / 0.83793 = 119.34% of the column for that drawing to fill it.
       -6.79% then puts the ink's left edge on that box's own left edge: 5.69%
       of 119.34%. The box is the shrink-wrapped .herogrid, which .hero centres
       in the column, so the drawing ends up centred with the column's 18px of
       padding as slack - and on a 320px phone, the narrowest this page
       supports, that is the whole margin the art gets.

       It was width:135% with margin-left:-14.25% until 2026-09-09, which
       centred the OPENING on the column and let the plate's sides run off
       both edges of the screen into .page's overflow-x:clip. That bought a
       67% opening (259px on a 390px phone) and cost the mallard, the white
       duck and the windmill their outer halves - the ask was to see them
       whole, over the video if need be, and the ducks already overlap the
       opening by design. The opening is 64.6% of the column now, 229px on the
       same phone, and the plate is centred on its own ink rather than on the
       hole, which is 17px right of the plate's centre.

       The two vertical margins are the old -20% and -6% scaled by the same
       0.884 the plate shrank by, because a percentage margin resolves against
       the column's width, not the plate's: the empty top band still folds
       under the buttons, the duck heads still start 22.7% down the plate, and
       the corner feathers still tuck under the callouts. */
    .frame{position:relative; top:auto; left:auto; width:119.34%; max-width:none; margin:-17.68% 0 -5.3% -6.79%; height:auto; aspect-ratio:1160 / 643;}
    /* The chip sits on the video, in the opening's top-right corner as on
       desktop (the user's call, 2026-09-14). It used to sit above the drawn
       top line between the duck heads, where it read as slipped off the
       player, 6-9px under the ghost button. The opening is only 183-255px wide
       on a phone, so the chip is smaller here - 24px, 11px type, 6px in from
       the corner - which keeps it 6px or more clear of the play knob from
       360px up; below 360px even that crowds the knob, so the words go and the
       info mark stays, with "Describe this teaser" still its accessible name.
       The open panel spans the plate's visible width (10.56% to 84.6%) from
       the opening's top down, with room at the top for the chip. */
    .frame .mediaalt > summary,
    .frame .mediaalt[open] > summary{top:calc(31.10% + 6px); right:calc(25.34% + 6px); left:auto; transform:none; height:24px; padding:0 9px 0 7px; gap:4px; font-size:11px;}
    .frame .mediaalt .altmark{width:16px; height:16px;}
    .altbody{top:31.10%; right:15.4%; bottom:0; left:10.56%; padding-top:40px;}
    /* The loose feathers, placed for this layout rather than inherited from
       the 920px one, where four of them lay across the callouts, the band's
       two paragraphs and (at 320px) off the right edge. Each is anchored to
       an edge of the empty space it sits in - under the callouts, the band's
       top and bottom padding, the footer's top corner - never to a line of
       text, so a different wrap on another engine cannot slide one onto a
       word. tests/media-fit.spec.ts checks them at phone widths too. */
    .features .pf-4{left:auto; right:22%; top:calc(100% + 4px);}
    .pf-5{left:auto; right:28px; top:6px;}
    .pf-6{left:22%; right:auto; top:auto; bottom:14px;}
    .pf-7{left:auto; right:6px; top:-30px;}
    /* One column. `.features` is a grid above 820px, so the flex-direction
       this used to set did nothing and the two callouts stayed side by side —
       57px of text column each, four words to a line. */
    .features{grid-template-columns:1fr; gap:20px; width:auto;}
    .feature{width:auto;}
    .closerow{flex-direction:column; gap:14px;}
    .footrow{flex-direction:column; align-items:flex-start; gap:12px;}
    .footlinks{flex-wrap:wrap; gap:18px;}
    /* The dock sits over the end of the page, so the last band needs room. */
    body{padding-bottom:4.5rem;}
  }

  /* Windows High Contrast and any other forced-colours mode replaces every
     background and border with a system colour, which flattens controls that
     are distinguished by their fill alone: the CTA, the ghost button, the play
     affordance and the dock all become the same ground as the page. Borders
     drawn in system colours survive, so each control gets one. Not a WCAG
     success criterion in itself — 1.4.3 and 1.4.11 are evaluated in the
     default mode — but EN 301 549 expects author content to keep working when
     the platform's own contrast settings are on. */
  /* Below 360px the chip's words would cover the play knob: the mark alone,
     a 24px circle, with the label kept for screen readers. */
  @media (max-width:359px){
    .frame .mediaalt > summary,
    .frame .mediaalt[open] > summary{width:24px; padding:0; justify-content:center;}
    .frame .mediaalt .altlabel{position:absolute; width:1px; height:1px; margin:-1px; overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap;}
  }
  /* Very short windows - a phone on its side, or 400% zoom on a laptop - would
     give a fixed dock half the screen. It goes back into the flow at the end
     of the page instead (1.4.10). */
  @media (max-height:420px){
    .dock{position:static;}
    body.has-dock{padding-bottom:0;}
    html{scroll-padding-bottom:0;}
  }
  /* Print the page, not the fixed dock and the mascot over it. */
  @media print{
    .dock, #dod-duck, .skip{display:none !important;}
    body.has-dock{padding-bottom:0;}
  }
  @media (forced-colors: active){
    .pill, .ghost, .dock .dockcta, .dock .dockbtn{
      border:1px solid ButtonBorder; background:ButtonFace; color:ButtonText;
    }
    .pill .disc, .playaff .knob{
      border:1px solid ButtonBorder; background:ButtonFace;
    }
    /* The triangle is a CSS border trick, so it needs the system ink by name. */
    .playaff .knob::after{border-left-color:ButtonText;}
    /* ...but forced colours also paint the triangle's transparent top and
       bottom borders, which turned the play mark into a square (a stop icon).
       Opting the pseudo-element out keeps them transparent. */
    .playaff .knob::after{forced-color-adjust:none; border-top-color:transparent; border-bottom-color:transparent; border-left-color:ButtonText;}
    /* The wordmark is dark ink on a transparent PNG and all but vanishes on a
       dark high-contrast ground: give it its own paper. */
    h1 .wordmark{forced-color-adjust:none; background:#FDF6E3; padding:4px 8px; border-radius:6px;}
    .pill .disc svg{color:ButtonText;}
    .dock{border-top:6px solid CanvasText; background:Canvas;}
    a.skip{border:1px solid ButtonBorder;}
    /* The description chip and its panel are distinguished by fill alone, and
       they sit on video: with the platform's colours forced they would be an
       unbordered word floating over the frame. */
    .mediaalt > summary{border:1px solid ButtonBorder; background:ButtonFace; color:ButtonText;}
    .altbody{border:1px solid CanvasText; background:Canvas; color:CanvasText;}
    .altbody p{color:CanvasText;}
    a:focus-visible, [tabindex]:focus-visible, button:focus-visible,
    .playaff:focus-visible,
    .mediaalt > summary:focus-visible{
      outline:3px solid Highlight; outline-offset:3px; box-shadow:none;
    }
  }

  @media (prefers-reduced-motion:reduce){
    a.skip, .pill, .dock{transition:none;}
}
