/* ==========================================================================
   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; }
/* IT IS NOT SHOWN IN BARE MODE, AND THAT IS THE CORRECTION (TASK-1488[Q]).
   TASK-1487[Q] lit it here because the bar was the only exit a popped-out
   window had.  The user's answer to seeing it: "the pop out still has this
   grey bar that is no good, get rid of the grey bar when popped out."  So the
   whole bar goes in that mode (see the rule further down) and the exit lives
   in a control row instead -- the cartridge's own where it declares one, the
   host's own where it does not.  This link is still the paint that row's
   button borrows, which is why it stays in the markup rather than being
   deleted: metaeden-v4-game.js reads its computed ink when there is no
   cartridge button to read.  A display:none element still computes a colour. */
/* 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 .gback,
.v4-game-page.gbare .gnote,
.v4-game-page.gbare footer { display: none; }


/* THE BAR GOES. ALL OF IT, AND WITHOUT WAITING FOR ANYTHING (TASK-1488[Q],
   round two).  The user, with a screenshot of a popped-out Spam Clicker: "the
   pop out still has this grey bar that is no good, get rid of the grey bar when
   popped out."

   WHAT THEY WERE LOOKING AT, and why the first cut left it there.  The bar used
   to go only once the exit had landed in the cartridge's own control row, and
   the host found that row by a marker two cartridges declared.  Spam Clicker
   declares a control row -- SOUND / WIPE SAVE, the exact "same level as the
   Fullscreen, Sound : On buttons" the ask names -- and had never been given the
   marker, so the gate did what it was written to do and kept the bar.  Nineteen
   other cabinets were in the same position.  The gate was right and its reach
   was not.

   SO THE GUARANTEE MOVED INSTEAD OF WIDENING.  It used to be a CSS gate: hide
   the bar only where .gexit says a replacement exists.  It is now a property of
   metaeden-v4-game.js's syncExit, which in bare mode ALWAYS leaves a reachable
   exit -- in the cartridge's own row where one is declared and on screen, and in
   a host row of its own where none is.  There is still no state with neither;
   what changed is that the fallback is one control on the game's own frame
   rather than a strip of page chrome carrying three.

   AND IT IS CSS RATHER THAN A CLASS THE ADAPTER ADDS, deliberately.  This
   stylesheet is render-blocking and metaeden-v4-game.js is deferred, so a rule
   waiting on a class the adapter sets is a rule that paints the grey bar first
   and takes it away afterwards -- in front of whoever just opened the window,
   which is the failure the inline gbare script at the top of v4-game.html
   exists to avoid for the header and the marquee.  The same reasoning, the same
   answer.

   POP OUT WENT WITH IT.  Its own rule hid it here (it opens this same surface,
   so in this mode it reloads the window you are already in); with the bar gone
   there is nothing left for that rule to say. */
.v4-game-page.gbare .gfsbar { display: none; }

/* THE HOST'S OWN ROW (TASK-1488[Q]) IS THE GAME MENU'S SYSTEM ROW NOW
   (TASK-1634[Q]).  metaeden-v4-game.js builds .gexitrow in bare mode for the
   two controls this page owns -- Share Game Link w/Friend and Return To
   MetaEden.Net -- and it used to stand in the tray band, wearing one plate per
   theme borrowed from the fullscreen bar.  It stands inside the game menu panel
   now, under the SYSTEM heading, and is dressed by the panel rules further down
   (search for "THE TITLE-SCREEN SYSTEM LAYER").  It has no plate of its own any
   more: the panel is the plate, in every theme, which is what lets a bare
   window drop the tray without dropping the way out.  Its old layout rule and
   the four theme plates are gone rather than left matching nothing (recover
   from 24d6c217b). */

/* THE RESERVED ROW ALONG THE BOTTOM, AND WHY IT IS ONE ROW (TASK-1544[Q]).
   The user, with a screenshot of a popped-out Spam Clicker: "on pop-outs if we
   still have IMs, can we at least put them on a reserved row at the bottom so
   that it's not blocking/overlapping the content? move the content up a few
   pixels above the reserved row for IMs, make it look like a taskbar/tray."

   WHAT WAS OVERLAPPING.  met-windows.js paints three taskbar controls at the
   corners of the real viewport at bottom:0 -- the minimized-chat chips on the
   left, METAMP and the "(N) Buddies Online | ME|T|List" button on the right.
   That placement is correct on metaeden.net and has its own paragraph over
   there: they are the DESK'S taskbar, and the desk is what the fake browser
   sits on.  A popped-out game has no browser and no desk, so those three
   plates were simply standing on the game's own bottom row.

   IT TAKES ITS HEIGHT; IT DOES NOT COVER.  #gframe is a flex column and
   #gstage is flex:1, so a band appended after the stage shortens the stage by
   exactly its own height and the browser does the arithmetic.  That is the
   same in-flow answer .gexitrow gave the exit one ticket ago, applied to the
   rest of the furniture -- and it is why nothing here is positioned.  A fixed
   band would be the bug wearing a plate.

   THE RETURN TAB WAS IN IT, AND IS NOT ANY MORE (TASK-1634[Q]).  From
   TASK-1544[Q] to TASK-1632[Q] the band's right-hand end carried the host's
   own controls and the cartridge's relocated bank, so that the bottom of the
   window was one strip and not two.  The user then asked for the bare page to
   read as a standalone title screen, which means NO strip at all by default:
   the tray is hidden until the visitor switches it on from the game menu, and
   every control that used to live in it lives in that menu instead (the panel
   rules further down).  What the band carries when it is on is exactly the
   two met-windows.js ends -- chips and status controls -- plus the game's own
   tagline in the leftover space.

   ITS HEIGHT IS FIXED, not content-driven.  A band that grew when a chip
   arrived would reflow the stage under a round already in progress, which is
   the fixed-frame rule this codebase states for every panel.

   44px, sized to the tallest thing that docks into it, which is a
   met-windows.js chip (13px Tahoma in a bevelled plate, 30 tall), with the
   period taskbar's own breathing room around it.  The exit used to be the
   tallest occupant (.bbtn came out at 40) and set this number; it left for
   the menu and the number stayed, because a 44px tray is the tray the user
   already signed off on and reflowing it for 10px is not a favour to anyone.
   met_popout_exit_contrast_rig.test.js measures the chip against this box.
   (Written without quoting the declaration: F6 in the sibling suite scans
   this file for type sizes and reads the prose as well as the rules, which is
   exactly the trap CLAUDE.md names.)

   THE PLATE IS THE FULLSCREEN BAR'S.  Four lines and not one, the same H35
   discipline, borrowed from the same twins -- so a theme recoloured over there
   moves both together and this file still invents no paint.  The bevel is a
   box-shadow rather than a fifth set of theme colours: a raised top edge and
   a shaded bottom in translucent white and black read as a 1996 tray on all
   four plates at once, which is what met-windows.js's own chips already do
   for their shadow.

   OFF BY DEFAULT, AND DECIDED BEFORE ANYTHING PAINTS (TASK-1634[Q]).  The
   band exists in every bare state (metaeden-v4-game.js still reserves it, so
   met-windows.js still finds its two ends and docks into them) but it is
   display:none unless <body> carries .gtrayon.  That class is put there by
   the inline script at the top of v4-game.html, which reads the per-browser
   preference (localStorage "met.bare.taskbar", "1" = shown) in the same
   breath as it reads ?bare=1 -- so a window that remembers the tray ON paints
   it on its first frame, and a window that does not never paints it at all.
   The same reasoning as the .gfsbar rule above: this sheet is render-blocking
   and the adapter is deferred, so a rule waiting on the adapter is a rule that
   flashes.  The game menu's "Show taskbar" switch toggles the class and writes
   the preference; the tray then comes back IN FLOW under the stage, the stage
   shrinks by exactly the band's height, and nothing floats over the game.

   ...AND EVERY SURFACE THAT BELONGS TO THE TRAY GOES WITH IT.  met-windows.js
   parents its chips into the band's ends, so those hide with the band.  Its
   OPEN windows (#mwaway: the IMs, the buddy list, METAMP) and its tooltip are
   fixed to the viewport, and the friends door (.sgf-win) is a fixed window of
   its own, so each is named here and hidden on the same class.  Switching the
   tray on shows all of them again exactly where they were. */
