/* ==========================================================================
   metaeden-v4-game.css -- what the v4 play surface adds (TASK-1113[Q]).

   Loaded ONLY by sites/v4-game.html, after metaeden.css (the cartridges' own
   presentation) and metaeden-v4.css (the chrome and all four themes).  Every
   rule is scoped under .v4-game-page, which is a class on that page's own
   <body>, so nothing here can reach the site root, the section shell or the
   arcade floor.

   THE COLOUR RULE THIS FILE KEEPS: it declares no text colour of its own.
   Anything the surface says inherits the body colour on the body background,
   which is one of four pairs metaeden-v4.css already ships -- 9.4:1 to 11.1:1
   once the CRT wash is composited at the shipped combo/35, measured the same
   headless way metaeden_net_scanline_contrast.test.js measures.  Reaching for
   h2 .sub would have been the natural thing and is exactly what not to do:
   that pair is 4.40:1 in the gold theme and 4.23:1 unthemed, two of the 33
   already under the floor and owned by TASK-1107[Q].  A new surface does not
   get to add a 34th.  Differentiation here is size, weight, spacing and a
   rule -- never a dimmer ink.
   server/metaeden_net_v4_game_surface.test.js measures the four pairs and
   also greps this file for a text-colour declaration, so the rule cannot
   lapse by somebody adding one.

   THE BORDER COLOURS ARE BORROWED, NOT INVENTED.  Each theme already owns a
   divider colour in metaeden-v4.css -- h2's bottom rule in three of them, and
   in grey the #888 that hr, th, .side, .docbody and .ring all draw with.  The
   boxes below use that colour, and in grey they use grey's own idiom too: a
   top-and-bottom rule instead of a full box, exactly like .side and .docbody.
   Borders are decoration; the 4.5:1 floor is about ink.
   ========================================================================== */

/* [hidden] has to beat the display values below, or an element the adapter
   left hidden would be laid out anyway. */
.v4-game-page [hidden] { display: none !important; }

/* THE TAB ROW'S SHORT FORMS ARE OFF HERE (TASK-1186[Q]).  Two of the six links
   carry both a long word and a short one in the markup (v4-game.html's own note
   says why it is markup and not a rule); this file is the surface's ALWAYS
   sheet, so it declares the desktop answer -- the long word -- and
   mobile-e-game.css, which no desktop fetches, swaps them at phone widths.
   Stated in this direction on purpose: a phone that somehow never gets the
   mobile sheet reads "documents" and "guestbook" and is merely wide, while the
   other way round it would read both words of each pair. */
.v4-game-page .tabnarrow { display: none; }

/* THE PLAY COLUMN IS CAPPED, and the site root's is not, on purpose.  The
   shell is min(100% - 48px, 1760px), which is right for a table of eight rows
   and wrong for one cabinet: metaeden.css caps its cartridge internals between
   520px and 760px, so an un-capped column leaves a 640px-wide game adrift in
   1700px of frame.  820px clears the widest cartridge in that file plus this
   surface's own padding and border, so the eight still to come fit without
   this number moving.

   AND IT IS CENTRED IN THAT FRAME, user's word: everything in this section
   reads centred except the site's own top banner, which lives in <header>,
   outside .gplay, and is untouched by anything in this file. */
.v4-game-page .gplay { max-width: 820px; margin: 0 auto; }

/* THE HEADING LINE -- title plus blurb -- centred with the column it sits in.
   It was one <h2> with the blurb as a span inside it until TASK-1186[Q]; it is
   now an <h2> and a <p> side by side in .ghead, so that a phone can move the
   blurb out from under the title and put it below the game (the user's ask;
   mobile-e-game.css does the moving, and v4-game.html's own note carries the
   reasoning).  A CENTRED FLEX ROW, aligned on the baseline, reproduces what
   the inline pair drew: measured against the shipped page at 1280, the title
   and the blurb land on the same line with a 9px gap between them, so 9px is
   what the gap says.  It wraps rather than squeezing, which is what the inline
   version did too.

   AND THE HEADING RULE IS ON THIS BOX, NOT ON THE <h2>.  metaeden-v4.css gives
   every h2 `margin: 28px 0 10px; padding-bottom: 6px; border-bottom: 1px solid`
   and a colour per theme, and while the blurb lived inside the h2 that rule
   spanned the whole heading.  A flex item is sized to its content, so left on
   the h2 the divider would now stop at the end of the title.  Moved out here
   it spans exactly what it always spanned.  The four colours are BORROWED, not
   invented -- they are metaeden-v4.css's own h2 border colours, the same
   borrowing (and the same reason) as .gstage/.gmiss below; grey draws no
   divider under an h2 and draws none here either. */
.v4-game-page .ghead {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  justify-content: center;
  column-gap: 9px;
  margin: 28px 0 10px;
  padding-bottom: 6px;
  border-bottom: 1px solid #cee3f8;
  text-align: center;
}
/* border-color rather than border-bottom-color, which is the .gstage rules'
   idiom two blocks down and is not cosmetic: the guard suite reads "the text
   before `color:`" to tell a border colour from an ink, so the longhand reads
   to it as an unmeasured text colour and turns section F red.  Three of the
   four sides have no width here, so setting all four says the same thing. */