.v4-game-page.gbare .gtray {
  flex: 0 0 auto;
  box-sizing: border-box;
  display: flex;
  align-items: center;
  gap: 6px;
  height: 44px;
  margin: 0;
  padding: 0 6px;
  box-shadow: inset 0 2px 0 rgba(255, 255, 255, 0.32),
              inset 0 -2px 0 rgba(0, 0, 0, 0.45);
}
.v4-game-page.gbare.grey .gtray { background: #cbcbcb; }
.v4-game-page.gbare.dark .gtray { background: #000000; }
.v4-game-page.gbare.neon .gtray { background: #0c0b07; }
.v4-game-page.gbare.dxhr .gtray { background: #0d0b08; }
.v4-game-page.gbare:not(.gtrayon) .gtray,
.v4-game-page.gbare:not(.gtrayon) #mwaway,
.v4-game-page.gbare:not(.gtrayon) #mwtip,
.v4-game-page.gbare:not(.gtrayon) .sgf-win { display: none; }

/* THE TWO ENDS (TASK-1544[Q]).  A tray has task buttons at one end and status
   controls at the other, and met-windows.js needs to be told which is which --
   so the band carries two marked slots and that file addresses them by the
   marker rather than by knowing anything about this page.  So the band is two
   flex children:

     [ tasks (chips + the game's tagline) ][ sys (METAMP + buddies + friends) ]

   THERE WAS A THIRD ZONE from TASK-1545[Q] to TASK-1632[Q]: the cartridge's
   relocated SOUND / WIPE bank, or the host's own exit row, at the right-hand
   end, flex:0 0 auto so the way out could never be squeezed.  TASK-1634[Q]
   moved all of that into the game menu panel, and the zone went with it; there
   is no [data-met-controls] element in the band any more and no rule here
   for one.

   THE PRIORITY IS MEASURED, NOT GUESSED (TASK-1545[Q]).  At common pop-out
   widths (640..1280) both ends fit; below that the chip end shrinks first
   (flex:1, its own ellipsis clips a long chat row) and the status end shrinks
   next (flex:0 1, the buddies label truncates -- met-windows.js carries the
   ellipsis rule for its own in-tray label).  With nothing rigid left in the
   row it cannot overflow the glass at any width down to a phone's, which is
   what the fit sheet's 375px promise now rests on.  Nothing wraps and nothing
   scrolls, so the band stays one fixed-height strip. */
.v4-game-page.gbare .gtray-tasks {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  overflow: hidden;
}
.v4-game-page.gbare .gtray-sys {
  /* was flex:0 0 auto; it shrinks now so its label truncates before the
     controls end has to give up a pixel (TASK-1545[Q]). */
  flex: 0 1 auto;
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 4px;
  overflow: hidden;
}
/* THE GAME'S OWN TAGLINE, MOVED INTO THE TRAY'S LEFT (TASK-1547[Q]).
   metaeden-v4-game.js's relocateCaption drops the marked caption -- each
   cartridge's one-line help/tagline, e.g. .meta-vanish-boom-3d-help -- into the
   tasks zone, which is the dead LEFT space the user's arrow pointed at (the
   flex:1 grower that stands empty on a solo pop-out).  This dresses it as period
   STATUS-BAR TEXT and gives it the LOWEST layout priority.

   LOWEST PRIORITY, MEASURED: flex:1 1 0 means the caption takes only the
   leftover space and contributes NOTHING to the tasks zone's natural width, so
   the band-level three-zone solve (TASK-1545[Q]) does not move and the controls
   zone still never shrinks.  On overflow the basis-0 caption collapses to its
   ellipsis FIRST, before the interactive chips give up a pixel -- passive text
   yields to the chips, never overlaps them.  min-width:0 + nowrap + ellipsis is
   the single-line truncation the taskbar idiom wants.

   ITS OWN GREY STATUS PLATE, NOT A BUTTON.  The .gtray plate is grey only in the
   grey theme (near-black in the other three), so #000 ink read straight on it
   would vanish in dark/neon/dxhr.  The caption therefore carries the same grey
   #c0c0c0 backing the buddies chip and the relocated controls wear -- so its
   #000 Tahoma is readable in ALL FOUR themes (about 11.5:1 on #c0c0c0) and the
   whole bar reads as one 1996 taskbar.  It is a SUNKEN status field (an inset
   bevel), not a raised beveled button, which is what keeps it "status text" and
   not a chip.  `background` shorthand, not `background-color`, and the only ink
   is #000 -- both accounted for in the surface suite's colour allowlist.

   SCOPED TO .gbare .gtray, so a FRAMED page (no tray) matches none of this and
   the caption stays a stage row styled by the cartridge's own .meta-<slug>-help
   rule, byte-for-byte.  WHY HERE AND NOT met-windows.js: that file injects its
   stylesheet only when it has a chip to show, and a solo pop-out has none -- this
   sheet is always loaded on v4-game.html (the TASK-1546[Q] trap).

   NOTE THAT THE TRAY IS OFF BY DEFAULT SINCE TASK-1634[Q], so the tagline is
   only on screen once a visitor switches the taskbar on.  It still relocates
   on every bare pass, because the band it lands in is built on every bare
   pass; it is simply not painted until the band is. */
.v4-game-page.gbare .gtray [data-met-cap] {
  flex: 1 1 0;
  min-width: 0;
  align-self: center;
  overflow: hidden;
  white-space: nowrap;
  text-overflow: ellipsis;
  margin: 0;
  padding: 3px 8px;
  background: #c0c0c0;
  color: #000;
  font: 12px Tahoma, Verdana, sans-serif;
  letter-spacing: normal;
  text-align: left;
  text-transform: none;
  box-shadow: inset 1px 1px 0 rgba(0, 0, 0, 0.4),
              inset -1px -1px 0 rgba(255, 255, 255, 0.55);
}

/* THE TRAY USED TO DRESS A RELOCATED CONTROL BANK AS ITS OWN GREY CHIP
   (TASK-1546[Q]).  No control docks in the band any more (TASK-1634[Q] moved
   the cartridge bank and the host's two controls into the game menu), so
   those rules are gone rather than left matching nothing (recover from
   24d6c217b; living_docs/CODE-DELETIONS.md has the row); the panel rules
   further down are what a relocated control wears now. */

/* ...AND THE INK IT WEARS IS THE ROW'S, WHICH TOOK A SPECIFICITY FIGHT TO SAY
   (TASK-1488[Q]).  The exit copies the class off the button beside it, and
   that was enough for everything EXCEPT the colour: the exit is an <a> and its
   neighbours are <button>s, so it alone also matches the theme's own link
   rule.  `body.grey a` is (0,1,2) and `.meta-xyz-btn` is (0,1,0), so the theme
   wins and the link takes the default anchor blue -- MEASURED in a browser at
   #0000ee on the row's #10182c, which is 1.88:1, beside neighbours at 12.04:1.
   Every theme did it, not just this one: dark handed it #00ffff, neon #f2c200,
   dxhr #f0b13e.  Those three happen to clear the floor and are still wrong,
   because a control that is supposed to match its row was being painted by
   whichever skin was on.  And `body.grey a:visited` is #551a8b at about
   1.57:1, which is what a returning visitor actually saw -- everybody arrives
   here FROM `/`, so the visited branch is the common case, not the edge.

   THE SPECIFICITY IS THE FIX, and it is the shape this stylesheet already
   uses: `.hd-you .modeswap` in metaeden-v4.css beat the same family of rules
   by adding a second class rather than by reaching for !important.  Here the
   attribute the host already marks the exit with does that job --
   `.v4-game-page a[data-met-exit]` is (0,2,1) and outranks any `body.<theme>
   a` at (0,1,2), in every skin, including one nobody has written yet.

   NO COLOUR IS NAMED HERE.  The value arrives in --met-exit-ink, which
   metaeden-v4-game.js reads off the sibling button's own computed style at the
   moment it appends the link -- so this file still invents no paint for a
   control that has to look at home in twenty different cabinets, which is the
   rule Y3 holds it to.  `currentColor` is the fallback rather than a hex: with
   no JS-measured value the link inherits the row's ink, which is wrong-ish but
   is never the browser blue this bug was made of.

   IT IS A FALLBACK, SINCE TASK-1634[Q].  In bare mode the exit stands inside
   the game menu panel, and the panel row rule further down restates its ink
   at a specificity that beats this one in every link state, so what a visitor
   sees there is the panel's ink and not the borrowed one.  This rule is kept
   for any exit that ever lands outside the panel (it costs nothing and it is
   the reason the link was never browser-blue again), which is why it still
   names no colour.

   :visited and :hover are listed because leaving either out hands that state
   straight back to the theme rule -- the base colour alone would fix the link
   and leave a visited one still at 1.57:1.  Hover therefore settles at the
   same ink instead of the sibling's brighter one; a computed style cannot be
   read for a state the element is not in.  The hover feedback is not lost, it
   is the border: `.meta-xyz-btn:hover` also lightens border-color, and nothing
   here contests that. */
.v4-game-page a[data-met-exit],
.v4-game-page a[data-met-exit]:link,
.v4-game-page a[data-met-exit]:visited,
.v4-game-page a[data-met-exit]:hover,
.v4-game-page a[data-met-exit]:focus,
.v4-game-page a[data-met-exit]:active {
  color: var(--met-exit-ink, currentColor);
}
.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 {
  /* The containing block for the title-screen layer below (TASK-1634[Q]):
     its chip, panel, mark and copyright line are positioned against this
     frame, so they ride with the stage and never with the window. */
  position: relative;
  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;
  /* THE CORNER INSETS, AS A CONVENTION (TASK-1635[Q], phase B.1).  Four
     names a cartridge in bare mode can pad its own corners with instead of
     guessing where the system layer stands: the chip's column top right
     (24 + 44), the mark's column bottom right (28 + 108, the mark's right
     offset plus its width), and the copyright line's plate bottom left,
     its height and its width.  EACH IS A FOOTPRINT WITH NO AIR: a corner
     element's offset plus its size, so a box padded by exactly the inset
     TOUCHES that element (at 1280 a legend padded by 136 ends at 1144,
     where the mark begins).  A cartridge adds its own air on top.
     The last two are FALLBACKS: metaeden-v4-game.js measures the line's
     rendered box on the first title pass it can and writes the real
     numbers over these on this same element.  Declared here, in the
     render-blocking sheet, so a cartridge reading var(--met-inset-tr) on
     its first frame gets a number rather than nothing; declared ONLY on
     the bare frame, so a framed page has no inset and a rule written as
     var(--met-inset-bl-h, 0px) collapses to nothing there.  ADD an inset
     to a side's own padding, calc(2px + var(--met-inset-br, 0px)), rather
     than putting it in that padding's place: a cartridge that marks its
     title on a framed page too would otherwise zero its own padding there.
     The phone
     sheet (metaeden-v4-game-fit.css) re-declares --met-inset-br where it
     shrinks the mark. */
  --met-inset-tr: 68px;
  --met-inset-br: 136px;
  --met-inset-bl-h: 40px;
  --met-inset-bl-w: 280px;
}
.v4-game-page.gbare [data-metaeden-wud],
.v4-game-page.gbare [data-metaeden-wud] #wudRoot { height: 100%; }

/* ==========================================================================
   THE TITLE-SCREEN SYSTEM LAYER (TASK-1634[Q], phase A).

   The user, over the bare-link mock-ups: the popped-out page should look like
   a standalone game's title screen.  So in bare mode the tray is off by
   default (above), and one uniform layer stands over every game instead: a
   game-menu chip at the top right, a studio mark at the bottom right, and the
   copyright line at the bottom left.  It is IDENTICAL on every cabinet, which
   is the whole point -- a cartridge added next year gets it without an edit,
   because metaeden-v4-game.js builds it from the same bare pass that reserves
   the tray.  Per-cabinet title lockups and attract modes are phase B and are
   not drawn here.

   THE MARK AND THE LINE ARE PAINTED ONLY ON A TITLE SCREEN (phase A.1).  A
   live audit of every cabinet found the two bottom corners standing on
   gameplay controls in 11 of 24: golf's hole buttons under the line and its
   Prev/Reset/Next under the mark, pool's Rack and Place Cue, artillery's
   Fire, fishing's CAST, the rink's NEW MATCH, tic-tac-toe's cells.  So
   .gsys-mark and .gsys-copy are display:none by default and display:block
   only while #gframe carries .gtitle -- the host's one title state, which
   metaeden-v4-game.js derives on every bare pass from a cartridge mount
   carrying data-met-title="1" (its own title or attract screen is showing)
   and clears when the marker goes.  The default is written in THIS sheet,
   which is render-blocking, so the first frame paints them hidden and no
   cabinet ever sees a flash of a mark it never asked for.  The chip and the
   panel are never gated: the way out is always on screen.  A cabinet that
   never sets the marker simply never shows the mark.

   THE DEMO TAG IS A SECOND MARKER ON THE SAME PLATE (TASK-1635[Q], phase
   B.1).  Behind a title screen the game engine plays an attract round with
   nobody at the controls, and the design says so beside the copyright
   line: a small bordered pill reading "Demo play", in the line's own ink,
   standing on the line's own plate.  A cartridge announces the attract
   round with data-met-demo="1" on its mount, beside data-met-title="1",
   and metaeden-v4-game.js derives a SECOND class on #gframe from it
   (.gdemo) on the same pass that derives .gtitle.  .gsys-demo is
   display:none by default and painted ONLY under #gframe.gtitle.gdemo --
   both classes, so the tag can never show without the title state it
   belongs to, and a demo marker left on a mount after play starts paints
   nothing.  It is the first child of the copyright <p>, ahead of the
   line's own text node, and floated left, so it draws FIRST on the line
   the way the design draws it at every width (built after the text, the
   float could not rise above the line box the text had already filled,
   and at a phone's width it dropped to the row under the wrapped line).
   The line's own text node is still exactly the studio line.  It names no colour: currentColor for its border and the
   line's ink inherited, so the contrast rig's measurement of the line IS
   the measurement of the tag.  12px, not the design's 10, for the reason
   the group headings are 12: this surface holds every size to the
   project's floor (F6).

   THE FOUR INSET NAMES are declared on .v4-game-page.gbare #gframe above
   (search for --met-inset-tr): --met-inset-tr, --met-inset-br,
   --met-inset-bl-h and --met-inset-bl-w, the chip's, the mark's and the
   copyright line's reservations, for a cartridge to pad its own corners
   with in bare mode.  Maglev Mesa's control legend is the first to use
   them (metaeden-maglev-mesa.js).

   INSIDE THE FRAME, OVER THE STAGE, AND NOT INSIDE THE STAGE.  #gstage is a
   scroll container whose direct children are cartridges: every `#gstage > *`
   rule above sizes a cabinet, and a chip standing among them would be given a
   cabinet's width.  The layer is a child of #gframe instead, absolutely
   positioned over the stage's box.  When the tray is switched on the stage
   shrinks by the band's height and the layer's bottom edge follows it (the
   .gtrayon rule), so the mark and the copyright line ride up with the stage
   exactly as the design's flex column has them do; 44px there is the band's
   own declared height, and the guard suite reads both and requires them
   equal.

   pointer-events IS THE SEAM.  The layer itself passes every press through to
   the game; only the chip and the panel take them back.  The mark and the
   copyright line never do, so a player's click lands on the game under them.

   ITS OWN PLATE, BECAUSE IT SITS OVER ANY GAME'S ART.  The chip's three bars
   are white on a near-black translucent plate, so it reads over a white
   lockup, a neon grid and a night park alike -- the plate is what is measured
   against, not whatever cabinet is underneath.  The panel is the same idea
   with a solid plate.  Every colour here is the design's own, named once,
   and the surface suite's colour scan allows exactly this set inside .gsys
   and nothing else; the contrast rig measures the inks against the plates.

   THE PANEL IS THE ONE HOME FOR THE CONTROLS in bare mode.  GAME holds the
   cartridge's own bank (what relocateBank moves; SOUND keeps its aria-pressed
   and reads as ON in the accent); SYSTEM holds the Show taskbar switch, Share
   Game Link w/Friend and Return To MetaEden.Net.  The tray, when shown,
   carries only its two met-windows.js ends.  Never draw a control in two
   places.

   THE LAYER IS NOT A STACKING CONTEXT; ITS CHILDREN ARE (TASK-1685[Q]).  It
   used to carry z-index 120 itself, which put EVERYTHING in it -- the studio
   mark and the copyright line as well as the controls -- over the Friends
   and Chat window (86).  The user saw the RE:REZZED mark painted across the
   chat's messages and its Send button.  So the layer stays z-index:auto and
   each child takes its own rung: the chip and the panel at 120 (in the open
   page band, above met-windows.js's chips at 90 and the friends window at
   86, so an open menu is never under a docked chip), and the mark and the
   line at 60 -- under EVERY floating window (70..99 in browser-frame.css's z
   scale: the MET List's World window at 70, the chat windows at 71..75, the
   friends window at 86, the chips at 90), so a title decoration never paints
   over a window or a control somebody is reaching for.  The framed layer
   below takes the same 60 for the same reason.  The z-bands suite reads them.

   The system typeface is IBM Plex Mono, loaded by v4-game.html from Google
   Fonts the way v4-metaeden.html and metlfg.html already load theirs, with a
   monospace fallback stack for a visitor whose font request never lands. */
.v4-game-page.gbare .gsys {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: auto;
  margin: 0;
  padding: 0;
  pointer-events: none;
  font-family: 'IBM Plex Mono', 'Courier New', Courier, monospace;
}
.v4-game-page.gbare.gtrayon .gsys { bottom: 44px; }

/* The chip: 44x44, three 18x2 bars; open, a times glyph on a solid plate with
   a brighter hairline.  Both states are the same box, so nothing shifts. */
.v4-game-page.gbare .gsys-chip {
  position: absolute;
  z-index: 120;
  top: 24px;
  right: 24px;
  width: 44px;
  height: 44px;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  gap: 4px;
  margin: 0;
  padding: 0;
  border: 1px solid rgba(255, 255, 255, 0.24);
  border-radius: 10px;
  background: rgba(8, 8, 10, 0.84);
  color: #ffffff;
  font: 22px/1 'IBM Plex Mono', 'Courier New', Courier, monospace;
  cursor: pointer;
  pointer-events: auto;
}
.v4-game-page.gbare .gsys-chip .gsys-bar {
  display: block;
  width: 18px;
  height: 2px;
  border-radius: 1px;
  background: #ffffff;
}
.v4-game-page.gbare .gsys-chip .gsys-x { display: none; }
.v4-game-page.gbare .gsys-chip[aria-expanded="true"] {
  border-color: rgba(255, 255, 255, 0.55);
  background: #08080a;
}
.v4-game-page.gbare .gsys-chip[aria-expanded="true"] .gsys-bar { display: none; }
.v4-game-page.gbare .gsys-chip[aria-expanded="true"] .gsys-x { display: block; }
.v4-game-page.gbare .gsys-chip:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 2px;
}

/* The panel hangs from the chip.  max-width keeps it on a phone's glass
   (300 + 24 fits 375; the cap is for anything narrower), which is what keeps
   the fit sheet's no-horizontal-scroll promise true without a media query.

   max-height KEEPS EVERY ROW REACHABLE ON A PHONE HELD SIDEWAYS.  The body is
   overflow:hidden in bare mode, so a panel taller than the glass would simply
   lose its last rows; at 667x375 the panel's top is 80 and a cabinet with a
   bank (SOUND, WIPE SAVE, then the three SYSTEM rows) runs past the bottom.
   100% here is the layer's height, which is the frame's less the tray when
   the tray is on, so the cap follows the tray the way the mark does; the
   100px is the 80px hang plus a 20px foot, so the panel's bottom edge is
   always 20px inside the layer's and the rows past it scroll.

   THE PANEL OVER THE MARK.  The panel, the mark and the copyright line are
   all absolutely positioned children of the layer, and the mark comes later
   in the DOM, so at a short viewport the phoenix once drew ACROSS the panel's
   last rows.  The panel's 120 against the mark's 60 keeps it on top; see the
   layer's note for why each child carries its own rung (TASK-1685[Q]). */
.v4-game-page.gbare .gsys-panel {
  position: absolute;
  z-index: 120;
  top: 80px;
  right: 24px;
  width: 300px;
  max-width: calc(100vw - 48px);
  max-height: calc(100% - 100px);
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  gap: 2px;
  margin: 0;
  padding: 8px;
  overflow-y: auto;
  border: 1px solid rgba(255, 255, 255, 0.16);
  border-radius: 12px;
  background: rgba(13, 13, 16, 0.96);
  box-shadow: 0 18px 40px rgba(0, 0, 0, 0.55);
  color: #ececee;
  pointer-events: auto;
}
.v4-game-page.gbare .gsys-panel[hidden] { display: none; }
.v4-game-page.gbare .gsys-group {
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.v4-game-page.gbare .gsys-group[hidden] { display: none; }
/* Group headings.  The design draws these at 10px; this surface holds every
   size it declares to the project's 12px floor (F6 in the guard suite), so
   they are 12px with the design's tracking. */
.v4-game-page.gbare .gsys-head {
  margin: 0;
  padding: 8px 12px 4px;
  font-size: 12px;
  letter-spacing: 0.24em;
  text-transform: uppercase;
  color: #8f949c;
}
.v4-game-page.gbare .gsys-group-sys .gsys-head {
  margin: 6px 4px 0;
  padding: 10px 8px 4px;
  border-top: 1px solid rgba(255, 255, 255, 0.10);
}
/* ...and no rule above SYSTEM when GAME is empty (a cabinet with no bank). */
.v4-game-page.gbare .gsys-group[hidden] + .gsys-group-sys .gsys-head {
  margin-top: 0;
  border-top: 0;
}

/* THE CONTAINER A RELOCATED BANK ARRIVES IN, restyled before its buttons are.
   Every cartridge that declares data-met-controls builds that bank as a
   HORIZONTAL flex row (`.meta-xyz-btns{display:flex;gap:8px}`,
   `.meta-vanish-boom-3d-btns{display:flex;gap:6px}`, spamcleaner's
   `.scx-bot{display:flex;flex-wrap:wrap}`), and the whole marked element is
   what relocateBank parks in the GAME group -- so without this rule SOUND and
   WIPE SAVE stood side by side in the 284px column, each `width:100%` row
   shrunk to share one line, their labels wrapping.  The design's GAME group
   is one control per row, which is what the row rule below draws once the
   bank is a column.  Written at two classes plus the attribute so it beats
   the cartridge's own single-class rule; align-self:stretch because a
   cartridge's `align-items` on its parent no longer applies here and the
   group is a column. */
.v4-game-page.gbare .gsys [data-met-controls] {
  display: flex;
  flex-direction: column;
  flex-wrap: nowrap;
  align-self: stretch;
  gap: 2px;
  margin: 0;
  padding: 0;
}

/* A ROW, whichever element it is: the switch, a relocated cartridge button,
   the share button, the exit link.  The relocated bank arrives wearing its
   own cartridge class and the two host controls arrive wearing .bbtn (the
   page's own control class, copied the way buildExit always has), so this
   rule is written at a specificity that beats every one of those and the
   theme's own link rule, in every link state. */
.v4-game-page.gbare .gsys-row,
.v4-game-page.gbare .gsys [data-met-controls] button,
.v4-game-page.gbare .gsys [data-met-controls] a,
.v4-game-page.gbare .gsys [data-met-controls] a[data-met-exit],
.v4-game-page.gbare .gsys [data-met-controls] a[data-met-exit]:link,
.v4-game-page.gbare .gsys [data-met-controls] a[data-met-exit]:visited,
.v4-game-page.gbare .gsys [data-met-controls] a[data-met-exit]:focus,
.v4-game-page.gbare .gsys [data-met-controls] a[data-met-exit]:active {
  display: flex;
  align-items: center;
  gap: 12px;
  box-sizing: border-box;
  width: 100%;
  min-width: 0;
  min-height: 44px;
  margin: 0;
  padding: 8px 12px;
  border: 0;
  border-radius: 8px;
  background: transparent;
  color: #ececee;
  font: 13px/1.3 'IBM Plex Mono', 'Courier New', Courier, monospace;
  letter-spacing: normal;
  text-transform: none;
  text-align: left;
  text-decoration: none;
  white-space: normal;
  box-shadow: none;
  cursor: pointer;
}
.v4-game-page.gbare .gsys-row:hover,
.v4-game-page.gbare .gsys [data-met-controls] button:hover,
.v4-game-page.gbare .gsys [data-met-controls] a:hover,
.v4-game-page.gbare .gsys [data-met-controls] a[data-met-exit]:hover {
  background: rgba(255, 255, 255, 0.06);
  color: #ececee;
}
.v4-game-page.gbare .gsys-row:focus-visible,
.v4-game-page.gbare .gsys [data-met-controls] button:focus-visible,
.v4-game-page.gbare .gsys [data-met-controls] a:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: -2px;
}
/* A cartridge toggle that is ON reads in the accent, keyed on the aria-pressed
   the SOUND buttons publish; a destructive control (data-met-danger, WIPE
   SAVE) reads in a warning red.  Both measured against the panel plate. */
.v4-game-page.gbare .gsys [data-met-controls] button[aria-pressed="true"] {
  color: #f08a24;
}
.v4-game-page.gbare .gsys [data-met-controls] [data-met-danger] {
  color: #ff8a8a;
}
.v4-game-page.gbare .gsys [data-met-controls] [data-met-danger]:hover {
  color: #ff8a8a;
}

/* The switch: label and sub-label on the left, a 40x22 pill on the right. */
.v4-game-page.gbare .gsys-lbl {
  flex: 1 1 auto;
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 2px;
}
.v4-game-page.gbare .gsys-sub {
  font-size: 12px;
  color: #9a9ea6;
}
.v4-game-page.gbare .gsys-tog {
  position: relative;
  flex: 0 0 auto;
  display: inline-block;
  width: 40px;
  height: 22px;
  border-radius: 11px;
  background: #3a3d45;
}
.v4-game-page.gbare .gsys-knob {
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: 8px;
  background: #ffffff;
}
.v4-game-page.gbare .gsys-switch[aria-checked="true"] .gsys-tog { background: #f08a24; }
.v4-game-page.gbare .gsys-switch[aria-checked="true"] .gsys-knob { left: 21px; }

/* The studio mark and the copyright line.  BOTH ARE HIDDEN BY DEFAULT and
   painted only under #gframe.gtitle (the rule after them; see the block
   comment above for why).  The mark is a picture with its own alpha and
   needs no ink.  The line WEARS THE CHIP'S PLATE, for the chip's reason: the
   bare page's own ground is black, but the ground is only what shows where a
   cabinet is letterboxed.  Anywhere a cabinet's stage reaches the
   bottom-left corner the line stands on the cabinet's art, and on Neon Pool
   that art is the cabinet's own #c0c0c0 Rack and Place Cue buttons --
   measured live at 1.29:1 for this grey on that plate.  On the near-black
   plate the line reads over any art (the contrast rig composites it over
   black AND over white and measures both), and the plate takes no presses,
   so a control under it is still the control a click lands on. */
.v4-game-page.gbare .gsys-mark {
  position: absolute;
  z-index: 60;
  right: 28px;
  bottom: 20px;
  /* The asset is square (216x216, drawn at 2x), so the box IS the picture's
     shape and no fit rule is needed -- and this sheet reaches for none
     anyway (H40 in the guard suite). */
  width: 108px;
  height: 108px;
  display: none;
  pointer-events: none;
}
.v4-game-page.gbare .gsys-copy {
  position: absolute;
  z-index: 60;
  left: 28px;
  bottom: 26px;
  /* THE PLATE NEVER REACHES THE MARK'S COLUMN (TASK-1635[Q]): its right
     edge is capped at the mark's own inset, so at a phone's width the
     "Demo play" tag and the line wrap onto two rows of one plate rather
     than running under the phoenix.  Read off the same name a cartridge
     reads, so the two cannot disagree about where the mark stands.
     THE CAP BOUNDS THE BORDER BOX, and that is the sizing rule below, not
     a default: the first cut left this content-box, so the cap held the
     text at 247px at 375 wide and the 16px of padding ran past it, the
     plate's right edge at 291 over a mark whose left edge was 275.  The
     rows wrapped and the plate still ran under the phoenix (N10d in the
     contrast rig computes the plate's edge from the sizing, and goes red
     on that shape). */
  box-sizing: border-box;
  max-width: calc(100% - 28px - var(--met-inset-br, 136px));
  margin: 0;
  padding: 4px 8px;
  border-radius: 6px;
  background: rgba(8, 8, 10, 0.84);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #d9d9d9;
  display: none;
  pointer-events: none;
}
/* ...and painted while a title screen is on show.  ONE rule for both, on the
   frame's own class, so there is one place the state is read.  Nothing else
   in the layer is keyed on it: the chip, the panel and the .gtrayon lift
   above are the same in both states. */
.v4-game-page.gbare #gframe.gtitle .gsys-mark,
.v4-game-page.gbare #gframe.gtitle .gsys-copy { display: block; }

/* The "Demo play" tag (phase B.1; see the block comment above).  Hidden by
   default; a float so it stands FIRST on the line although the line's text
   comes first in the element; the line's own ink for its border and its
   text, so no colour is named here and the rig's one measurement covers
   both.  The 14px is the design's gap between the tag and the line. */
.v4-game-page.gbare .gsys-demo {
  display: none;
  float: left;
  box-sizing: border-box;
  margin: 0 14px 0 0;
  padding: 3px 8px;
  border: 1px solid currentColor;
  border-radius: 3px;
  font-size: 12px;
  letter-spacing: 0.2em;
  line-height: 1;
}
/* ...and painted only under BOTH classes: a title screen, with an attract
   round running behind it. */
.v4-game-page.gbare #gframe.gtitle.gdemo .gsys-demo { display: inline-block; }

/* THE BARE PHONE GATE'S TITLE CARD (TASK-1646[Q]).  A noMobile cabinet opened
   by a bare link on a phone mounts nothing, and bare mode hides the heading
   and the note the gate paints on a framed page, so the window was black.
   metaeden-v4-game.js builds #gphone only in that one case (paintBarePhoneGate)
   and this is its whole dress: the studio mark, the cabinet's name, its
   blurb, the line saying where it plays and two ways on.  It is a member of
   the title-screen system layer's family and wears that layer's palette on
   the bare page's own black (#000, set by the .gbare rule above): #ececee
   and #d9d9d9 for text, #ffffff for the two links and their borders.  Every
   pair is over 14:1, and metaeden_net_bare_phone_gate_live measures them in
   a real browser.  No z-index: in this state nothing else on the page paints. */
.v4-game-page.gbare .gsys-gate {
  position: fixed;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 10px;
  padding: 24px 20px;
  overflow-y: auto;
  text-align: center;
  color: #d9d9d9;
  font: 14px/1.45 'IBM Plex Mono', 'Courier New', Courier, monospace;
}
.v4-game-page.gbare .gsys-gate-mark { width: 72px; height: 72px; flex: none; }
.v4-game-page.gbare .gsys-gate .gsys-gate-title {
  margin: 4px 0 0;
  max-width: 100%;
  font: 700 28px/1.15 'IBM Plex Mono', 'Courier New', Courier, monospace;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  text-decoration: none;
  overflow-wrap: anywhere;
  color: #ececee;
}
.v4-game-page.gbare .gsys-gate p { margin: 0; max-width: 34em; }
.v4-game-page.gbare .gsys-gate .gsys-gate-line {
  margin-top: 8px;
  font-size: 16px;
  color: #ececee;
}
.v4-game-page.gbare .gsys-gate .gsys-gate-ways {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  gap: 10px;
  margin-top: 10px;
}
.v4-game-page.gbare .gsys-gate a.gsys-gate-link,
.v4-game-page.gbare .gsys-gate a.gsys-gate-link:link,
.v4-game-page.gbare .gsys-gate a.gsys-gate-link:visited,
.v4-game-page.gbare .gsys-gate a.gsys-gate-link:hover,
.v4-game-page.gbare .gsys-gate a.gsys-gate-link:active {
  display: inline-flex;
  align-items: center;
  box-sizing: border-box;
  min-height: 44px;
  padding: 8px 16px;
  border: 1px solid #ffffff;
  border-radius: 8px;
  background: transparent;
  color: #ffffff;
  font: 14px/1.3 'IBM Plex Mono', 'Courier New', Courier, monospace;
  text-decoration: none;
}
.v4-game-page.gbare .gsys-gate a.gsys-gate-link:focus-visible {
  outline: 2px solid #ffffff;
  outline-offset: 2px;
}

/* THE FOCUS THE HOST HANDED A TITLE DRAWS NO RING (TASK-1637[Q]).  On a bare
   page the host focuses a title's own mount when nothing else has focus, so
   Enter reaches a game whose keys are gated on focus (handTitleFocus in
   metaeden-v4-game.js).  The mount wears data-met-quiet-focus for as long as
   THAT focus lasts and loses it on the first blur, so this rule never hides a
   ring somebody reached by Tab or by a click. */
.v4-game-page.gbare #gstage [data-met-quiet-focus]:focus,
.v4-game-page.gbare #gstage [data-met-quiet-focus]:focus-visible {
  outline: none;
  box-shadow: none;
}

/* ==========================================================================
   THE FRAMED TITLE (TASK-1638[Q]).  The user, over the framed page still
   showing a cabinet's old pre-boot card: "please have all the new titles
   also show inside these old views as well. all of them."  So a framed page
   (no .gbare: the ordinary view and its .gfs big view) paints the same title
   state inside its stage: the studio mark bottom right, the copyright line
   bottom left, and the Demo play tag on the line's plate.  metaeden-v4-game.js
   derives .gtitle and .gdemo on #gframe from the same two markers there
   (syncFramedTitle) and builds a layer of the mark and the line only: THE
   GAME MENU CHIP STAYS BARE-ONLY, because the framed page has its own links
   above the stage, so nothing here names .gsys-chip or .gsys-panel.

   SIZED FOR A STAGE THAT IS SMALLER THAN A WINDOW, AND FOR THE ONE THAT IS
   NOT.  Out of the big view the stage is a box inside the page (the 820px
   column's 16:9, 461px tall on a desk); in .gfs it is the window's 16:9.  So
   the mark and the corner gap are ONE arithmetic over the frame's own short
   side rather than two sets of numbers: --gsys-mark is a tenth of it, held
   between 64px and the bare page's 108px, and --gsys-gap is 2.6% of it,
   held between 14px and the bare page's 28px.  The short side is --gfs-h
   (the height every cartridge already sizes against, redeclared by each
   mode) or the viewport's 16:9 height, whichever is less, so a phone held
   upright in the big view gets the 64px floor and the corners never meet.
   A desk's ordinary view lands on the floor too (64 and 14); a 1920 by 1080
   big view lands on the bare numbers exactly.  The line stays at the 12px
   floor in both, on the chip's own plate, in the ink the contrast rig
   already measures on the bare page: the same two colours, no new pair.

   THE CONTAINING BLOCK.  The layer is positioned against #gframe as the bare
   one is.  The .gfs frame is fixed and already positioned; the ordinary
   framed frame was not, so it is made relative here, and only there, so the
   .gfs rule's position:fixed is never contested.  Only the two BOTTOM
   corners are painted, and the stage is the frame's last child with no
   margin of its own, so those corners are the stage's corners whatever
   stands above it.

   THE INSETS are declared on the framed frame too, at the framed layer's
   footprint: --met-inset-tr is 0 (no chip to clear), --met-inset-br is the
   gap plus the mark (the same "offset plus size, no air" arithmetic as the
   bare 136), and the bottom-left pair are the same fallbacks the bare frame
   declares, which metaeden-v4-game.js overwrites with the line's measured
   box on the first title pass exactly as it does there.

   z-index 60 is in the open page band, and deliberately UNDER every floating
   window (70..99): met-windows.js's docked chips (90), the friends window
   (86), the chat windows (71..75) and the MET List's World window (70).  On a
   framed page those float over the whole viewport, and a studio mark must
   never paint over a window or a control somebody is reaching for.  It was
   80 until TASK-1685[Q], which put it over the World window and the chat
   windows.  The layer takes no presses at all. */
.v4-game-page:not(.gbare):not(.gfs) #gframe { position: relative; }
.v4-game-page:not(.gbare) #gframe {
  --gsys-short: min(var(--gfs-h), calc(100vw * 9 / 16));
  --gsys-mark: clamp(64px, calc(var(--gsys-short) * 0.1), 108px);
  --gsys-gap: clamp(14px, calc(var(--gsys-short) * 0.026), 28px);
  --met-inset-tr: 0px;
  --met-inset-br: calc(var(--gsys-gap) + var(--gsys-mark));
  --met-inset-bl-h: 40px;
  --met-inset-bl-w: 280px;
}
.v4-game-page:not(.gbare) .gsys {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  z-index: 60;
  margin: 0;
  padding: 0;
  pointer-events: none;
  font-family: 'IBM Plex Mono', 'Courier New', Courier, monospace;
}
.v4-game-page:not(.gbare) .gsys-mark {
  position: absolute;
  right: var(--gsys-gap, 14px);
  bottom: calc(var(--gsys-gap, 14px) * 0.72);
  width: var(--gsys-mark, 64px);
  height: var(--gsys-mark, 64px);
  display: none;
  pointer-events: none;
}
.v4-game-page:not(.gbare) .gsys-copy {
  position: absolute;
  left: var(--gsys-gap, 14px);
  bottom: var(--gsys-gap, 14px);
  box-sizing: border-box;
  max-width: calc(100% - var(--gsys-gap, 14px) - var(--met-inset-br, 78px));
  margin: 0;
  padding: 4px 8px;
  border-radius: 6px;
  background: rgba(8, 8, 10, 0.84);
  font-size: 12px;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  color: #d9d9d9;
  display: none;
  pointer-events: none;
}
.v4-game-page:not(.gbare) #gframe.gtitle .gsys-mark,
.v4-game-page:not(.gbare) #gframe.gtitle .gsys-copy { display: block; }
.v4-game-page:not(.gbare) .gsys-demo {
  display: none;
  float: left;
  box-sizing: border-box;
  margin: 0 14px 0 0;
  padding: 3px 8px;
  border: 1px solid currentColor;
  border-radius: 3px;
  font-size: 12px;
  letter-spacing: 0.2em;
  line-height: 1;
}
.v4-game-page:not(.gbare) #gframe.gtitle.gdemo .gsys-demo { display: inline-block; }
/* The focus the host handed a framed title draws no ring either, for the
   reason the bare rule above gives. */
.v4-game-page:not(.gbare) #gstage [data-met-quiet-focus]:focus,
.v4-game-page:not(.gbare) #gstage [data-met-quiet-focus]:focus-visible {
  outline: none;
  box-shadow: none;
}

.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 bar had four of these in bare mode too (TASK-1487[Q]) and they went with
   the bar (TASK-1488[Q]).  The four values then stood beside the .gexitrow
   block, on the host's own exit row, until TASK-1634[Q] moved that row into
   the game menu panel and the four lines went with it (recover from
   24d6c217b): the panel plate rgba(13, 13, 16, 0.96) is what the row's ink is
   measured against now, in every theme.  The plates still borrowed from these
   twins in bare mode are the tray's own, four lines under .gtray. */

/* The cartridge gets the whole frame.  overscroll-behavior keeps a wheel at
   the end of a scroll from chaining to the page underneath.

   AND WHAT IT DOES NOT USE IS SPLIT EVENLY (TASK-1621[Q]).  Several cartridges
   cannot grow into the height they are given -- word fret lays its fret out at
   405px whatever the window is, and half a dozen more are the same -- and the
   stage used to pool every one of those leftovers UNDER the game: 632px of
   black beneath word fret at 1920x1080, 506 under lex trace, 399 under
   minesweeper.  A column that centres turns each of those into a letterbox,
   which is what the user asked for and is the same answer the pillarbox on a
   canvas cabinet already gives.

   `safe` is the whole reason this is safe: centred flex content that OVERFLOWS
   its container is unreachable at the start edge, and the tracker is taller
   than this stage on purpose (1893px measured).  `safe center` aligns an
   overflowing item to the start instead, so it still scrolls to all of itself.
   A browser that does not know the keyword throws the whole declaration out and
   lands back on flex-start, which is exactly the behaviour that shipped before
   this ticket -- so the degradation is the status quo rather than a broken
   page.

   `flex: 1` STAYS THE FIRST DECLARATION IN THIS BLOCK ON PURPOSE.
   server/met_popout_exit_contrast_rig.test.js's G10 mutation anchors on the
   two selectors plus that one line, to prove the bottom row can take its
   height from the stage, and it THROWS rather than quietly passing when its
   target moves. The three lines above it were written before it on the first
   cut of this ticket and cost that suite a run. */
.v4-game-page.gfs #gstage,
.v4-game-page.gbare #gstage {
  flex: 1;
  display: flex;
  flex-direction: column;
  justify-content: safe center;
  min-width: 0;
  min-height: 0;
  margin: 0;
  padding: 0;
  border: 0;
  overflow: auto;
  overscroll-behavior: contain;
}

/* THE POPPED-OUT STAGE IS 16:9 AT EVERY WINDOW SIZE (TASK-1652[Q]).  The
   user: "games should stay inside a 1080p ratio ... Sure users can resize
   after".  Every pop-out door now opens a 1920x1000 window (MetPopLink.
   popFeatures), whose content box is never 16:9, and a visitor can drag it to
   any shape after.  So the stage is the largest 16:9 box that fits the room,
   centred, and the rest is the body's own black as letterbox or pillarbox --
   the big view's min() arithmetic (.gfs #gframe), measured against the room
   instead of the viewport.

   THE ROOM IS THE WINDOW LESS THE TRAY, when the tray is on: 44px is the
   band's own declared height (.gbare .gtray), and the auto margins split the
   leftover above and below the stage so the tray stays at the foot.

   THE FRAME STAYS THE WINDOW, and that is why this is on the stage.  The
   title layer (.gsys: the menu chip, its panel, the mark and the copyright
   line) is positioned against #gframe, so it keeps the window's corners
   whatever shape the picture is; moving the lock up to the frame would have
   carried the menu into the middle of a wide window.

   --gfs-h FOLLOWS THE STAGE, because every fit rule below reads it as "the
   height of the box the cartridge is in".  It was the viewport's height when
   the stage was the viewport; now it is the stage's.

   Declared AFTER the shared .gfs/.gbare block above rather than inside it, so
   that block's first line stays the anchor the contrast rig's G10 mutation
   looks for. */