.v4-game-page.dark .ghead { border-color: #333333; }
.v4-game-page.neon .ghead { border-color: #f2c200; }
.v4-game-page.dxhr .ghead { border-color: #3a3020; }
.v4-game-page.grey .ghead { border-bottom: 0; }
/* ...and the h2 gives up the box it no longer owns.  min-width:0 so a long
   title wraps inside its own item instead of forcing the row wider than the
   column. */
.v4-game-page .ghead > h2 {
  margin: 0;
  padding-bottom: 0;
  border-bottom: 0;
  min-width: 0;
}

/* IT IS NOT A LINK AND MUST STOP DRESSING AS ONE (TASK-1189[Q]).  The user:
   "Make the game title not blue and highlighted (otherwise it looks like a
   link, and it's not)."  The source is body.grey h2 in metaeden-v4.css --
   `color:#0000ee` plus `text-decoration:underline` -- and it is RIGHT where it
   lives: on the site root every h2 heads a section you can click through to,
   and the grey theme is the one that dresses like a 1996 document.  Here the
   h2 is the name of the cartridge already on the screen, so the same paint is
   a promise the page cannot keep.
     Overridden HERE rather than fixed there, deliberately: metaeden-v4.css is
   shared with the site root, and this is the game surface disagreeing about
   one heading, not the theme being wrong.  `inherit` rather than a literal so
   it takes whatever ink the theme gives its body -- no fifth colour pair for
   anyone to measure. */
.v4-game-page.grey .ghead > h2 {
  color: inherit;
  text-decoration: none;
}

/* The blurb under the heading.  Sized and spaced like h2 .sub, inked like the
   body, for the reason in the header above. */
.v4-game-page .gsub {
  font: 400 18px Michroma, Verdana, sans-serif;
  letter-spacing: 0;
  color: inherit;
}
.v4-game-page .ghead > .gsub { margin: 0; min-width: 0; }
.v4-game-page.grey .gsub {
  font-family: "Times New Roman", Times, serif;
  font-style: italic;
}

/* The frame around the cartridge. */
.v4-game-page .gstage,
.v4-game-page .gmiss {
  margin: 16px 0 0;
  padding: 10px;
  border: 2px solid #cee3f8;
}
.v4-game-page.dark .gstage,
.v4-game-page.dark .gmiss { border-color: #333333; }
.v4-game-page.neon .gstage,
.v4-game-page.neon .gmiss { border-color: #f2c200; }
.v4-game-page.dxhr .gstage,
.v4-game-page.dxhr .gmiss { border-color: #3a3020; }
.v4-game-page.grey .gstage,
.v4-game-page.grey .gmiss {
  padding: 12px 0;
  border: 0;
  border-top: 1px solid #888888;
  border-bottom: 1px solid #888888;
}

/* The cartridge sets its own top margin for the old surface's stacked shelf;
   inside a frame of its own it does not need one. */
.v4-game-page .gstage > * { margin-top: 0; }

.v4-game-page .gnote {
  margin: 14px 0 0;
  font-size: 17px;
  color: inherit;
  text-align: center;
}

.v4-game-page .gmiss { text-align: center; }
.v4-game-page .gmiss p { margin: 0 0 8px; }
.v4-game-page .gmiss p:last-child { margin-bottom: 0; }

.v4-game-page .gback {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 8px;
  margin: 24px 0 0;
}

/* --------------------------------------------------------------------------
   WHY THERE IS STILL NO @media (max-width: ...) BLOCK HERE, which is the
   first thing anybody adding a cartridge will reach for.  TASK-1136[Q]
   changed the REASON without changing the answer, so read this before
   deciding the note went stale.

   THE ORIGINAL REASON, which holds wherever METScape is still served.
   browser-frame.css:2 pins `body.browser-framed { min-width: 1920px }` and
   gives the frame itself a flat `width: 1920px`.  METScape is a picture of a
   1996 desktop, not a responsive shell.  So .shell resolves to its 1760px cap
   and .gplay to 820px, while a media query keys off the VIEWPORT rather than
   off that box: at 375px it would match while the container was still 820px
   wide, collapsing a cartridge that has plenty of room.  MEASURED, on the
   rig, at 375px.

   WHAT CHANGED.  browser-frame.css/js are no longer served to this page at
   every width.  They are gated to 769px and up, and below that
   mobile-e-game.css runs in their place; the swap comment in
   sites/v4-game.html is the whole story.  So under 768px the premise above is
   simply gone -- the column really is as narrow as the viewport, and a width
   rule really would mean what it says.  It just does not belong HERE.  The
   narrow-width rules for this surface live in mobile-e-game.css, which is
   served ONLY at those widths, so they have one home rather than two halves
   that can disagree about which of them is in force.

   AND ONE MORE HOME SINCE TASK-1143[Q], for one idea neither file can hold:
   metaeden-v4-game-fit.css carries how the FULLSCREEN MODE sits on a phone
   (both orientations -- its media gate has a max-height half, because a
   phone turned sideways is 844px wide and mobile-e-game.css's width gate
   cannot see it).  That file is about the mode's fixed frame, which lives
   in VIEWPORT space, so a viewport query is honest there in exactly the way
   this comment says it is not for the in-page rules this file owns.  The
   division of labour: this file = the mode at desktop, mobile-e-game.css =
   the PAGE at phone widths, the fit file = the MODE at phone viewports.

   AND A MECHANICAL REASON, worth knowing before you spend an afternoon
   rediscovering it.  metaeden_net_v4_game_surface.test.js asserts that every
   selector in this file is scoped to .v4-game-page (H45, plus the mutation
   harness that proves the check itself works).  That parser reads the text
   before each `{` as a selector, so an at-rule's prelude reads as an unscoped
   one: a media block here turns the suite red on arrival, whatever is inside
   it.

   TASK-1114..1121: if your cartridge is wider than the column, do NOT reach
   for a width query.  It scrolls inside its own box, below.  If METScape ever
   becomes responsive above the breakpoint, the honest tool is a container
   query on .gplay, not a viewport one.
   -------------------------------------------------------------------------- */

/* Wide content scrolls inside its own frame rather than pushing the page --
   the house rule from CLAUDE.md's "fixed-size panels, scroll for overflow".
   auto, not scroll: metaeden.css caps its cartridge internals at 760px and
   none of them needs this today, so no bar is drawn until one does. */
.v4-game-page .gstage { overflow-x: auto; }

/* ==========================================================================
   16:9 IN THE ORDINARY VIEW TOO (TASK-1158[Q]).

   THE ASK, in the user's words: "try to keep all games at their 1920x1080
   aspect ratio at all times no matter which mode they're in."  The lock below
   the fold has held that shape since TASK-1124[Q], but only under .gfs -- the
   big view.  Out of it the frame was whatever height the cartridge inside
   happened to want, so the same game was a 16:9 screen in one mode and an
   arbitrary rectangle in the other.  These declarations make the frame the
   same shape in both, so the mode changes how BIG the screen is and never
   what shape it is.

   IT IS THE SAME IDEA AS THE BIG VIEW, MEASURED AGAINST A DIFFERENT BOX.
   Down there the frame is the largest 16:9 rectangle that fits the VIEWPORT.
   Here it is the largest one that fits the COLUMN -- .gplay, capped at 820px
   just above -- so `aspect-ratio` alone says the whole of it: the width is
   already the column's, and the height follows from the ratio.  No min()
   arithmetic, because there is no second axis competing for the space.

   NOTHING IS EVER CROPPED TO ACHIEVE IT, which was the user's other standing
   instruction on this ticket ("please prefer letterboxing over cut off
   content").  A cartridge SHORTER than the box is centred in it and the
   spare height reads as letterbox.  A cartridge TALLER than the box scrolls
   INSIDE the stage -- the house rule this file already keeps two rules up --
   rather than being clipped by the frame or pushing the page.  There is no
   `overflow: hidden` anywhere in this block on purpose: that is the one way
   the shape could be bought with the player's content, and it is the exact
   bug this ticket was opened to fix on the arcade page.

   AND THE CARTRIDGES ARE SCALED INTO IT, which is the half that makes the
   shape worth having.  Locking the frame ALONE was tried first and measured,
   and that is why this paragraph exists: at 820x461 the stage is 397px, and
   eleven of the thirteen cabinets are taller than that (xyz by 391px,
   snakecycle by 305, solitaire by 213), so every one of them drew a scrollbar
   and showed about two thirds of itself through a 16:9 window.  That is the
   user's own "cut off content" wearing a different hat, and it is worse than
   the unlocked view it replaced.  The ask was "the largest 16:9 box that fits,
   centred, scaled not stretched", and SCALED is the load-bearing word.

   SO THE SIZING RULES BELOW THE FOLD ARE SHARED BY BOTH MODES rather than
   copied into this one.  Everything that fits a cartridge to its frame -- the
   canvas height, minesweeper's and solitaire's published scale knobs, the
   layout hand-backs for the raycade floor and xyz, the two screens that centre
   themselves -- hangs off `--gfs-h`, which means "the height of the frame",
   and each mode declares its own value for it.  One set of rules, two frames:
   a cartridge cannot come out right in one mode and wrong in the other,
   because there is only one description of how it is fitted.  (Rules that are
   about the MODE rather than the fit -- the fixed position, the letterbox
   bars, the control strip's themed background -- stay .gfs-only, as they were.)

   WHY --gfs-h CAN BE A CONSTANT HERE while the big view computes it from the
   viewport: down there the frame IS the viewport, so it has to be min()
   arithmetic over 100vw and 100vh.  Here the frame is the COLUMN, and the
   column does not vary anywhere this lock is in force.  .gplay is capped at
   820px above, and METScape pins body.browser-framed{min-width:1920px} from
   769px up, so the page is 1920 wide, .shell resolves to its 1760 cap and
   .gplay to 820 at every desktop width there is.  Below 769px METScape is not
   served at all and metaeden-v4-game-fit.css releases this whole lock (the
   column really is as narrow as the glass down there, and a 370x208 letterbox
   is the wrong spend of a phone).  So the one case where 820 would be a lie is
   the one case this rule is not in force for.

   box-sizing, MEASURED: .gstage is content-box and carries 10px of padding
   plus a 2px border, so a flex child at `flex: 1` came out 24px taller than
   the frame had and drew a scrollbar the moment the lock went on.

   AND IT IS A FLOOR, NOT A CEILING, which is the last thing this block learned
   and the thing to read before "correcting" it to `aspect-ratio: 16 / 9`.
   A hard lock was written first and measured, cabinet by cabinet.  With the
   fit rules below doing their work it still left nine of the thirteen taller
   than the box, by 7px to 188px -- and the residue is not the playfield, which
   these rules scale, but each cartridge's own CHROME: golf's console, lex
   trace's found-word list, solitaire's bottom row.  Those are fixed-height and
   nothing here can shrink them.  Dropping the canvas budget to half its size
   barely moved the total (822px to 345px) and made the games tiny for it.
   What a hard lock actually shipped was the screenshot that killed it: mini
   golf in a tidy 16:9 screen with its POWER METER AND BUTTONS below the fold,
   so putting meant scrolling first.

   The user gave the tiebreaker for exactly this in the same ticket -- "please
   prefer letterboxing over cut off content" -- so where the two asks collide,
   the shape yields.  A cartridge that fits gets a real 16:9 screen with the
   spare height as letterbox, which is the ask and is what most of them get; a
   cartridge that does not fit makes the box taller instead of hiding its own
   controls behind a scrollbar.  Nothing is ever cropped either way.

   If a later round wants the hard lock back, it is this one declaration
   (min-height -> aspect-ratio) plus per-cartridge chrome fits for the nine --
   the same work the arcade floor already does for its own eight, further down
   metaeden.css. */
.v4-game-page #gframe {
  display: flex;
  flex-direction: column;
  min-width: 0;
  min-height: calc(820px * 9 / 16);
  margin-right: auto;
  margin-left: auto;
  --gfs-h: calc(820px * 9 / 16);
}
.v4-game-page #gframe .gfsbar { flex: 0 0 auto; }
/* THE STAGE IS LEFT IN NORMAL FLOW, and that is a decision that cost a round
   to learn.  `display: grid` with `align-content: center` was the obvious way
   to turn spare height into a letterbox instead of a gap under the game, and
   it is wrong here: a centred grid row is sized to its CONTENT, so a cartridge
   that asks for `height: 100%` (the raycade floor and xyz both do, to stop
   their stages deriving a height from their width) resolves that percentage
   against a track that is itself waiting on the content, gets `auto`, and goes
   back to being as tall as it is wide.  Measured: it put xyz 177px over the
   frame in the BIG view, which had been exact before the centring was added.
   A cartridge cannot be handed a definite height and centred against its own
   content at the same time; the definite height is the one that matters, so
   the stage stays a plain block and the cartridges centre themselves.

   overflow stays auto rather than hidden: with the fit rules below nothing
   should reach it, and if something ever does the house rule is that it
   scrolls rather than being cropped.

   AND THE SCROLL CHAINS OUT OF IT AGAIN OUTSIDE THE MODE (TASK-1186[Q]).
   `overscroll-behavior: contain` sat on this rule unconditionally and is the
   whole of the user's second report: "it's also hard to scroll while in this
   view if the game is taking up the whole screen."  MEASURED at 375x812 on
   fishing, which is the cabinet they were holding.  The stage is
   `overflow: auto`, so it is a scroll container whether or not it has anything
   to scroll -- and there it has nothing: scrollHeight 456 against clientHeight
   456, a range of zero.  `contain` then means what it says.  A drag started
   anywhere in the game moved window.scrollY from 0 to 0; the same drag on the
   heading 118px higher moved it 0 to 128, the document's whole range.  The
   game covered y=288..746 of an 812px screen, so 56% of the glass was dead to
   a finger and the visitor's only purchase on the page was the header strip
   above it.  Releasing the containment here moved that same drag to 128.

   THE CONTAINMENT IS NOT WRONG, IT WAS JUST TOO WIDE.  What it guards is a
   cartridge whose own board really does pan inside the stage (minesweeper's
   two big boards) taking the page with it when it reaches an edge, and that
   is worth guarding IN THE MODE, where the frame is the viewport and there is
   no page to go to.  So it moves to .gfs and the ordinary in-page view -- the
   one a visitor is actually on when they tap a game -- gets its scroll back.
   Out there the page behind the stage is the thing they are reaching for. */
.v4-game-page #gframe .gstage {
  box-sizing: border-box;
  flex: 1;
  min-height: 0;
  overflow: auto;
}
.v4-game-page.gfs #gframe .gstage,
.v4-game-page.gbare #gframe .gstage { overscroll-behavior: contain; }

/* ==========================================================================
   THE FULLSCREEN MODE'S FRAME (TASK-1124[Q], one control since
   TASK-1143[Q]).  Born as WIDESCREEN: "full viewport but still constrained
   by aspect ratio, not literal full screen, the user can still opt to do
   that manually via the browser full screen."  TASK-1134[Q] then added a
   second control that layered the real Fullscreen API on top, and
   TASK-1143[Q] merged the two at the user's direction -- one button, the
   fill always, the monitor as well where the browser allows.  Everything in
   this block is the FILL half and is exactly what widescreen was: the class
   is still .gfs, the arithmetic is untouched, and the API half lives
   entirely in metaeden-v4-game.js (this stylesheet's only contact with it
   is the :fullscreen block further down).

   1920x1080 IS AN ASPECT, NOT A RESOLUTION.  Nothing here renders at a fixed
   1920 pixels.  The frame is the largest 16:9 box that fits the viewport, and
   what the viewport has left over goes black: side bars on an ultrawide, top
   and bottom bars on a phone, and on a 16:9 panel no bars at all.
   (At PHONE viewports the trade reverses and the letterbox is the wrong
   spend of the glass -- metaeden-v4-game-fit.css releases the lock there,
   inside the mode only.  See the media-query note above for why that lives
   in its own file.)

   THE FILL NEVER ASKS THE API.  These declarations are min() arithmetic
   over 100vw and 100vh, so the frame fills whatever viewport it is handed
   without being told -- an F11 pressed by the visitor, or the real element
   fullscreen the adapter now requests alongside, both just change what
   those units resolve against.  Where the API is refused or absent (an
   iPhone, a pane whose policy forbids it), this block IS the whole mode,
   which is why it must stay deliverable on its own.

   ONE CONTROL ON THE SURFACE, INHERITED BY ALL THE CABINETS.  Nothing below
   names a cartridge except the lines that say why they have to, and no
   engine file was touched.  A new cabinet added by the recipe in
   metaeden-v4-game.js gets this for free the day it lands.

   THE FRAME IS THE ELEMENT THAT ALREADY EXISTS.  #gframe wraps the control and
   #gstage in the markup and goes position:fixed here; the cartridge's own DOM
   is never moved, rebuilt or reparented, which is the whole of why a round in
   progress survives entering and leaving.  A move would probably survive too
   (browser-frame.js moves every mounted cartridge into .browser-viewport on
   every page in the ring, and the games work), but not moving is free.

   position:fixed RESOLVES AGAINST THE VIEWPORT HERE, and that is checked, not
   assumed: a transform, filter, will-change or contain on any ancestor would
   make it resolve against that ancestor instead, and browser-frame.css
   declares none of the four.  It matters because METScape pins
   body.browser-framed{min-width:1920px} and .browser-frame{width:1920px;
   height:1080px;overflow:hidden} -- the frame escapes all of that, including
   the overflow clip, because ancestor overflow does not clip a fixed
   descendant.  (METScape's own picture is 1920x1080, which is the same shape
   this mode asks for.  A coincidence, but a happy one.)
   ========================================================================== */

/* Out of the mode: a plain row above the stage, centred with the rest of the
   section rather than pinned to either edge.  The control is the site's own
   .bbtn, which is the one control metaeden_net_v4_game_surface.test.js
   already measures in all four themes -- a new control would have been a
   new pair to measure for no gain.  (One button on this strip since
   TASK-1143[Q]; the row layout never cared how many it held.) */
.v4-game-page .gfsbar {
  display: flex;
  justify-content: center;
  gap: 8px;
  margin: 16px 0 0;
}

/* THE TURN-YOUR-PHONE LINE IS OFF EVERYWHERE THIS FILE REACHES (TASK-1172[Q]).
   The <span class="grotate"> in the control's bar exists for exactly one
   state -- the MOBILE fullscreen mode, upright, on a cartridge whose row
   declares `wide` -- and that state is the fit file's to paint (its .gfsm
   rules; this file carries no @media, see the long note above, so the
   orientation half could never live here).  Hidden by default so a desktop,
   a screen reader on a desktop, and every out-of-mode phone view never meet
   it.  No colour declared: where the fit file reveals it, it inherits the
   body ink on the bar's own borrowed plate, which is a pair the suite
   already measures. */
.v4-game-page .grotate { display: none; }

/* THE WAY HOME IS OFF EVERYWHERE EXCEPT THE MODE (TASK-1212[Q]).  Same shape
   as the line above, and for the same kind of reason.  Out of the mode this
   page already carries seven links off it -- the mark, five nav tabs, "back to
   games" -- and every one of them is reachable; inside the mode #gframe goes
   fixed at z-index 300 over all seven and none of them is (measured, both
   devices, see the markup's own note).  So the door exists exactly where the
   page would otherwise have none, and adding an eighth control to the row the
   phone has been fighting to keep short would be chrome for nothing.

   NO COLOUR DECLARED, which is this file's standing rule and here it costs
   nothing: the link is a .bbtn sitting on .gfsbar's own themed plate, so it
   wears the pair the suite already measures in all four themes.  Only the
   phone needs a pair of its own, because over there it floats off the plate
   and over game art, and that pair lives in metaeden-v4-game-fit.css beside
   the close box and the rotate chip that already made the same trade. */
.v4-game-page #ghome { display: none; }
.v4-game-page.gfs #ghome { display: flex; }
/* TASK-1366[Q]: and pop out on the same gate, because this bar belongs to
   the FRAME.  Outside the mode there is no frame to escape and the bar is a
   zero-height strip with nothing in it; a control here would have added 30px
   above the stage on every game page, in a mode nobody asked to leave. */
.v4-game-page #gpopbtn { display: none; }
.v4-game-page.gfs #gpopbtn { display: flex; }

/* THE PAGE'S OWN SCROLLBARS GO AWAY FOR THE DURATION, and this is load-bearing
   arithmetic rather than tidiness.  vw and vh are defined against the initial
   containing block with scrollbars ASSUMED NOT TO EXIST, while a fixed element
   is laid out in the real viewport rect, which does not include them.  METScape
   pins body.browser-framed{min-width:1920px}, so every viewport narrower than
   about 1935px has a horizontal scrollbar -- the normal case here, not an edge
   one -- and the frame was measured 15px taller than the space it had, centred,
   losing 7px off the top and the bottom.  With no scrollbar the two agree
   exactly and the lock below is exact.  It also stops a wheel at the end of the
   stage from scrolling a page nobody can see. */
.v4-game-page.gfs { overflow: hidden; }

/* In widescreen.  THE ASPECT LOCK IS THESE THREE DECLARATIONS.  width and
   height each take the smaller of what the viewport allows and what the other
   axis implies, so the box is the largest 16:9 rectangle that fits and is
   centred by inset+margin.  aspect-ratio says the same thing declaratively;
   it is what a reader sees first and what a browser without min()/calc() in
   this position would still honour.  The suite computes the ratio these
   declarations produce at four viewports rather than trusting the words. */
/* TASK-1364[Q]: FULLSCREEN COVERS THE FAKE BROWSER, z-index 1006.

   The user: "when i full screen the minigame the browser frame still displays
   over it."  It did, and the two halves had been disagreeing since this rule
   was written: it is position:fixed inset:0 at 100vw by 100vh, which means to
   cover EVERYTHING -- while browser-frame.css paints the METScape chrome at
   1001 through 1005, and 300 loses to all five.  So the top of every cartridge
   sat behind the toolbar and the location bar; it is barely visible on a
   canvas game whose art starts lower and unmissable on one with a toolbar at
   its own top edge, which is how it finally surfaced.

   1006 IS CHOSEN, NOT PICKED.  It clears the chrome by one and stays under
   every deliberate full-screen takeover on this site, all of which SHOULD
   still cover a game in fullscreen: the door at 1000002, the arcade cabinet
   and its backdrop at 1000000 and 1000001, and the boss key cover at
   2147483647 -- the one thing entitled to sit above everything.

   It is scoped to .gfs, so leaving fullscreen hands the chrome back with no
   second rule to remember.  The original note below is unchanged and still
   true; it just now describes something that happens. */
/* THE AUTOMATIC MINIMUM SIZE IS TURNED OFF BELOW, and it is the one pair of
   declarations in this block that a reader will take for redundant. It is not.
   The bug it answers is invisible on a monitor and was measured on a 360x800
   phone (TASK-1136[Q]).

   A box with a preferred aspect-ratio and `overflow: visible` is given an
   automatic minimum size in the ratio-DEPENDENT axis: a browser will not make
   it smaller than its own min-content, so that honouring the ratio can never
   clip what is inside. Here the height is the definite one, so WIDTH is the
   dependent axis, and the frame's min-content is whatever the widest row of
   the mounted cartridge happens to be. Golf's 18-hole strip measures 403px.
   Above roughly 700px of viewport that minimum is never reached and the lock
   is exact, which is why this sat unseen for as long as METScape kept every
   visitor's page 1920px wide. At 360 it won outright: the frame came out
   403x203, a ratio of 1.99 rather than 1.78, hanging 43px off the right of
   the screen with the control strip and the game following it out.

   Zero says "the ratio is the whole of the shape", which is what the three
   declarations under it already claim. Content that does not fit still
   scrolls inside #gstage, which is exactly what that rule's own overflow is
   for; the frame never needed to grow to hold it.

   THE HEADLESS PIN CANNOT SEE THIS, which is worth knowing before trusting
   it. frameBoxAt() in metaeden_net_v4_game_surface.test.js evaluates the
   width and height declarations at four viewports, 375x812 among them, and
   models the arithmetic only. An automatic minimum applied by the layout
   engine is not in the declarations, so that pin stayed green throughout.
   This shape is claimed in a parser and only ever true in a browser. */
/* TASK-1366[Q]: POPPED OUT -- the game and nothing else.

   Set on <body> by an inline script that runs the moment the tag opens, rather
   than by anything that waits for load, so the furniture is never painted and
   then taken away. What is hidden here is the chrome ABOUT the game: the site
   header and nav, the marquee, the way back, and the bar carrying the control
   that opened this window, which in a window opened to escape the frame would
   be a door back into the room you just left.

   THE COLUMN IS FLATTENED, NOT JUST THE FRAME.  This page is a centred 820px
   reading column inside a shell with margins of its own, and sizing only
   #gframe to the viewport gave a 2040px box starting at the column's left edge
   and running off the right of the screen -- the right size, in the wrong
   place, with the page scrolling to make room for it.  Every ancestor between
   the game and the viewport gives up its width, margin and padding here.

   overflow:hidden is the other half of that: with the chrome gone the game is
   exactly one viewport tall, and a page that can still scroll a few pixels
   slides the game under the edge of the window on a trackpad. */
.v4-game-page.gbare { margin: 0; background: #000000; overflow: hidden; }
.v4-game-page.gbare .hd,
.v4-game-page.gbare .ghead,
.v4-game-page.gbare .gmarquee,
.v4-game-page.gbare .gfsbar,
.v4-game-page.gbare .gback,
.v4-game-page.gbare .gnote,
.v4-game-page.gbare footer { display: none; }
.v4-game-page.gbare .shell,
.v4-game-page.gbare .page.gplay {
  width: 100%;
  max-width: none;
  margin: 0;
  padding: 0;
}
.v4-game-page.gbare #gframe {
  width: 100vw;
  height: 100vh;
  height: 100dvh;
  max-width: none;
  aspect-ratio: auto;
  margin: 0;
  /* What every cartridge sizes its canvas against.  Left at the inline
     default a popped-out window is a full-viewport frame with a 461px game
     sitting in the corner of it. */
  --gfs-h: 100vh;
  --gfs-h: 100dvh;
}
.v4-game-page.gbare [data-metaeden-wud],
.v4-game-page.gbare [data-metaeden-wud] #wudRoot { height: 100%; }

.v4-game-page.gfs #gframe {
  position: fixed;
  inset: 0;
  z-index: 1006;
  display: flex;
  flex-direction: column;
  width: min(100vw, calc(100vh * 16 / 9));
  height: min(100vh, calc(100vw * 9 / 16));
  aspect-ratio: 16 / 9;
  min-width: 0;
  min-height: 0;
  margin: auto;
  background: #000000;
  --gfs-h: min(100vh, calc(100vw * 9 / 16));
}

/* WHERE THE MONITOR HALF USED TO BE (TASK-1134[Q], removed by TASK-1158[Q]).
   Two rules stood here, `#gframe:fullscreen` and its `::backdrop`, and they
   existed for one reason: the adapter used to ask the browser to make #gframe
   a real fullscreen element, and the user-agent stylesheet hands such an
   element `width:100%;height:100%`, which would have stretched the frame to
   the SCREEN's aspect and lost the 16:9 this mode is about.  They repeated the
   lock at a selector that outranked it.

   THE USER TOOK THE API BACK OUT, and these went with it: "I don't want to use
   the browser's real full screen mode, I want to leave it up to the user to
   trigger that from their own/actual browser software."  Nothing on this page
   calls requestFullscreen any more, so #gframe can never match :fullscreen,
   and a rule that can never match is a rule that sends its next reader looking
   for the code that would trigger it.  Deleted rather than left as a comment
   with braces.

   WHAT COVERS THE CASE ANYWAY, and why nothing was lost: F11 (or the browser's
   own menu) fullscreens the DOCUMENT, not an element, so no :fullscreen
   selector was ever involved in that path.  All the mode is made of is min()
   arithmetic over 100vw and 100vh, and those simply resolve against whatever
   viewport the browser hands the page -- so a visitor who reaches for their own
   browser's fullscreen gets the mode at the new size for free, which is exactly
   the arrangement the user asked for.
   metaeden-v4-game-fit.css lost its own matching :fullscreen release for the
   same reason on the same ticket. */

/* THE LETTERBOX BARS.  One pseudo-element, fixed over the whole viewport,
   painted behind the frame inside the frame's own stacking context -- so it
   covers the page, the METScape chrome and the CRT wash (body::after, z-index
   200 in metaeden-v4.css) while the frame itself stays on top of it.

   THE WASH DOES NOT PAINT INSIDE WIDESCREEN, and that is a decision rather
   than a side effect.  It is a CRT effect over the SITE's chrome, and this is
   the moment the site's chrome goes away; it also drops a dark lattice over a
   game's own pixels, which is the one place on this surface where nobody has
   measured anything.  It is covered rather than switched off, which is why it
   comes back the instant the mode does: nothing about the visitor's own CRT
   setting is touched, and the frame simply sits on top of it. */
.v4-game-page.gfs #gframe::before {
  content: "";
  position: fixed;
  inset: 0;
  z-index: -1;
  background: #000000;
}

/* THE CONTROL RIDES THE MARQUEE (TASK-1163[Q]).  The user: "remove the 'leave
   full screen' bar and build it into the 'Now playing' bar instead (preferably
   on the right side of the screen)."  So the strip that stood here is gone as a
   ROW: the control goes out of flow and sits in a gap the banner reserves for it
   down its right edge, and the ~46px the row was spending goes to the game.

   READ THE PARAGRAPH THIS REPLACED BEFORE MOVING IT AGAIN, because it warned
   against exactly this shape and it was right about the thing it measured.
   Floating the control at top:10;right:10 put it over the STAGE, where it
   landed on .meta-golf-top -- golf right-aligns its live readout there, so the
   mode cost the player the hole, the par, the strokes and the total, and
   minesweeper and solitaire put counters in the same corner.  The conclusion
   drawn from that was "there is no corner of a nine-cabinet surface that is
   reliably empty", and it still holds: no corner of the STAGE is.  What changed
   is that there is now something else to sit on.  The marquee is CHROME, not a
   cartridge -- this file owns its padding, no engine draws in it, and it is
   blank down the right by construction -- so a control parked there cannot
   cover a readout in any cabinet, present or future.  The old warning is about
   the stage; it is not a warning about the banner.

   THIS IS THE ARCADE PAGE'S OWN ARRANGEMENT, not a new one.  Over there
   .meta-arcade-modal-close is a sibling of .meta-arcade-play-marquee, absolutely
   positioned at top:12px;right:12px over the 112px the banner's own padding
   holds open for it (metaeden.css).  .gfsbar has been that sibling all along, so
   the whole change here is to reserve the gap this page had been zeroing out and
   put the control in it.  The inset below is the arcade's, to the pixel.

   THE RESERVATION IS WIDER THAN THE ARCADE'S because the label is: "Close" fits
   112px and "leave fullscreen" measures 156px at .bbtn's 24px type, 180px with
   the plate's own padding and 192px with the inset.  200px clears it without
   crowding the title, which keeps its minmax(0, 1fr) column and simply gets
   less of the row.

   IT STAYS A SIBLING, AND THAT IS THE ACCESSIBILITY POINT.  The banner is
   aria-hidden -- it is a second RENDERING of the page's own <h2>, so a screen
   reader should meet the title once -- and a focusable control inside an
   aria-hidden subtree is a control a keyboard can reach and a screen reader
   cannot name.  Positioning it over the banner puts it in the same place on the
   glass while leaving it outside that subtree entirely, so it stays a real,
   focusable, announced <button> with the id, the label and the handler it has
   always had.  Nothing in metaeden-v4-game.js was touched for this.

   THE PLATE KEEPS THE THEME'S OWN PAGE BACKGROUND, borrowed from
   `body.<theme>,body.<theme> .browser-viewport` in metaeden-v4.css rather than
   invented, exactly the way this file already borrows its border colours.  That
   choice is what keeps the control's contrast a SOLVED problem instead of a new
   one: neon, dxhr, dark and the unthemed base each give .bbtn a background of
   its own, so the pair is the one F3 already measures, and grey gives it none
   (background:none, color:#0000ee) so its pair becomes ink-on-page, which is
   exactly what F3.grey already measures.  Zero new pairs in any theme.  Pure
   black was the alternative and it fails: #0000ee on #000000 is about 2.4:1.
   The suite pins each borrowed value against the value in metaeden-v4.css, so
   a theme recoloured over there cannot leave a stale colour here.

   The un-themed fallback stays black on purpose.  paintChrome always writes one
   of the four, and the markup ships grey, so a body with no theme class is not
   reachable here; if it ever were, the base .bbtn carries its own #f2f7fc
   background and reads fine on black.

   AND THE PLATE IS NOW THE ONLY THING HOLDING THAT UP, which is why it did not
   go away when the row did.  A strip spanning the frame and a plate in the
   banner's corner paint the same colour behind the same button, but the thing
   BEHIND the plate changed: it used to be the frame's own black, and it is now
   metaeden.css's synthwave strip art with a gradient over it.  Three of the four
   themes would have survived losing the plate on their own -- neon, dxhr and
   dark each give .bbtn an opaque background of its own -- and grey would not:
   its .bbtn is background:none with color:#0000ee, so dropping the plate would
   have put dark blue link text straight onto that artwork.  Grey is also the
   theme the markup ships.  Keeping the plate keeps all four pairs exactly the
   ones the suite already measures, and adds no colour to this file. */
.v4-game-page.gfs .gfsbar {
  position: absolute;
  z-index: 4;
  top: 12px;
  right: 12px;
  align-items: center;
  margin: 0;
  padding: 8px 12px;
  background: #000000;
}
.v4-game-page.gfs.grey .gfsbar { background: #cbcbcb; }
.v4-game-page.gfs.dark .gfsbar { background: #000000; }
.v4-game-page.gfs.neon .gfsbar { background: #0c0b07; }
.v4-game-page.gfs.dxhr .gfsbar { background: #0d0b08; }

/* The cartridge gets the whole frame.  overscroll-behavior keeps a wheel at
   the end of a scroll from chaining to the page underneath. */
.v4-game-page.gfs #gstage,
.v4-game-page.gbare #gstage {
  flex: 1;
  min-width: 0;
  min-height: 0;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: auto;
  overscroll-behavior: contain;
}

/* THE CANVAS CARTRIDGES, and the one rule that grows all of them without
   naming any of them.  Three of the nine draw on a <canvas> with a FIXED
   backing store (golf 720x400, pool 760x420, artillery 760x390) and are sized
   entirely by CSS: metaeden.css gives each width:100%, a max-width, height:auto
   and its own aspect-ratio.  So they do not reflow on a resize -- they scale --
   and scaling is what they were written for.  Their pointer maths divides the
   click by getBoundingClientRect().width, so input stays correct at any
   rendered size.  The arcade floor already relies on exactly this
   (body.metaeden-arcade-site .meta-golf-canvas{width:auto;height:100%}), which
   is why this is the site's idiom and not an invention.

   height + width:auto, NEVER object-fit.  A canvas is a replaced element, so
   object-fit:contain would letterbox the drawing INSIDE an element box of the
   wrong shape -- and getBoundingClientRect returns the element box, not the
   drawn content, so every click would land somewhere the ball is not.  Setting
   the height and letting the authored aspect-ratio derive the width keeps the
   element box at the game's own shape, which is the shape the input maths
   assumes.

   The fraction leaves the game's own chrome rows (the readout above, the
   console below) the rest.  Every one of the three canvases is WIDER than
   16:9 (1.80, 1.81, 1.95 against 1.778), so none of them can ever be wide
   enough to hit max-width and have the clamp break the shape -- and lowering
   the fraction only makes that safer, never less so.

   IT WAS 0.68 UNTIL TASK-1158[Q], and the marquee is why it moved.  That
   ticket added a second chrome row inside the frame, so the budget the 68%
   was chosen against is 68px smaller than it was.  Measured at 1692x1149:
   golf came out 17px taller than the frame and drew a scrollbar down a game
   that had fitted exactly before.  0.64 gives back 38px, which clears it with
   room rather than by a pixel, and no cartridge's playfield is visibly
   smaller for it.  If a third chrome row is ever added here, measure this
   again rather than assuming it still fits: golf is the tightest of the
   three and is the one to check.

   IT WENT THE OTHER WAY IN TASK-1163[Q] AND WAS LEFT AT 0.64 ANYWAY.  That
   ticket took the control's own 46px row back out of the frame, so the budget
   this fraction is measured against grew rather than shrank and every cartridge
   still sized by this rule has more room than it was checked with, not less.
   0.68 would probably fit again (38px of the 46 back, 8px to spare at
   1692x1149), which is a margin thin enough that it wants measuring on all
   three canvases rather than arithmetic on one, and nothing was wrong at 0.64 --
   so it stays until someone has a reason and a rig.  Fishing and strip mine no
   longer pass through here in either mode; they have a contain fit of their own
   further down. */
.v4-game-page #gstage canvas {
  width: auto;
  height: calc(var(--gfs-h) * 0.64);
  max-width: 100%;
  max-height: none;
}

/* THE TWO CARTRIDGES THAT PUBLISH A SCALE KNOB, and the only two lines below
   that name a cartridge at all.  metaeden.css authors --mine-cell on
   .meta-mines-game and --sol-card-w/h/gap on .meta-sol-board as the size of
   one cell and one card: they are the seam that stylesheet provides, and the
   arcade floor turns the same two knobs for the same reason
   (--mine-cell:clamp(32px,2.5vw,50px), --sol-card-w:144px).  Turning a
   published knob is not restyling a game; without it these two would sit at
   24px cells and 78px cards in the middle of a black field and "widescreen"
   would have done nothing for them.  Everything else -- 3d tic tac toe's
   minmax(136px,1fr) columns, word fret, lex trace, the tracker -- fills the
   width it is given on its own, and the suite pins that these two knobs still
   exist under those names so a rename goes red instead of silently doing
   nothing. */
.v4-game-page #gstage .meta-mines-game { --mine-cell: calc(var(--gfs-h) * 0.05); }
.v4-game-page #gstage .meta-sol-board {
  --sol-card-w: calc(var(--gfs-h) * 0.13);
  --sol-card-h: calc(var(--gfs-h) * 0.18);
  --sol-gap: calc(var(--gfs-h) * 0.014);
}

/* THE ONE CARTRIDGE THE RULE ABOVE HAD TO BE TAUGHT ABOUT (TASK-1134[Q]), and
   the reason is that it is the only one carrying TWO canvases that are not the
   same kind of thing.  Vanish Boom 3D paints its world on a canvas that its own
   stylesheet lays out ABSOLUTELY inside the stage (inset:0, 100% by 100%,
   object-fit:contain over a 320x180 buffer) and hangs a MINIMAP canvas in the
   corner at its authored pixel size.  The rule above carries an id, so it wins
   over both of those declarations and would have stretched the world canvas off
   its own box and blown the minimap up to two thirds of the screen.  Neither is
   a scale knob being turned; both are the cartridge's own layout being handed
   back to it. */
.v4-game-page #gstage canvas.meta-vanish-boom-3d-view {
  width: 100%;
  height: 100%;
  max-width: none;
}
.v4-game-page #gstage canvas.meta-vanish-boom-3d-minimap {
  width: auto;
  height: auto;
}

/* ...and then the floor is given the room, which is the point of asking for it.
   The cartridge is already a flex column; letting it have the frame's height and
   the stage have what the readout and the help line do not use is the whole
   change.  aspect-ratio:16/9 on the stage yields to a definite height, which is
   correct here -- the FRAME is the 16:9 thing now, and the world canvas
   letterboxes itself inside whatever shape it is given.

   box-sizing MEASURED, not assumed: the cartridge is content-box and carries 14px
   of padding, so height:100% alone came to 30px more than the frame had and drew
   a scrollbar down the side of a first-person game. */
.v4-game-page #gstage .meta-vanish-boom-3d-game { box-sizing: border-box; height: 100%; }
.v4-game-page #gstage .meta-vanish-boom-3d-stage { flex: 1; min-height: 0; }

/* AND THE SECOND CARTRIDGE OF EXACTLY THAT SHAPE (TASK-1158[Q]).  Dot Matrix:
   XYZ arrived after the block above was written and has the identical build: a
   flex-column game root with content-box padding, and a stage that derives its
   HEIGHT from its width (`aspect-ratio: 5/3` in the cartridge's own sheet, with
   the view canvas absolutely positioned inside it).  Left alone in a frame this
   wide the stage came out 989px tall against a canvas of 647, and the cartridge
   scrolled 364px inside the mode -- measured at 1692x1149, and present since the
   cabinet landed rather than caused by this ticket.  Handing the stage a
   definite height to yield to is the same two lines, for the same reason: the
   FRAME is the 16:9 thing here, and the view canvas already letterboxes itself
   inside whatever shape the stage is given. */
.v4-game-page #gstage .meta-xyz-game { box-sizing: border-box; height: 100%; }
.v4-game-page #gstage .meta-xyz-stage { flex: 1; min-height: 0; }

/* ==========================================================================
   THE ARCADE MARQUEE (TASK-1158[Q]).

   The user asked for "the colorful arcade border that we made for
   arcade.metaeden.net" on this mode, and confirmed with a screenshot which
   element they meant: the synthwave banner that page paints directly above
   its now-playing screen.

   THERE IS NO ART IN THIS BLOCK, AND THAT IS THE POINT.  The element in
   v4-game.html wears arcade-metaeden.html's own class,
   .meta-arcade-play-marquee, and metaeden.css -- which this page already
   loads, first in its ladder -- paints it: the strip artwork, the gradient
   over it, the neon type, the kicker's amber, the rule along the bottom.  One
   definition, two pages, nothing copied and nothing to keep in step.  That is
   why this ticket changed nothing in metaeden.css's marquee rules and could
   not have regressed the arcade page's own look with them.

   WHAT IS LEFT FOR THIS FILE is the handful of places the two surfaces
   genuinely differ, and they hang off the SECOND class on that element,
   .gmarquee, so no selector here names a .meta- cartridge -- the rule the
   suite enumerates.

   ONLY IN THE BIG VIEW.  Out of the mode the page already says what is
   playing, in its own <h2> right above the frame; a banner repeating it would
   be the same words twice on one screen.  In the mode that heading is covered
   by the frame, so the banner is the only thing left saying it -- which is
   also why the markup marks it aria-hidden: it is a second RENDERING of the
   heading, not a second heading. */
.v4-game-page .gmarquee { display: none; }

/* The arcade page reserves 112px down the right of the banner for its Close
   button, which is absolutely positioned over that gap.  THIS FRAME NOW HAS
   SUCH A BUTTON TOO (TASK-1163[Q]) and reserves the same way -- 200px rather
   than 112px, because "leave fullscreen" is a longer word than "Close"; the
   arithmetic is with .gfsbar further up.  IT IS 540px SINCE TASK-1212[Q], and
   that is a measurement of the WORST case rather than a round number.  The way
   home joined the cluster as its leftmost control, and the label beside it is
   not one fixed word: TASK-1208[Q] made #gfsbtn read "back to <floor title>"
   while a cabinet is open, which is longer than "leave fullscreen".  Measured
   live at 1280x800, grey, with golf open on the vanish boom floor: 511px of
   plate, inset 12px from the edge, so 523 is the floor and 540 leaves air for
   a floor title longer than that one.  (The 200px this said before was already
   166px short of that state -- the overrun arrived with the longer label and is
   fixed here rather than merely widened.)  The bar's width does not vary with
   the VIEWPORT, only with that label, so one number clears it at every size the
   mode is reachable at.  Under-reserving does not hide the cluster: it runs the
   banner title beneath it.  Under-reserving does not hide
   the control, it runs the marquee TITLE beneath it, which is the failure the
   reservation exists to prevent.  Until that ticket the reservation was
   zeroed out here, correctly: the control had a row of its own and the gap would
   have been a hole in the art with the title crammed left of it.  The row is
   gone and the hole is now the control's seat, so what was right to remove is
   right to put back.  The rest is the same tightening
   `body.metaeden-arcade-site` already applies to the banner over there, for
   the same reason: standalone it is a page header and can be generous, inside
   a fixed 16:9 screen it is chrome and every row it takes is a row the game
   does not get. */
/* AND IT IS TIGHTER THAN THE ARCADE'S, which is a measurement rather than a
   taste.  Over there the banner sits above a cabinet that is as tall as the
   window; here it sits inside a frame locked to 16:9, so every row it takes is
   a row the game does not get, and the game is what the visitor pressed the
   button for.  At the banner's own 2.45rem heading and 18px padding it came to
   105px of a 952px frame -- eleven per cent, against seven on the arcade page.
   The numbers below bring it to about seventy, which is the same proportion
   the arcade spends, and the heading is still the largest type on the surface. */
/* THE ROW'S HEIGHT IS BOUNDED, NOT CONTENT-DRIVEN (TASK-1236[Q]).  The user,
   on a Meta Quest: "most games don't work with the arcade game because the
   title is so long it takes up multiple rows pushing the space for the game
   down so small that you can barely see anything."  THE MEASUREMENT, on this
   rig, at the viewport server/metaeden_net_frame_fits_viewport.test.js calls
   the Quest band (roughly 1200-1600 wide, 700-800 tall -- a Quest window is
   wider than it is tall, so the frame here is narrower than a desktop's even
   though the viewport is not phone-narrow): #gframe is flex-column and this
   row was flex:0 0 auto, sized to whatever its own content asked for, with
   #gstage taking flex:1 -- the leftover.  At 1280x720, "spam clicker: cookie
   cleaner" wrapped its title to four lines in a 141px-wide column (the
   description text in the row's other column was eating the rest), and the
   row came to 162px of a 720px frame -- 22 percent gone before the game
   painted a pixel.  At 1200x700, "vanish boom 3d" -- fourteen characters --
   still wrapped three lines, because the title's column had been squeezed to
   ZERO by that same description.  Two tightening passes on this chrome
   already (TASK-1186[Q], TASK-1189[Q]) fixed the SITE header above this one
   and never touched this row, which is why the fight kept being lost here.

   THE RULE: a row whose height depends on how long a string is has no floor.
   The next game can always be named something longer, so the fix is not a
   smaller font or a shorter reservation (both are still just today's longest
   name wearing a new number) -- it is taking the wrap away entirely.  Title
   and description are each clamped to ONE line with an ellipsis; a line's
   height is set by its font-size, which does not change with the string, so
   the row's height stops being a function of content and starts being a
   constant.  min-width:0 is what makes the clamp real: a grid item's
   automatic minimum is its own min-content otherwise, so text-overflow would
   never get the chance to fire and the column would just push the frame
   wide -- exactly the failure mode B/C already guard on the frame itself in
   the viewport-fits suite, one level down.

   THE COLUMN SPLIT MOVES FROM auto TO minmax(0, 1fr), for the reason the
   measurement above names: an auto (max-content) column has no ceiling, so a
   long description could still claim the whole row and leave the title a
   sliver to ellipsize inside, which is legible but not a fix worth shipping.
   2fr/1fr says the title always outweighs the description by the same
   proportion at every frame width, rather than pinning either to a pixel
   count that the next redesign of the control cluster's own reservation
   (540px, just above -- itself already a measured worst case, not a round
   one) would silently invalidate.

   THE 12PX FLOOR NEVER BINDS HERE.  The title stays at its authored 1.6rem
   (25.6px) -- CLAUDE.md's floor is what rules out shrinking it to fit
   instead, and there is no need to: even the ZERO-width case above still
   read as an ellipsis at full size once this shipped, never as smaller type.
   The full string is not lost: the adapter (metaeden-v4-game.js) mirrors it
   onto both elements' native `title` attribute, so a pointer that can hover
   -- including a Quest controller's own ray -- gets it back as a tooltip,
   and the real, un-clamped heading (#gname, above #gframe in the DOM) is
   what a screen reader hears; .gmarquee stays aria-hidden, exactly as before
   this ticket, since it has always been a second RENDERING of that heading
   and not a second heading.

   overflow:hidden on the row itself is the backstop, not the mechanism -- if
   either child breaks its own clamp because a following ticket touches this
   block again, the row still cannot grow past its now-constant content, and
   server/metaeden_net_v4_game_title_bounded.test.js pins the property (that
   the row's height cannot vary with the strings it is handed) rather than
   any pixel count or any title from today's table, so a longer name added
   next month cannot turn it red the way a frozen list would. */
.v4-game-page.gfs .gmarquee {
  display: grid;
  grid-template-columns: minmax(0, 2fr) minmax(0, 1fr);
  flex: 0 0 auto;
  gap: 2px 18px;
  min-height: 0;
  padding: 8px 540px 8px 16px;
  overflow: hidden;
}
/* THE TITLE'S OWN INK IS HANDED BACK TO IT (TASK-1172[Q], audit finding).
   The banner is borrowed wholesale from the arcade page, and metaeden.css
   authors its title at (0,1,1): white Trebuchet 800 over the strip art.
   This page's THEMES author `body.<theme> h2` at (0,1,2), which outranks
   that -- so in grey the marquee title rendered as underlined link-blue
   Times small-caps over synthwave artwork, the exact "wearing somebody
   else's chrome" the borrow existed to prevent.  The declarations below
   restate the ARCADE'S OWN values at a specificity the themes cannot
   reach: the `font` shorthand carries the borrowed family and weight (and,
   being a shorthand, resets grey's small-caps with it) at this frame's
   tightened size; the colour is metaeden.css's own #ffffff, borrowed
   exactly the way this file borrows its border and plate colours, and the
   suite pins the two values equal so a recolour over there cannot leave a
   stale copy here.  This is the ONE colour this stylesheet declares, and
   the no-ink pin names it as the one exception: the pair (white on the
   banner's own dark-graded art) is the arcade page's shipped pair, not a
   new one.
   min-width:0, white-space:nowrap, overflow:hidden and text-overflow:ellipsis
   are the bounded-height rule from the block comment above, applied to the
   title specifically. */
.v4-game-page.gfs .gmarquee h2 {
  min-width: 0;
  overflow: hidden;
  font: 800 1.6rem "Trebuchet MS", Arial, sans-serif;
  color: #ffffff;
  text-decoration: none;
  white-space: nowrap;
  text-overflow: ellipsis;
}
/* THE DESCRIPTION GETS THE SAME CLAMP, for the same reason: it is the OTHER
   variable-length string sharing this row (#gmarqsub, the game's own blurb),
   and the "vanish boom 3d" measurement above is what a long description does
   to a short title when only one of the two is bounded.  No colour or size
   declared here -- both are metaeden.css's own p:last-child rule already,
   untouched -- this only adds the clamp. */
.v4-game-page.gfs .gmarquee p:last-child {
  min-width: 0;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
}

/* ==========================================================================
   TWO MORE CARTRIDGES HANDED THEIR OWN LAYOUT BACK (TASK-1158[Q]), for
   exactly the reason the vanish-boom-3d block above already gives, and it is
   worth reading these two together.

   THE GENERIC CANVAS RULE NEAR THE TOP OF THIS SECTION SIZES EVERY <canvas>
   IN THE STAGE, and it was written when every cabinet had exactly one.  Two
   of the newer ones do not.  Fishing hangs a row of 16x16 species chips over
   its playfield and a field guide of 40x28 cards behind it; strip mine hangs
   16x16 inventory chips and 32x32 hotbar slots.  None of those is a
   playfield, and the generic rule carries an id, so it outranks each
   cartridge's own declaration and set all of them to 68% of the frame height.

   MEASURED ON THE RIG, before and after, because this shipped broken and the
   numbers are the clearest thing to leave behind: at a 1692x1149 viewport the
   big view's frame is 1692x952, so the rule made every one of those icons
   647px square.  Fishing's chip row alone came to 2637px, taking the cartridge
   to 3379px inside a 906px stage; strip mine reached 4051px.  Both scrolled
   about three and a half screens for a game that should have needed none.

   THE FIX IS THE SAME ONE THE MINIMAP GOT: give the icons back the size their
   own stylesheet authored, which is the size their width/height attributes
   already say.  This is layout handed back, not a cartridge restyled -- no
   colour, no font, no border, no background, and nothing that would look
   different if the generic rule had never reached them.

   NOTE FOR THE NEXT CABINET: this list grows by hand, and it should not have
   to.  server/metaeden_net_v4_game_surface.test.js now fails when a cartridge
   ships an auxiliary canvas that no rule here has been taught about, so the
   next one goes red on arrival instead of shipping at 647px square.

   XYZ'S MAP IS IN THE LIST WITHOUT EVER HAVING BEEN SEEN TO BREAK, and that
   is deliberate.  Its overlay ships closed, so its canvas measures 0x0 and the
   generic rule has nothing to inflate -- until a player presses the key that
   opens it, which no headless pin and no screenshot was ever going to catch.
   The guard below found it by reading the cartridge's own stylesheet instead
   of the rendered page, which is the whole argument for having it. */
.v4-game-page #gstage .fsx-chip canvas,
.v4-game-page #gstage .fsx-card canvas,
.v4-game-page #gstage .smx-chip canvas,
.v4-game-page #gstage .smx-slot canvas,
.v4-game-page #gstage .meta-xyz-map canvas {
  width: auto;
  height: auto;
  max-width: none;
}