.v4-game-page.gbare #gstage {
  --gfs-room-h: 100dvh;
  flex: 0 0 auto;
  align-self: center;
  box-sizing: border-box;
  width: min(100vw, calc(var(--gfs-room-h) * 16 / 9));
  height: min(var(--gfs-room-h), calc(100vw * 9 / 16));
  margin: auto 0;
  --gfs-h: min(var(--gfs-room-h), calc(100vw * 9 / 16));
}
.v4-game-page.gbare.gtrayon #gstage { --gfs-room-h: calc(100dvh - 44px); }

/* NO BACKPLATE IN THE POP-OUT (TASK-1652[Q], round 3).  The user, zoomed on
   the left edge of the rink popped out: "there's something weird going on
   like there's a frame behind the actual game ... i think we need to get rid
   of that backplate?"  What they saw, measured at 2000x1190: black letterbox,
   then the rink's CABINET -- .rink-game, a #0c0b0d plate the size of the
   whole stage with a 3px brass ridge and an 8px drop shadow -- then the ice
   section's own brass ridge (.rink-stage's border-right, standing in the
   band beside the arena once the title contain-fits the picture), and only
   then the game's own art.  A cabinet's frame belongs to a page the cabinet
   sits IN; in the pop-out the window is the cabinet, so the only things on
   screen are the black bars and the game.
   The same audit over all 25 cartridges found the other frames that stand
   between a bar and the art: golf's and pool's 1px neon border with its pink
   drop shadow (and their plates, which show beside a contain-fitted canvas),
   Vanish Boom VR's 1px panel border, the 1px screen border 13-Ships, ANTS
   and Maglev Mesa each draw round their playfield, and the 4px brass ridge
   round the rink's two stage-sized screens (build-a-skater and the
   calibrator), which lands exactly on the letterbox edge.  Every other
   cartridge's stage-sized fill is its own title art or screen and is left
   alone, which is also why the second group keeps its background: it is the
   screen, not a plate behind one.
   COLOURS ONLY, NEVER A WIDTH.  border-color goes transparent rather than the
   border going to 0, so every box keeps the size the fits above (and the
   rink's frameIceForTitle, which reads clientLeft/clientTop) were measured
   with; a transparent ridge paints nothing.  Scoped to .gbare: the framed
   page and its big view keep their cabinets exactly as they were.  This is the
   one block in this file allowed to decide how a cartridge looks, and all it
   decides is that its frame is not drawn (surface suite H43a/H43d). */