/* ...AND THE SCREEN THOSE TWO DRAW ON IS CENTRED WITH ITS OWN OVERLAYS
   (TASK-1158[Q]).  Sizing the canvas was only half of it, and the other half
   is visible the moment you look at the mode rather than measure it.

   The generic rule above gives the playfield canvas the right SHAPE, but its
   holder -- `.fsx-scr` / `.smx-scr` -- is a flex item that still stretches to
   the full width of the frame.  Two things go wrong at once, and they were
   photographed on the rig before this rule existed: the picture sits hard
   against the LEFT edge with the whole letterbox pooled on the right, and
   every overlay registered to that holder goes with the holder rather than
   with the picture.  For fishing that means the name plate (absolute, at
   right:14px of `.fsx-scr`) hangs in the black a third of a screen away from
   the game it names -- which is the same complaint that opened this ticket,
   arriving from the opposite direction: there it was cropped, here it is
   adrift.

   width:max-content shrinks the holder onto the canvas the rule above just
   sized, and the auto margins centre it.  The overlays then have the picture's
   own box to sit in, so the plate returns to the corner of the picture.

   THIS IS THE SAME IDEA AS THE ARCADE PANEL'S FIX in metaeden.css (search
   `.fsx-scr` there).  TASK-1158[Q] kept the two deliberately apart, on the
   grounds that over there the panel hands the holder a HEIGHT and the width has
   to be derived, while here the canvas was already sized by --gfs-h and the
   holder only had to stop being wider than it.  TASK-1163[Q] made the two the
   same problem, so the rules below are now the arcade's, and the reasoning
   still lives over there.

   WHAT --gfs-h COULD NOT SEE.  The old sizing gave the canvas 64% of the
   FRAME's height, which is a fraction of the whole frame rather than a
   measurement of what was left of it -- so it could not know what the marquee,
   the cartridge's own two chrome rows and its touch pad had already spent, and
   it never looked at the width at all.  Measured at a 2200x1300 viewport: the
   stage filled its 2200x1124 correctly and the picture inside it came out
   1410x794, leaving BOTH axes short at once -- 768px of unused width and 137px
   of unused height, with the game sitting in the middle of it.  That is the
   screenshot that opened the ticket.

   THE FIT IS A REAL CONTAIN NOW, and it is four small jobs rather than one
   clever declaration.  The chain from #gstage down to the screen is given a
   definite height (the mount, then the cartridge root, which is content-box and
   so needs border-box to mean what it says); the two chrome rows and the pad
   are pinned to their own height; the screen takes everything left over; and
   its own aspect-ratio turns that leftover height into the width that goes with
   it.  Whichever axis runs out first is the one that decides, which is what the
   user asked for in the words "stop at whichever axis hits the edge first".
   Measured the same way afterwards, with the control's row gone as well: the
   stage is 2200x1170 and the picture is 1737x977, up from 1410x794 -- half as
   much picture again, and the letterbox is only what 16:9 costs in a frame that
   is not 16:9 once its chrome comes out.

   THE HOLDER IS THE PICTURE, WHICH IS THE HALF THAT IS EASY TO LOSE.  Every
   overlay these two cartridges own -- fishing's name plate at right:14px, both
   start screens at inset:0 -- is registered to .fsx-scr / .smx-scr, so the
   holder has to end up exactly the size of the drawing or they go adrift in the
   black, which is the bug the block above this one exists to fix.  That is why
   the ratio sits on the HOLDER and the canvas merely fills it, rather than the
   canvas being sized and the holder shrink-wrapped: a holder that shrink-wraps
   has to measure a canvas whose height is a percentage of the holder, and the
   browser resolves the intrinsic width first and hands back the 320px buffer.

   AND THE CANVAS KEEPS ITS OWN RATIO ANYWAY, which is belt and braces worth
   having.  aspect-ratio on the holder is exact while the leftover HEIGHT is
   what binds, and that is the only case this mode can produce: the frame is
   locked 16:9 and the chrome comes out of its height, so the stage is always
   wider than 16:9 and always wider than either canvas (16:9 and 20:11).  Should
   the width ever bind instead -- a third chrome row, a narrower cabinet, a
   media query -- max-width would clamp the holder against a height flexbox has
   already fixed, and the holder alone would go out of shape: measured 0.62
   against 1.78 when the width was squeezed deliberately to force it.  Giving
   the canvas width:100%;height:auto instead of 100% of both means the picture
   letterboxes inside a wrong-shaped holder rather than stretching with it, so
   the failure that case degrades to is a plate a little off the corner, never a
   distorted game.  box-sizing is border-box because the canvas carries a 1px
   border of its own and 100% of a content box plus that border overflowed the
   holder by 2px. */
.v4-game-page.gfs #gstage > [data-metaeden-fishing],
.v4-game-page.gbare #gstage > [data-metaeden-fishing],
.v4-game-page.gfs #gstage > [data-metaeden-stripmine],
.v4-game-page.gbare #gstage > [data-metaeden-stripmine] { height: 100%; }

.v4-game-page.gfs #gstage .fsx,
.v4-game-page.gbare #gstage .fsx,
.v4-game-page.gfs #gstage .smx,
.v4-game-page.gbare #gstage .smx {
  box-sizing: border-box;
  height: 100%;
  min-height: 0;
}

.v4-game-page.gfs #gstage .fsx > *,
.v4-game-page.gbare #gstage .fsx > *,
.v4-game-page.gfs #gstage .smx > *,
.v4-game-page.gbare #gstage .smx > * { flex: 0 0 auto; }

.v4-game-page #gstage .fsx-scr,
.v4-game-page #gstage .smx-scr {
  width: max-content;
  max-width: 100%;
  margin-right: auto;
  margin-left: auto;
}

.v4-game-page.gfs #gstage .fsx-scr,
.v4-game-page.gbare #gstage .fsx-scr,
.v4-game-page.gfs #gstage .smx-scr,
.v4-game-page.gbare #gstage .smx-scr {
  flex: 1 1 auto;
  width: auto;
  min-height: 0;
}
.v4-game-page.gfs #gstage .fsx-scr,
.v4-game-page.gbare #gstage .fsx-scr { aspect-ratio: 16 / 9; }
.v4-game-page.gfs #gstage .smx-scr,
.v4-game-page.gbare #gstage .smx-scr { aspect-ratio: 20 / 11; }