.v4-game-page.gbare #gstage .rink-game,
.v4-game-page.gbare #gstage .rink-stage,
.v4-game-page.gbare #gstage .meta-golf-game,
.v4-game-page.gbare #gstage .meta-pool-game,
.v4-game-page.gbare #gstage .meta-pool-stage,
.v4-game-page.gbare #gstage .meta-vboom-vr {
  background-color: transparent;
  border-color: transparent;
  box-shadow: none;
}
.v4-game-page.gbare #gstage .rink-creation,
.v4-game-page.gbare #gstage .rink-calibrator,
.v4-game-page.gbare #gstage .meta-bships-stage,
.v4-game-page.gbare #gstage .meta-ants-stage,
.v4-game-page.gbare #gstage .meta-mesa-stage {
  border-color: transparent;
  box-shadow: none;
}

/* 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.

   WHAT STILL PASSES THROUGH HERE AFTER TASK-1621[Q], WHICH IS MUCH LESS THAN
   IT WAS.  That ticket gave the five canvas cabinets this rule was actually
   sizing -- golf, pool, artillery, snakecycle, CAG -- a ROOM and a real contain
   fit (two blocks down), and a rule further down the cascade beats this one, so
   in widescreen and in a pop-out those five no longer read the fraction at all.
   The ordinary in-page column still does, and is deliberately untouched: out
   there the frame's height is a FLOOR rather than a ceiling, so a height budget
   is the right shape and the numbers above are the measured ones.
   In the MODE the fraction is now the fallback for a canvas that has no room --
   today that is Dot Matrix: XYZ's first-person view, which is absolutely
   positioned inside a stage of its own and draws its own letterbox, and any
   auxiliary canvas a future cabinet hangs in its chrome before the block at
   ".fsx-chip canvas" has been taught about it.  A fraction of the frame is a
   poor answer for a playfield and a safe one for anything else, which is the
   right way round for a fallback. */
.v4-game-page #gstage canvas {
  width: auto;
  height: calc(var(--gfs-h) * 0.64);
  max-width: 100%;
  max-height: none;
}

/* ==========================================================================
   THE CABINET IS THE CARTRIDGE'S, ALL OF IT (TASK-1621[Q]).

   The user, over a screenshot of Stack Tracer popped out on a wide window:
   "unused space, have the pop out games use as much space as available while
   keeping to the 1080p aspect ratio."  Measured on the rig at 1920x1080 before
   this rule: the stage was 1920x1036 and the cartridge inside it was
   1920x885.6, so a 150px black band sat under the game with the tray under
   that -- about a fifth of the window, which is what the screenshot circled.
   It was not one cartridge's bug.  Every mount whose height is its own content
   left a band of its own: word fret 631px, lex trace 505px, Vanish Boom VR
   440px, 3D noughts and crosses 411px, minesweeper 398px, solitaire 387px,
   pool 212px, golf 137px.  The ones that did NOT -- fishing, strip mine, spam
   clicker, the rink, bugs, the mesa, crystal keep -- are exactly the ones a
   ticket had already come along and written `height: 100%` for by name.

   SO IT IS WRITTEN ONCE, FOR EVERYTHING, INSTEAD OF SEVEN MORE TIMES BY NAME.
   A cartridge in the mode is handed the stage's height and decides what to do
   with it; nothing here decides anything about a game.  The seven named rules
   are left where they are -- each says `height: 100%` too, so they agree with
   this one to the pixel, and each carries the reasoning of the ticket that
   found it.

   box-sizing MEASURED, not assumed, which is the same trap the vanish-boom-3d
   block two screens up fell into: several of these mounts are content-box and
   carry padding, so `height: 100%` alone comes out taller than the stage and
   draws a scrollbar down a game that fitted before.

   IT IS A CEILING, NOT A FLOOR, and that is the half that took a second pass.
   `height: 100%` on every mount took the band out of the MEASUREMENT and left
   it on the SCREEN: a cartridge that cannot grow into the height simply drew
   its 405px of content at the top of a 1036px box, with the same 631px of
   black under it.  So a mount is now capped at the cabinet rather than filled
   by it, and the stage above centres what it holds -- which turns every one of
   those leftovers into an even letterbox.  A cartridge that CAN use the height
   asks for it: the seven named blocks further down already did, the five with a
   room ask in the room block below, and the tracker is the one mount taller
   than the cabinet (1905x1893 measured), where the cap does not clip it because
   the stage is `overflow: auto` and still scrolls to its full 1903px.

   WHAT IT DOES NOT DO is make a DOM cartridge fill the space it is given.
   Minesweeper and solitaire grow through the scale knobs above; the rest lay
   themselves out at their own size inside a taller box.  That is the
   cartridge's own layout and this file does not get to decide it.

   THE WIDTH IS HERE BECAUSE THE COLUMN ABOVE NEEDS IT, and it is the one line
   of this block that is not about height at all.  A flex item carrying
   `margin: 0 auto` -- which several cartridges' own stylesheets do, to centre
   themselves in an ordinary page -- has STRETCH turned off by those auto
   margins and shrinks to fit instead.  Measured the moment the stage became a
   column: crystal keep came out 0px wide and CAG 456px, because each shrinks
   around a child that is itself sized from its parent.  A definite width is
   what a block-level mount had all along; saying so keeps it. */
.v4-game-page.gfs #gstage > *,
.v4-game-page.gbare #gstage > * {
  box-sizing: border-box;
  width: 100%;
  max-height: 100%;
  min-height: 0;
}

/* THE ONE CARTRIDGE THAT IS A WHOLE PAGE IN A FRAME, and the one in the user's
   screenshot (TASK-1621[Q]).  Stack Tracer's adapter mounts /games/tower-stack/
   in a same-origin iframe and sizes the mount `min(1040px, 82vh)` -- 885.6px at
   1920x1080, in a 1036px stage, which is the 150.4px band the report circled.
   A page fills whatever viewport it is handed, so this one can use the whole
   cabinet, and its own stylesheet says so for widescreen already
   (`.gfs [data-metaeden-tower-stack]{height:100%}` in metaeden-tower-stack.js).
   Named rather than derived from `:has(> iframe)` for a reason the adapter's own
   header gives: the preboot screen occupies exactly the box the frame will, so
   deriving it from the frame's presence would resize the panel under the
   visitor at the moment they press OPEN. */
.v4-game-page.gfs #gstage > [data-metaeden-tower-stack],
.v4-game-page.gbare #gstage > [data-metaeden-tower-stack] { height: 100%; }

/* ==========================================================================
   THE ROOM, AND THE ONE FIT THAT FILLS IT (TASK-1621[Q]).

   THE FIT IS THE ONE RULE AND IT NAMES NO CARTRIDGE: the playfield fills the
   largest box of its OWN aspect that fits the room it is in, centred, with the
   leftover split evenly as letterbox or pillarbox.  Measured at 1920x1080,
   before and after, content box and true ratio:

     golf        1244.1x691.2 (1.800) -> 1490.4x828.0 (1.800)
     pool        1461.4x691.2 (2.114) -> 1885.1x891.6 (2.114)
     artillery   1346.9x691.2 (1.949) -> 1744.1x895.0 (1.949)
     snakecycle  1105.9x691.2 (1.600) -> 1428.8x893.0 (1.600)
     CAG          958.0x691.2 (1.386) -> 1686.3x948.6 (1.778)

   CAG IS THE ONE THAT WAS NOT MERELY SMALL.  Its buffer is 960x540 and it was
   being drawn at 1.386 -- the squash the maglev mesa block further down
   predicts in writing, arriving here by the same route: an author height with
   width:auto, then max-width clamping the width against the cartridge's own
   960px cap, and a replaced element does not re-derive an author height when
   max-width binds.  `max-width: none` on the mount is the ants block's move
   (TASK-1491[Q]) for the ants block's reason; the cap stays in the cartridge's
   own stylesheet, where the arcade floor is entitled to it.

   WHY A ROOM AT ALL, AND WHY IT IS NAMED WHEN THE FIT IS NOT.  A contain fit
   needs to know the box it is fitting into, and the box a playfield actually
   gets is the stage MINUS the cartridge's own chrome rows -- the readout above,
   the console below.  This file cannot know what those cost (they REFLOW: golf's
   console is 114px at 1920 and taller when the window narrows), which is the
   whole reason the rule above it had to guess a fraction.  So the browser is
   asked instead: the mount becomes a column, its chrome rows keep their own
   height, and the row carrying the canvas takes what is left and declares
   itself a size container.  `100cqh` and `100cqw` are then EXACTLY the room,
   measured after reflow, at every window shape.
   The five are named because that turn -- making a mount a column -- is a
   layout decision about a cartridge, and it is wrong for a cartridge that has
   already made one.  Driven generically over all 24 on the rig it collapsed
   crystal keep's mount to 0px wide (its width comes from its own JS fit, and
   size containment stops a row contributing to it) and re-stretched its canvas
   to 1.677.  Which cartridges get a room is this surface's business and is the
   list this file already keeps seven of; WHAT a room does to a playfield is the
   one rule below, and it will fit the eighth cabinet without being edited.
   The row inside is found with `:has(> canvas)` rather than named, so no
   cartridge's internal class appears here.

   `width: 100%` ON THE MOUNT IS NOT DECORATION.  CAG's own stylesheet carries
   `margin: 0 auto`, which in a flex column turns stretch OFF and makes the item
   shrink to fit -- and a row with size containment contributes nothing to that
   fit, so the cabinet came out 456px wide around a 454px picture, measured.  A
   definite width is what stops a size-contained room from deciding how wide its
   own cartridge is.

   THE ARITHMETIC IS THE HEIGHT, NOT THE WIDTH, and that is not a preference.
     * min() rather than an aspect-ratio with a max on the other axis: a
       replaced element only re-derives the axis you did not author, so every
       "aspect-ratio + max-width" spelling stretches the moment the OTHER axis
       binds.  Driven on the rig at 1920x838, 800x838 and 1920x300, every such
       candidate came out at the container's own ratio in one of the three;
       min() came out at 1.800 in all three.
     * HEIGHT, with box-sizing: border-box, because these canvases carry a 4px
       neon border of their own.  Authoring the height means the border comes
       OUT of the budget, so the derived width lands just inside the room
       (measured 693.6 in a 700px room) instead of just outside it.  Authoring
       the width instead overflowed by ~4px, and capping that with max-height
       clamped the height without re-deriving the width -- a 0.4% squash, which
       is a distortion however small.
     * `aspect-ratio: auto` so the width is derived from the canvas's own
       BUFFER rather than from any aspect-ratio its stylesheet declares: that
       property applies to the border box under border-box sizing, which would
       put the border inside the ratio and squash the drawing by the same 0.4%.
       The buffer ratio is what the drawing is, and it is what the pointer maths
       assumes.
     * NEVER object-fit, for the reason the block above this one gives.
     * no max-width: with a correct ratio it can never bind, and if a ratio is
       ever wrong the honest failure is a playfield that sticks out where
       somebody will see it, not one silently squashed to fit.

   --gcart-ar IS THE CARTRIDGE'S, NOT OURS.  CSS cannot read a canvas's own
   buffer into a calc, so the ratio has to be declared -- and it is declared
   beside each `aspect-ratio` in metaeden.css, where a game's shape belongs and
   where the arcade floor can have it too.  The default is 16/9 because that is
   what the user asked for, and it is the safe direction to be wrong in: a ratio
   WIDER than the drawing only makes the fit smaller, while a narrower one is
   what overflows.  `(var(...))` is parenthesised because substitution is
   textual and `calc(100cqw / 9/5)` is a division by 9 and then by 5 -- that is
   not a hypothetical, it is what the first cut on the rig drew golf at: 76x42.
   server/metaeden_net_v4_game_surface.test.js reads each cabinet's buffer out
   of its own engine and requires the declared ratio to equal it, so the two
   cannot drift. */
.v4-game-page.gfs #gstage > [data-metaeden-golf],
.v4-game-page.gfs #gstage > [data-metaeden-pool],
.v4-game-page.gfs #gstage > [data-metaeden-artillery],
.v4-game-page.gfs #gstage > [data-metaeden-snakecycle],
.v4-game-page.gfs #gstage > [data-metaeden-cag-flight-ops],
.v4-game-page.gbare #gstage > [data-metaeden-golf],
.v4-game-page.gbare #gstage > [data-metaeden-pool],
.v4-game-page.gbare #gstage > [data-metaeden-artillery],
.v4-game-page.gbare #gstage > [data-metaeden-snakecycle],
.v4-game-page.gbare #gstage > [data-metaeden-cag-flight-ops] {
  display: flex;
  flex-direction: column;
  width: 100%;
  height: 100%;
  max-width: none;
}

.v4-game-page.gfs #gstage > [data-metaeden-golf] > *,
.v4-game-page.gfs #gstage > [data-metaeden-pool] > *,
.v4-game-page.gfs #gstage > [data-metaeden-artillery] > *,
.v4-game-page.gfs #gstage > [data-metaeden-snakecycle] > *,
.v4-game-page.gfs #gstage > [data-metaeden-cag-flight-ops] > *,
.v4-game-page.gbare #gstage > [data-metaeden-golf] > *,
.v4-game-page.gbare #gstage > [data-metaeden-pool] > *,
.v4-game-page.gbare #gstage > [data-metaeden-artillery] > *,
.v4-game-page.gbare #gstage > [data-metaeden-snakecycle] > *,
.v4-game-page.gbare #gstage > [data-metaeden-cag-flight-ops] > * { flex: 0 0 auto; }