.v4-game-page.gfs #gstage canvas.fsx-cv,
.v4-game-page.gbare #gstage canvas.fsx-cv,
.v4-game-page.gfs #gstage canvas.smx-cv,
.v4-game-page.gbare #gstage canvas.smx-cv {
  box-sizing: border-box;
  width: 100%;
  height: auto;
  max-width: 100%;
  max-height: 100%;
}

/* OUTSIDE THE BIG VIEW, THE AXIS THAT MATTERS FLIPS.  The user, looking at
   fishing in the ordinary window: "can't we make the game fill the width
   available here? it can be as tall as needed to keep ratio at the width
   that can still be filled here."

   The generic rule above sizes the playfield off --gfs-h, which is right in
   .gfs -- there the frame IS the viewport and height is the thing in short
   supply -- but wrong here, where .gplay is capped at a fixed 820px (see the
   top of this file) and the frame's height is only a FLOOR, free to grow.
   Measured: at 820px wide, --gfs-h in this mode is a constant 461px, so the
   generic rule's 64% draws fishing's exactly-16:9 canvas at 295px tall and
   527px wide -- 293px of the column going unused on either side, which is
   the gap in the screenshot.

   BOTH ENGINES ALREADY SIZE THEMSELVES THE WAY THE USER WANTS, and this rule
   is nothing but getting out of their way: metaeden-fishing.js's own
   stylesheet has `.fsx-scr canvas.fsx-cv{width:100%}` with no CSS height at
   all, which leaves a <canvas> to do what a replaced element always does --
   derive its height from its own intrinsic ratio, 320x180 here, exactly
   16:9.  metaeden-stripmine.js's canvas is the same idea at 640x352 (20:11).
   The generic rule only ever overrides that because it carries the higher
   specificity `#gstage canvas`; naming the class beats it back.

   :not(.gfs) is the whole of the change.

   AND IT IS THE SAME FIT AS THE MODE'S, WHICH IS WHY THE TWO STILL READ
   DIFFERENTLY (TASK-1163[Q]).  It is fair to ask why the block above does not
   simply cover both.  A contain fit stops at whichever axis runs out first, and
   out here only one of them ever does: .gplay caps the column at 820px, while
   the frame's height is a min-height and nothing above it is bounded, so the
   page just gets taller and the height axis never runs out at all.  Feed an
   unbounded height into a contain fit and every term that mentions height drops
   out of it; what is left is width:100% with the ratio deriving the rest, which
   is the rule already written below.  So this is not a second technique sitting
   next to the first, it is the same one with the height constraint removed --
   and writing it that way keeps the mode's four-part chain, which exists only to
   discover a leftover height, out of a mode that has no leftover to find. */