.v4-game-page.gfs #gstage > [data-metaeden-golf] > *:has(> canvas),
.v4-game-page.gfs #gstage > [data-metaeden-pool] > *:has(> canvas),
.v4-game-page.gfs #gstage > [data-metaeden-artillery] > *:has(> canvas),
.v4-game-page.gfs #gstage > [data-metaeden-snakecycle] > *:has(> canvas),
.v4-game-page.gfs #gstage > [data-metaeden-cag-flight-ops] > *:has(> canvas),
.v4-game-page.gbare #gstage > [data-metaeden-golf] > *:has(> canvas),
.v4-game-page.gbare #gstage > [data-metaeden-pool] > *:has(> canvas),
.v4-game-page.gbare #gstage > [data-metaeden-artillery] > *:has(> canvas),
.v4-game-page.gbare #gstage > [data-metaeden-snakecycle] > *:has(> canvas),
.v4-game-page.gbare #gstage > [data-metaeden-cag-flight-ops] > *:has(> canvas) {
  display: flex;
  flex: 1 1 auto;
  align-items: center;
  justify-content: center;
  min-width: 0;
  min-height: 0;
  container-type: size;
}

.v4-game-page.gfs #gstage > [data-metaeden-golf] > * > canvas,
.v4-game-page.gfs #gstage > [data-metaeden-pool] > * > canvas,
.v4-game-page.gfs #gstage > [data-metaeden-artillery] > * > canvas,
.v4-game-page.gfs #gstage > [data-metaeden-snakecycle] > * > canvas,
.v4-game-page.gfs #gstage > [data-metaeden-cag-flight-ops] > * > canvas,
.v4-game-page.gbare #gstage > [data-metaeden-golf] > * > canvas,
.v4-game-page.gbare #gstage > [data-metaeden-pool] > * > canvas,
.v4-game-page.gbare #gstage > [data-metaeden-artillery] > * > canvas,
.v4-game-page.gbare #gstage > [data-metaeden-snakecycle] > * > canvas,
.v4-game-page.gbare #gstage > [data-metaeden-cag-flight-ops] > * > canvas {
  box-sizing: border-box;
  aspect-ratio: auto;
  width: auto;
  height: min(100cqh, calc(100cqw / (var(--gcart-ar, 16 / 9))));
  max-width: none;
  margin-left: auto;
  margin-right: auto;
}

/* 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; }

/* ...AND THE VIEW CANVAS IS HANDED ITS OWN LAYOUT BACK (TASK-1637[Q]).  The
   cartridge authors it absolute, inset 0, 100% by 100% of the stage: the stage
   box IS the picture box.  The id-carrying `#gstage canvas` fallback further up
   outranked that, so on a bare page at 1280x720 the picture came out 768x461,
   anchored top-left in a 1250px stage, clipped at the bottom and with the right
   38% of the stage empty; on the framed page it was 492x295 in a 790x475
   stage.  The framed page never relied on the fallback, it was wrong there
   too.  The same hand-back the Vanish Boom view got just above, for the same
   reason.  The big view (.is-filled) keeps its own centred contain fit, which
   the fallback was outranking in exactly the same way: the second rule, one
   class heavier, gives it back.  `.is-filled` is written apart from the
   cartridge class on purpose, and so is the state below, because
   server/metaeden_net_v4_game_surface.test.js lists every cartridge name this
   sheet carries (H43a) and reads a name as the run after `.meta-` up to a
   space, a brace or a comma. */
.v4-game-page #gstage canvas.meta-xyz-view {
  width: 100%;
  height: 100%;
  max-width: none;
  max-height: none;
}
.v4-game-page #gstage .is-filled canvas.meta-xyz-view {
  width: min(100vw, 166.6vh);
  height: min(100vh, 60vw);
}

/* A STAGE THAT FILLS THE PICTURE HAS TO KEEP THE PICTURE'S SHAPE.  Every lens
   the engine has is 5:3 (XYZCore.LENSES), so the buffer always is, and the
   cartridge's own sheet gives the stage `aspect-ratio: 5/3` to match.  In the
   bare page and the pop-out the rule above hands the stage the row's HEIGHT
   (flex: 1), and a stage stretched to the full width as well is a box of the
   wrong shape: at 1280x720 it came out 1250x305, which a canvas filling it
   would draw 4:1.  So in those two modes the stage takes the height it is
   given and derives its width from the ratio, centred; and where the row is
   taller than a full-width 5:3 box, the max-height caps the height at three
   fifths of the cartridge's width so the ratio binds the other way.  The
   cartridge is an inline-size container for that one calc and nothing else;
   its width comes from the stage it sits in, never from its content.  Scoped
   to the two modes because only there is the row's height definite: in the
   ordinary framed column the stage is full width and the ratio sets its
   height, which is already right.
     The mode classes sit inside :where(), so this weighs what the stage rule
   above weighs and wins by coming later, while the phone fit's sideways grid
   (metaeden-v4-game-fit.css, `.v4-game-page.gfs #gstage .meta-xyz-stage`) is
   one class heavier and still places the stage its own way.  One :where() per
   selector, not one list inside it: the surface suite's scope check (H45)
   splits selectors on commas.  The big view is a fixed box over the whole
   window and takes none of this, which the last rule says; align-self goes
   back too, because a centred absolutely positioned box shrinks to its content
   instead of filling its insets, and this one's content is a 0px-tall flow. */
.v4-game-page.gbare #gstage .meta-xyz-game,
.v4-game-page.gfs #gstage .meta-xyz-game { container-type: inline-size; }
.v4-game-page:where(.gbare) #gstage .meta-xyz-stage,
.v4-game-page:where(.gfs) #gstage .meta-xyz-stage {
  align-self: center;
  width: auto;
  max-width: 100%;
  max-height: calc(100cqw * 3 / 5);
}
.v4-game-page #gstage .is-filled.meta-xyz-stage {
  align-self: auto;
  max-width: none;
  max-height: none;
}

/* ==========================================================================
   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;
}

/* 13-SHIPS IS THE NEXT ONE THE NOTE ABOVE PREDICTED (TASK-1443[Q]), and it
   arrived exactly as that note said it would: red on the day it shipped,
   named by the guard rather than found in a screenshot.
   ITS CANVAS IS A PLAYFIELD, NOT AN ICON, which is why the numbers here are
   not the auxiliary block's width:auto/height:auto.  The cartridge authors
   `.meta-bships-stage canvas{width:100%;height:auto}` over a 960x540 buffer
   inside a stage capped at 980px, so it letterboxes itself and has done its
   own arithmetic already.  The generic rule carries an id and would otherwise
   overrule that with a height in --gfs-h, turning a canvas that fits into one
   sized against the frame instead of against its own stage.
   Layout handed back and nothing else: no colour, no font, no border, no
   background, and nothing that would look different if the generic rule had
   never reached it.
   THE 980px CAP IS ORDINARY FLOW'S ONLY, SINCE TASK-1631[Q].  The last block
   in this file releases it in the big view and the pop-out and gives the stage
   a contain fit instead; out here -- the in-page panel, and the arcade floor,
   which mounts this cabinet too -- the cap still binds and is still right.
   This hand-back is unchanged by that: the picture fills its own stage in
   every view, and it is the STAGE that stopped being 980px wide. */
.v4-game-page #gstage .meta-bships-stage canvas {
  width: 100%;
  height: auto;
  max-width: 100%;
  max-height: none;
}

/* AND THE ONE AFTER IT (TASK-1455[Q]), which is the third time the note two
   blocks up has predicted its own next customer.  ANTS: Colonize Earth authors
   `.meta-ants-stage canvas{width:100%;height:auto;display:block}` over a
   960x540 buffer inside a stage capped at 980px, so it letterboxes itself and
   has done its own arithmetic already.  The generic rule carries an id and
   would otherwise overrule that with a height in --gfs-h, sizing a playfield
   against the frame instead of against its own stage.
   Layout handed back and nothing else: no colour, no font, no border, no
   background, and nothing that would look different if the generic rule had
   never reached it.
   ITS 980px CAP READS THE SAME WAY as 13-SHIPS' above, and for the same
   reason: the last block in this file releases it in the big view and the
   pop-out, ordinary flow keeps it, and this hand-back is what the picture
   keeps filling its stage with either way.  THIS RULE IS THE ONLY HAND-BACK
   THIS CARTRIDGE HAS SINCE TASK-1631[Q] -- the mode used to carry a second
   copy of it whose one difference, max-height:100%, cost a 0.09% squash the
   moment it bound. */
.v4-game-page #gstage .meta-ants-stage canvas {
  width: 100%;
  height: auto;
  max-width: 100%;
  max-height: 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;
}

/* CRYSTAL KEEP (TASK-1569[Q]): a 1536 by 864 canvas, 16:9 exactly, inside a
   mount ck.hud.js lays out as a flex column of two boxes, the stage and the
   dock under it.  In the big view the mount takes the frame's height; out of
   it the mount's height is its own content and the column's width binds.

   THE CANVAS FILLS THE STAGE, AND THE STAGE IS SIZED TO THE CANVAS
   (TASK-1595[Q]).  It used to CONTAIN inside a stage that took whatever
   height was going -- width and height auto under a 100% ceiling on both
   axes -- and that is exactly what left a dead band between the hall and the
   dock wherever the room was taller than 16:9 of its width.  ck.hud.js
   measures the room and writes the stage's box (--ck-cw, --ck-ch), so the
   fit is already done by the time this rule is read and the canvas has only
   to fill what it is given.  The line is still here for the reason it was
   added: the generic `#gstage canvas` rule carries an id and would otherwise
   put a 64% viewport height on it, in both modes. */
.v4-game-page.gfs #gstage > [data-metaeden-crystal-keep],
.v4-game-page.gbare #gstage > [data-metaeden-crystal-keep] { height: 100%; }
.v4-game-page #gstage canvas.ck-canvas {
  width: 100%;
  height: 100%;
  max-width: 100%;
  max-height: 100%;
}