.v4-game-page:not(.gfs):not(.gbare) #gstage .fsx-scr,
.v4-game-page:not(.gfs):not(.gbare) #gstage .smx-scr {
  width: 100%;
}
.v4-game-page:not(.gfs):not(.gbare) #gstage canvas.fsx-cv,
.v4-game-page:not(.gfs):not(.gbare) #gstage canvas.smx-cv {
  width: 100%;
  height: auto;
  max-width: 100%;
  max-height: none;
}

/* ==========================================================================
   AND THE THIRD CARTRIDGE OF THAT SHAPE, WHICH IS NOT A CANVAS AT ALL
   (TASK-1214[Q]).  The user, on cabinet 14: "make sure this game is specially
   1080p ratio."

   THE RATIO ITSELF IS NOT HERE, AND THAT IS THE POINT OF THIS BLOCK.  Spam
   Clicker's desk is a DOM desktop, so unlike fishing and strip mine above it
   has no canvas whose intrinsic size could speak for it; it now declares
   `aspect-ratio: 16/9` on .scx-scr in its own stylesheet, beside the number it
   replaces.  That is where a game's shape belongs -- this file's own rule two
   screens up is that naming a cartridge here is allowed only to turn its
   published knob or to hand its layout back, and deciding what shape a game is
   would be neither.  It also has to travel: cabinet 14 is still owed its slot
   on the arcade wall (see EXPECTED_ABSENT in server/lib/arcade-floor.js), and a
   ratio written in this file would not be there when it lands.

   WHAT IS HERE IS THE DEFINITE HEIGHT THE RATIO NEEDS TO YIELD TO, which only
   the surface can know.  Out of the mode the cartridge needs nothing from us:
   .gplay caps the column, the frame's height is a floor free to grow, so width
   binds, the ratio derives the height, and the desk comes out 798x450 -- the
   same shape and within a pixel of the same size as Gone Fishin's screen in
   that view (798x451, measured side by side).  The paragraph above the
   :not(.gfs) rules says why that asymmetry is one technique and not two.

   IN THE MODE THE AXIS FLIPS AND THE FRAME IS A HARD 16:9, so without these
   rules the desk kept the height it happened to have and stretched to the
   frame's full width: measured 1801x848, a 2.12:1 strip inside a 1823x1025
   cabinet, with 18px of the stage simply unused underneath it.  The chain is
   the fishing one, one element longer because this cartridge has chrome on
   BOTH sides of its screen: the mount takes the stage's height, the root is
   content-box and so needs border-box to mean 100%, the top and bottom strips
   are pinned to their own size, the middle row takes what is left, and the
   screen fills that row's height while its own ratio derives the width.
   Measured after: 1540x866, exactly 1.778, centred with the leftover width as
   pillarbox and zero overflow on either axis -- 18px TALLER than it was, and
   the letterbox is only what 16:9 costs in a stage that is 1.91 once the
   marquee comes out of the frame.

   justify-content is the whole of why it centres.  .scx-mid is the cartridge's
   own flex row and was left stretching one full-width child; a child that now
   derives a narrower width would otherwise sit hard against the left edge with
   the pillarbox pooled on the right, which is exactly the bug the fishing
   block above this one exists to fix, arriving by a different route. */
.v4-game-page.gfs #gstage > [data-metaeden-spamcleaner],
.v4-game-page.gbare #gstage > [data-metaeden-spamcleaner] { height: 100%; }

.v4-game-page.gfs #gstage .scx,
.v4-game-page.gbare #gstage .scx {
  box-sizing: border-box;
  height: 100%;
  min-height: 0;
}

.v4-game-page.gfs #gstage .scx-top,
.v4-game-page.gbare #gstage .scx-top,
.v4-game-page.gfs #gstage .scx-bot,
.v4-game-page.gbare #gstage .scx-bot { flex: 0 0 auto; }

.v4-game-page.gfs #gstage .scx-mid,
.v4-game-page.gbare #gstage .scx-mid {
  flex: 1 1 auto;
  min-height: 0;
  justify-content: center;
}

.v4-game-page.gfs #gstage .scx-scr,
.v4-game-page.gbare #gstage .scx-scr {
  flex: 0 1 auto;
  width: auto;
  height: 100%;
  min-height: 0;
}

/* ==========================================================================
   AND THE FOURTH, WHICH BRINGS ITS OWN 1920x1080 WITH IT (TASK-1260[Q]).

   Rink of Destruction is not fitted here the way the three cartridges above
   are.  TASK-1258[Q] gave it a stage of its own -- .rink-fit-stage, a literal
   1920x1080 box that its whole DOM is authored against, transform-scaled by
   min(frameW/1920, frameH/1080) -- so this surface has nothing left to decide
   about its shape.  It decides ONE thing, which is how big a box the stage is
   drawn into, and both rules below are about handing that box over intact.

   THE MOUNT TAKES THE STAGE'S HEIGHT, same line the three cartridges above
   needed and for a reason that is specific here.  The cartridge's frame is
   `height:100%` with `aspect-ratio:16/9` behind it, and that fallback is what
   keeps it honest in ordinary document flow, where a host hands it a width and
   no height at all.  #gstage is not that host: it has a definite height and
   its child does not, so `100%` computed to auto, the fallback fired, and the
   frame derived 1031px of height from 1834px of width inside a stage that had
   968.  MEASURED at 2000x1040, which is the report: 64px of overflow, a
   scrollbar down a game that letterboxes precisely so it will never need one,
   and the locker sidebar's bottom 61px past the frame.  The arcade floor never
   showed it because its own panel rule already gives the mount a height.

   AND THE PLAYFIELD IS HANDED BACK, which is the vanish-boom-3d case two
   screens up, arriving through the one seam a fixed stage cannot defend.  The
   generic canvas rule carries an id, so it outranks the cartridge's own
   `.rink-fit-stage .rink-canvas{width:100%;height:100%}` and sizes the ice off
   --gfs-h -- a VIEWPORT length, reaching inside a box whose whole purpose is
   that it is 1920 design px wide on any screen.  Measured at the same
   viewport: 1065x666 design px of ice in a 1566x977 section, the picture
   pinned top-left of its own frame while the scoreboard, the rosters and the
   announcer stayed centred on the section around it.  Nothing was stretched
   and nothing was cropped, so a screenshot reads it as "the layout is wrong"
   rather than as a sizing rule -- which is what the report said.
   Both modes, unlike the mount above: the ordinary view drew the same ice at
   472x295 for the same reason, off its own smaller --gfs-h.

   THE HEIGHT IS auto RATHER THAN 100% (TASK-1261[Q]), and the difference is the
   whole of what this hand-back means.  It is not "fill the box": it is "the
   cartridge decides", and what the cartridge says is width:100% with its own
   aspect-ratio:8/5 deriving the rest.  Since the rink adopted its full-page
   sidebar the ice no longer fills its section's height -- 952.5 of a 987px row,
   with the difference letterboxed by the section's own align-content:center --
   so 100% would stretch a 1600x1000 backing store into a 1.56 box and distort
   the picture.  auto keeps the element box and the drawn box the same box,
   which is what the pointer maths and the scoreboard's seating both read. */
.v4-game-page.gfs #gstage > [data-metaeden-rink],
.v4-game-page.gbare #gstage > [data-metaeden-rink] { height: 100%; }

.v4-game-page #gstage canvas.rink-canvas {
  width: 100%;
  height: auto;
  max-height: none;
}