/* ==========================================================================
   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;
}

/* ==========================================================================
   AND THE FIFTH, WHICH NEEDED ONLY ITS CAP TAKING OFF (TASK-1491[Q]).  The
   user, on cabinet 18: "even in full screen the bug game screen is still super
   small, please fix."

   ONE CAUSE, NOT TWO, AND THE HALF THAT WAS ALREADY RIGHT IS WORTH SAYING
   FIRST.  ANTS: Colonize Earth does NOT reach the generic canvas rule:
   TASK-1455[Q] handed `.meta-ants-stage canvas` back three blocks up, so the
   picture was already width:100% of its own stage with its 960x540 buffer
   deriving the height, and none of the 64% budget for chrome rows this
   cartridge does not have was ever being spent on it.  The whole of the fault
   was `.meta-ants-game{max-width:980px}` in metaeden-ants.js, which is right in
   ordinary flow and is what the arcade floor wants, and which in the mode
   clamped a picture that had 2000x1052 of stage to fill.
   MEASURED at 2000x1150 before: stage 2000x1052, canvas 978x550 -- 49% of the
   width and 52% of the height, sitting in a black field, which is the
   screenshot that opened the ticket.  After: 1866x1050, up from 0.54 megapixels
   to 1.96, with the leftover width as pillarbox because a 16:9 game in a stage
   that is 1.90 once the two chrome bars come out of the frame cannot help
   costing that.

   ...AND 13-SHIPS, WHICH IS THE SAME CABINET TWICE (TASK-1631[Q]).  The user,
   on the pop-out: "why is the game view for this game so small even in poped-out
   mode? (and in full screen too)".  It is the report above with a different
   cabinet number.  metaeden-bships.js authors the same `max-width:980px` on its
   own root, TASK-1443[Q] handed its canvas back two blocks up in the same words,
   and nothing here had ever released the cap for it.
   MEASURED in an 1850x1000 pop-out before: stage 1850x956, picture 978x550 --
   53% of the width, which is the screenshot that opened this one.  After:
   1699x956.  Fullscreen at that viewport went 978x550 to 1648x927; its frame is
   locked 16:9, so the stage there is 1777x927.

   THE FIT BOUNDS BOTH AXES NOW, WHICH IS WHAT THE FIRST CUT DID NOT.  Until
   this ticket the holder took `height:100%` and let its ratio derive the width,
   with `max-width:100%` behind it -- and a box whose height is already definite
   does not re-derive that height when max-width binds.  In the big view that
   never showed, because the frame is locked 16:9 and the chrome comes out of its
   HEIGHT, so the stage is always wider than the game.  A POP-OUT IS LOCKED TO
   NOTHING: it is the window, less one row.  MEASURED on the rig at 900x900 with
   the first cut: a 900x856 holder around a picture of 898x505 -- 351px of
   bordered, scanlined, vignetted box below a game that had stopped there, since
   both overlays are inset:0 of that holder.  No stretch and no crop, and still
   the wrong box.
   min(100%, calc(100cqw * 9 / 16)) is the whole of the correction: the height is
   the smaller of what is left and what the width can pay for, so whichever axis
   runs out first decides and the ratio derives the other from a height already
   inside both.  Driven at 1850x1000, 1400x640, 900x900 and 700x1000 the holder
   came out 1.778 in all four; the first cut came out 1.051 and 0.734 in the last
   two.  container-type is inline-size rather than size: the mount has to publish
   its WIDTH to a child solving for height, and size containment would also
   require it to stop reading its own content in the block axis, which is not a
   promise this mount can make.

   WHY THESE TWO ARE STILL OUT OF THE GENERIC --gcart-ar RULE, which is the
   question this block has now been asked twice.  That rule puts the size
   container on the canvas's HOLDER and solves for the canvas.  Here the holder
   is the thing that has to end up the size of the drawing, because both
   overlays each cartridge owns -- .meta-ants-scan / .meta-ants-vig and their
   13-SHIPS twins -- are absolute at inset:0 of it, and a holder that is a size
   container cannot be measured from its canvas.  So the ratio sits on the holder
   and the canvas merely fills it, which is the fishing block's reasoning further
   up, and is why these two have a block of their own rather than two more lines
   in that selector list.  box-sizing is border-box because each stage carries a
   1px border of its own, and the canvas keeps width:100%;height:auto rather than
   100% of both, so a wrong-shaped holder would letterbox the game instead of
   stretching it.

   NO CHROME ROW TO PIN, AND ONE THAT CAN ARRIVE ANYWAY.  Ants draws its readout
   and its control hints INSIDE the 960x540 buffer (TOPB 50, BOTB 54 in
   metaeden-ants.js) and its mount has exactly one child.  13-SHIPS' mount has
   four, three of them display:none -- but metaeden-bships-net.js shows
   [data-bships-net-live] the moment a fleet is linked.  flex-direction:column
   with flex:0 1 auto is what makes that a non-event: the row lands under the
   picture at its own height and the holder shrinks by it, re-deriving its width
   from the shrunk height because the ratio is on the box rather than on a
   number.  MEASURED at 1850x1000 with that row shown: 1651x929 at 1.778, the row
   1850x27 beneath it, nothing overflowing.
   align-self:center is the pillarbox and justify-content:center the letterbox;
   without them the picture pools against an edge, which is the bug the fishing
   block above exists to fix.

   ONLY THE MODE.  Out here the cap never binds: .gplay is 820px, narrower than
   the 980 it would clamp at, so the picture already fills the column.  Measured
   818x460 in the ordinary window, for BOTH cabinets and unchanged by this
   ticket, which is the width available with the ratio deriving the rest.
   Nothing to hand back and nothing to fix, so this block has no :not(.gfs) half;
   the fishing one needs its half because that cartridge really was sized off
   --gfs-h out there.  The cap is left where it is in metaeden-ants.js and
   metaeden-bships.js for the same reason: the arcade floor mounts both cabinets
   too and is entitled to it. */
.v4-game-page.gfs #gstage > [data-metaeden-ants],
.v4-game-page.gbare #gstage > [data-metaeden-ants],
.v4-game-page.gfs #gstage > [data-metaeden-bships],
.v4-game-page.gbare #gstage > [data-metaeden-bships] {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  justify-content: center;
  height: 100%;
  min-height: 0;
  max-width: none;
  container-type: inline-size;
}

.v4-game-page.gfs #gstage .meta-ants-stage,
.v4-game-page.gbare #gstage .meta-ants-stage,
.v4-game-page.gfs #gstage .meta-bships-stage,
.v4-game-page.gbare #gstage .meta-bships-stage {
  box-sizing: border-box;
  flex: 0 1 auto;
  align-self: center;
  width: auto;
  height: min(100%, calc(100cqw * 9 / 16));
  max-width: 100%;
  min-height: 0;
  aspect-ratio: 16 / 9;
}

/* ==========================================================================
   AND THE SIXTH, WHICH WAS NOT SMALL SO MUCH AS OUT OF SHAPE (TASK-1491[Q]).
   The user, on the same day and about the cabinet next door: "similar issue
   with this, aspect ratio should at least be the same ratio as 1080p."

   THE SECOND HALF OF THAT SENTENCE IS THE FINDING.  Maglev Mesa's buffer is
   ALREADY 960x540, so there was nothing to correct about the shape it draws;
   what was wrong was the shape it was being drawn INTO.  Unlike Ants above,
   this cartridge styles its playfield `canvas.meta-mesa-view` by CLASS in its
   own stylesheet and was never handed back, so it reached the generic rule --
   and the generic rule is the one that can produce a stretch, by a route its
   own header does not cover.  It sets an author HEIGHT with width:auto, which
   a replaced element resolves through its intrinsic ratio; then max-width:100%
   clamps that width against the cartridge's own 980px cap, and a replaced
   element does NOT re-derive an author height when max-width binds.  So the
   width came down and the height stayed where it was put.
   MEASURED at 2000x1150 before: 978x720, a ratio of 1.358 against the 1.778 it
   draws -- the picture squashed by a quarter of its width inside a stage that
   had 2000x1052 to give it.  This is the failure the fishing block's header
   predicts in writing ("max-width would clamp the holder against a height
   flexbox has already fixed"), arriving at a canvas rather than a holder.
   After: 1792x1008, exactly 1.778.

   ONE CHROME ROW, WHICH IS THE WHOLE OF WHAT MAKES THIS NOT THE ANTS BLOCK.
   `<p class="meta-mesa-under">` carries the control legend in the cartridge's
   own DOM, OUTSIDE the picture, so this cannot take the frame's full height the
   way the block above does -- the row's height has to come out of the budget
   first.  That is the fsx/smx chain rather than the ants one: the mount is a
   flex COLUMN, the row is pinned to its own height, and the stage takes the
   leftover with its ratio deriving the width that goes with it.
   The auto margins are why it centres and, less obviously, why it can derive a
   width at all: a stretch-aligned column item takes the full cross size and
   aspect-ratio would have nothing left to decide, and auto margins on both
   sides are what turn stretch off.  Same device as .fsx-scr's.

   AND THE HAND-BACK IS BOTH MODES AND NEITHER, which the fit above is not.
   Out of the mode the cap never binds (.gplay is 820px) so nothing is squashed
   there, but the generic rule still drew 524x295 in an 820px column -- correct
   shape, three fifths of the width -- for the same reason fishing did before
   TASK-1163[Q].  Handing the canvas back gives it the column, measured 818x460,
   and it is the same one line either way, so it is written once outside both
   mode selectors rather than twice inside them. */
.v4-game-page #gstage canvas.meta-mesa-view {
  box-sizing: border-box;
  width: 100%;
  height: auto;
  max-width: 100%;
  max-height: 100%;
}

.v4-game-page.gfs #gstage > [data-metaeden-maglev-mesa],
.v4-game-page.gbare #gstage > [data-metaeden-maglev-mesa] {
  box-sizing: border-box;
  display: flex;
  flex-direction: column;
  height: 100%;
  min-height: 0;
  max-width: none;
}

.v4-game-page.gfs #gstage .meta-mesa-under,
.v4-game-page.gbare #gstage .meta-mesa-under { flex: 0 0 auto; }

.v4-game-page.gfs #gstage .meta-mesa-stage,
.v4-game-page.gbare #gstage .meta-mesa-stage {
  box-sizing: border-box;
  flex: 1 1 auto;
  width: auto;
  min-height: 0;
  max-width: 100%;
  margin-left: auto;
  margin-right: auto;
  aspect-ratio: 16 / 9;
}

/* ---- the phone list (TASK-1647[Q]) -----------------------------------------
   /sites/v4-game.html?list=phone, filled by metaeden-v4-game.phonelist.js from
   the GAMES[] table: games that play on a phone first, the rest under their
   own heading.  Colours inherit the theme like .gmiss does; every run is 15px
   or more, and each game's link is a 44px tap row. */
.v4-game-page .gphonelist {
  margin: 16px 0 0;
  text-align: left;
}
.v4-game-page .gphonelist .gpl-lead {
  margin: 0 0 12px;
  font-size: 17px;
}
.v4-game-page .gphonelist .gpl-sec { margin: 0 0 20px; }
.v4-game-page .gphonelist .gpl-head {
  margin: 0;
  padding: 6px 12px;
  border-bottom: 2px solid #cee3f8;
  font: 400 24px/1.2 "Bebas Neue", "Arial Narrow", Verdana, sans-serif;
  letter-spacing: 2px;
  text-transform: uppercase;
}
.v4-game-page.dark .gphonelist .gpl-head { border-color: #333333; }
.v4-game-page.neon .gphonelist .gpl-head { border-color: #f2c200; }
.v4-game-page.dxhr .gphonelist .gpl-head { border-color: #3a3020; }
.v4-game-page.grey .gphonelist .gpl-head { border-color: #888888; }
.v4-game-page .gphonelist .gpl-head i {
  font-style: normal;
  font-size: 18px;
  margin-left: 6px;
}
.v4-game-page .gphonelist .gpl-note {
  margin: 8px 12px 0;
  font-size: 15px;
}
.v4-game-page .gphonelist .gpl-list {
  list-style: none;
  margin: 0;
  padding: 0;
}
.v4-game-page .gphonelist .gpl-list li {
  padding: 4px 12px 10px;
  border-bottom: 1px solid rgba(128, 128, 128, 0.35);
}
.v4-game-page .gphonelist .gpl-game {
  display: flex;
  align-items: center;
  min-height: 44px;
  font-size: 18px;
}
.v4-game-page .gphonelist .gpl-blurb {
  margin: 0;
  font-size: 15px;
  line-height: 1.4;
}
