/* Registered, and the registration is load-bearing twice over.
   `getComputedStyle` hands back an unregistered custom property UNRESOLVED — `parseFloat`
   of "clamp(0px, ...)" is NaN — and seatNamePlacement has to read this number to know how
   much height the coordinate labels have claimed before it decides whether a name may take
   a line.
   And a style query compares an unregistered property as TOKENS, so
   `style(--bug-coord-gap: 0px)` would never match the clamp() that computes to zero.
   Registered, it is compared as a computed length and matches. That query is how the
   coordinates switch to sitting on the squares, so without this declaration the switch
   silently never fires.
   The length initial value also means every element that sets nothing reserves nothing.

   Every mode sets it, through the shared rule below — including the two that always
   resolve to zero. That is the point: no mode is named, each supplies its own room. */
@property --bug-coord-gap {
    syntax: '<length>';
    inherits: true;
    initial-value: 0px;
}

/* THE ANALYSIS PAGE'S NAME DECISION, registered for the same second reason as the gap
   above: a style query compares an unregistered property as TOKENS, so `style(--bug-name-
   outside: 1)` would never match the `min(1, round(down, ...))` that computes to 1.
   Registered as a number, it is compared as a computed value and matches.

   Initial value 0 — a stack that supplies no room reserves no line, which is what makes
   portrait and full zoom keep their names inside the strip without either being named. */
@property --bug-name-outside {
    syntax: '<number>';
    inherits: true;
    initial-value: 0;
}

/* WHICH WAY THE MOVE-TIMES CHART IS DRAWN, decided here and carried out by
   `trackChartOrientation()` in movetimeChart.ts. Registered for the same reason as the two
   above: unregistered, `getComputedStyle` hands the value back as an unresolved token.

   The RULE lives in CSS because it is a question about a box, and the boxes are what this
   file decides — the panel is tall and narrow in portrait's tools column and wide and short
   in a landscape one, so the answer follows the layout rather than being a second opinion
   about it. Only the ACT of re-rendering needs script: `inverted` is a Highcharts render
   option that transposes axes, bar direction, tooltip anchoring and hit areas, and no
   stylesheet can reach that. A `transform: rotate(90deg)` would turn the picture on its
   side, labels and pointer geometry included, which is a different thing entirely.

   Initial value 0 — horizontal. A panel whose tab is not selected has `display: none` and
   so no principal box, which makes it no container at all and leaves this at its initial
   value. That is also what makes showing the tab work: the box goes from nothing to real,
   the observer fires, and the answer is computed for the first time against a real size. */
@property --bug-chart-inverted {
    syntax: '<number>';
    inherits: true;
    initial-value: 0;
}

/* THE USERNAME'S CEILING, AND THE ONE SIZE ON THIS PAGE THAT IS NOT DERIVED FROM A BOARD.
   16.8px is what a single-board round page draws a username at: `round-player0` is
   `font-size: 1.2em` in site.css over an `html` of 14px, measured on the live page at
   1276 wide — 16.8px computed, in a 50px line box. Every other variant on this site uses
   that size and it reads well, so it is the right ceiling here too.

   One constant, in every mode and for both seats. A username's readable size is not a
   property of a chessboard: sizing it from the square gave 7.21px on a seat whose row was
   165.3px wide with nothing else on it, and 16.74px on its neighbour, for no reason other
   than that one board was smaller than the other. The pocket must match its board because
   it sits pieces on squares; a name must be legible. */
:is(.round-app, .analysis-app).bug {
    --bug-name-fs-max: 16.8px;
}

/* site.css gives every page a `margin-bottom: 2vmin`, which on this one is a page
   taller than the viewport it has just been pinned to — about 8px of it in portrait.
   It does not scroll, because all three modes set `overflow: hidden` on this body,
   so it is simply clipped: the bottom seat strip loses the end of a username to a
   margin nobody can see.

   Neutralised here rather than in site.css, which every other page depends on, and
   at the top level rather than per mode, because all three pin the body the same way
   and inherit the same margin.

   The analysis view is included since it started pinning itself in portrait for the same
   reason. Measured before: a document 843px tall inside an 835px viewport, all of the
   difference being this margin. */
body[data-variant='bughouse'][data-view='round'],
body[data-variant='bughouse'][data-view='analysis'] {
    margin-bottom: 0;
}

/* ROOM UNDER A BOARD FOR ITS FILE LABELS, AND WHAT HAPPENS WHEN THERE IS NONE.
   ---------------------------------------------------------------------------
   The labels are absolutely positioned and hang below the board by --files-bottom, which
   lands them on the seat strip beneath. Where the mode has height to spare, that height is
   spent pushing the strip clear of them instead; where it has none, they go back to sitting
   on the squares, which is what the two mobile modes have always done.

   The overhang is read from --files-bottom rather than written as 16px, so it follows the
   labels. Read HERE, one level above the stacks that override --files-bottom, which is what
   keeps it from being a var() cycle: at this element --files-bottom is still the :root value.

   The floor exists because a 4px label is not a small label, it is an unreadable one. Short
   landscape's spare height is only its quantisation remainder — 4.3px measured at 1276x351 —
   and a gap that size would buy nothing while costing the strip its room. Below the floor the
   gap is not merely small, it is ZERO, so the room goes back to the strip and the labels move
   onto the squares.

   TUNED, not guessed. The label is 0.75 of the gap, so the floor is really a decision about
   the smallest letter worth putting outside the board, and the honest comparison is against
   what replaces it: an internal label is 0.3 of a square, measured 18.3px on a 61px square.
   At a floor of 8 the last external label is 6px, which loses that comparison badly — the
   labels would be getting smaller in order to stay outside, when going inside would make them
   three times the size. At 12 the smallest external label is 9px and still reads, and anything
   under it goes internal and gets bigger rather than smaller. Measured across the ramp by
   driving --bug-coord-room directly: 16 -> 12px label, 14 -> 10.5, 12 -> 9, 10 -> 7.5, 8 -> 6. */
/* BOTH TWO-BOARD PAGES, one treatment. Everything about coordinates in this file — these two
   numbers, the on-the-squares rules near the end, the parity colouring and the `not style()`
   block that puts the labels back outside — is written once and selected for both app roots.

   It was scoped to the round page alone until 2026-08-29, which left the analysis page computing
   `--bug-coord-gap` and then ignoring it: measured 0px at 100% zoom on a 1276x551 window, the
   round page's signal for "no room, draw them on the squares", while its files sat at
   `bottom: -16px` outside the board at chessground's own 0.8 opacity and 11.9px.

   Widened rather than copied, deliberately. The colouring below is sixteen selectors that pair
   each label with the square it sits on; a second copy under the other app class would be the
   same body of rules with no mechanism keeping the two in step. */
:is(.round-app, .analysis-app).bug {
    --bug-coord-overhang: calc(-1 * var(--files-bottom));
    --bug-coord-floor: 12px;
}

/* The rule, written once, against a room each mode supplies for itself.
   A mode that sets no room gets the 0px default and its labels sit on the squares — which is
   how portrait and short landscape keep their present appearance without being named.

   `--bug-coord-fits` is the floor as arithmetic rather than as a branch: the capped room over
   the floor, rounded down and capped at 1, is 0 below the floor and 1 at or above it. It
   multiplies the gap to nothing in the first case and leaves it alone in the second, so ONE
   variable carries the whole answer — which is what lets the style query ask one question. */
.bug-own-stack, .bug-partner-stack {
    --bug-coord-room: 0px;
    --bug-coord-capped: clamp(0px, var(--bug-coord-room), var(--bug-coord-overhang));
    --bug-coord-fits: min(1, round(down, var(--bug-coord-capped) / var(--bug-coord-floor), 1));
    --bug-coord-gap: calc(var(--bug-coord-capped) * var(--bug-coord-fits));
}

/* The gap itself: the board's box carries it, so the strip below is pushed and the board is
   not resized. On the board box rather than on a grid row because the two stacks live in
   different containers — the viewer's in the app's grid, the partner's in .bug-right-column —
   and a row could only ever have served one of them.

   `display: block` is load-bearing here, not tidying, and for the second time in this file.
   `selection` is an unknown element with no display of its own, so it is an INLINE box, and a
   vertical margin on an inline box does NOTHING — the gap computed to a correct 16px, the
   margin was set, and the strip did not move a pixel. The portrait block already blockified
   these two for the same underlying reason (a width does not apply to an inline box either);
   stated once here for every mode instead, since the trap is the element's, not the layout's. */
.bug-own-stack > selection, .bug-partner-stack > selection {
    display: block;
    margin-bottom: var(--bug-coord-gap);
}


/* THE RESIZE HANDLE FOLLOWS THE MODE, NOT THE VIEWPORT WIDTH.
   ------------------------------------------------------------------------------------
   The handle exists to drag a board's zoom, so it belongs exactly where zoom reaches the
   board — which is tall landscape and nowhere else. Short landscape and portrait size their
   boards from `--bug-sq` and `--bug-portrait-sq` with no zoom scale anywhere in the
   expression, so there is nothing for a drag to change: cgCtrl.ts's handler would write the
   Zoom setting, `setBoardZoom` would republish, and every track would come back the same. A
   control that visibly does nothing is worse than an absent one.

   WHAT THIS REPLACES was two half-rules that happened to agree most of the time. site.css
   shows `cg-resize` under `@media (min-width: 799.3px)` — a WIDTH test, which is not the
   question — and short landscape then hid it again by hand. Portrait was never stated at all;
   it escaped only by being 386px wide on the harness, so a portrait tablet would have shown a
   dead handle. And the width test failed in the other direction too: measured at 682x647,
   which IS tall landscape and does honour zoom, the handle vanished because the window had
   crossed 799.3px — the mode that most needs the control was the one that lost it, and with no
   handle and boards too large for the viewport the only way back was the settings panel.

   `display: none` rather than `visibility` or opacity: a display-none element takes no pointer
   events, so the control is gone rather than merely invisible.

   THE GEOMETRY IS RESTATED, and only because site.css keeps it INSIDE that width query — the
   positioning, the size and the two pseudo-element strokes all disappear below 799.3px along
   with the display. Setting `display: block` alone would leave a static, zero-sized box in
   flow. Restated here it is scoped to the bughouse apps, so the rest of the site keeps site.css
   rule for rule.

   One thing the handle does NOT honour, worth knowing before trusting it as the whole control:
   its drag clamps to 0..100 (`cgCtrl.ts`), not to `minZoomPercent()`, so it can drive the
   stored setting below the layout's floor. `setBoardZoom` clamps on the way to the boards, so
   nothing is drawn too small — the drag simply stops having an effect while the slider would
   have stopped moving. */
:is(.round-app, .analysis-app).bug cg-resize {
    display: none;
}
/* THE COLLAPSED SITE NAV MUST NOT OCCUPY HEIGHT ON A PAGE THAT CANNOT SCROLL.
   ------------------------------------------------------------------------------------
   Under `max-width: 799px` site.css turns the header nav into a burger menu and hides it with
   `transform: translateX(-100%)`. A transform moves where a box is PAINTED and not where it is
   LAID OUT, so the nav keeps its full height in flow — a column of 20rem-wide links, measured at
   1380px — and the document grows by it. Nothing shows, and the page scrolls 600-800px of empty
   space: measured 603px at 627x835 and 790px at 682x647.

   This has only ever been reachable on a two-board page in a mode that both PINS the body and
   SHOWS the header. Portrait and short landscape hide the header, so it was masked there; tall
   landscape shows it but was, until the cut-off moved, always wider than 799px. Routing portrait
   tablets here makes every tablet narrower than that threshold, so it is now the common case.

   `position: fixed` and not `absolute`. Out of flow is only half of it: an absolutely positioned
   box still contributes SCROLLABLE OVERFLOW where it extends past the viewport, and this menu is
   900px tall against an 835px screen — measured, that left 65px of the 603 still there. A fixed
   box contributes none, and viewport-anchored is what a burger menu wants anyway.

   The reveal is untouched: the links slide in under their own transforms, over the page, which is
   what the menu already does. `max-height` with `overflow-y: auto` is the other half of making it
   fixed — a menu taller than the screen has to be scrollable on its own, since there is now no
   page scroll to reach its foot with.

   Scoped to the bughouse pages: the same declaration site-wide would be the better fix, but every
   other page scrolls anyway and none of them promises not to.

   AND SCOPED TO THE WIDTH AT WHICH THE MENU COLLAPSES, which is not optional. Unscoped, the same
   rules reach the ordinary horizontal nav on a wide screen, where `position: fixed` shrinks it to
   fit — 4px narrower than its own content — and `overflow-y: auto` makes CSS compute the OTHER
   axis to `auto` as well. The result was a 20px horizontal scrollbar drawn under the menu links,
   measured at 2495px wide: a grey bar across the top of the page with nothing wrong behind it. */
@media (max-width: 799px) {
    body[data-variant='bughouse'] .topnav {
        position: fixed;
        max-height: 100%;
        overflow-y: auto;
    }
}

/* THE THREE MODES, AND THE ONE LINE THAT SEPARATES THEM.
   ------------------------------------------------------------------------------------
   Every viewport resolves to exactly one of:

     (aspect-ratio <= 9/16)                        PORTRAIT — phones only
     (aspect-ratio >  9/16) and (height <  600px)  SHORT LANDSCAPE
     (aspect-ratio >  9/16) and (height >= 600px)  TALL LANDSCAPE — the desktop layout

   THE CUT-OFF IS 9/16 BECAUSE THAT IS WHAT PORTRAIT CAN AFFORD, not because it is where
   phones stop. Portrait exists to give the player's own board the FULL WIDTH; the partner
   board then has to fit in what height is left, above it and with a gap. The own stack is
   ten rows of `w/8`, so `1.25w`; a partner stack of `f` times that square costs `1.25fw`;
   and the three must fit `h`:

       w/h  <=  (1 - gap) / (1.25 x (1 + f))

   So the cut-off ratio and "how small may the partner board be" are ONE decision seen from
   two sides. At 9/16 the partner board is 39% of the own board's square — 148px beside a
   375px board on a 375x667 phone — which is already better than the 36% this shipped with.
   Widen the cut-off and that fraction falls: 31% at 3/5, 26% at 16:10.

   It also lands in the gap the device population leaves. The widest common PHONE is 16:9 =
   0.5625 (iPhone SE 2/3, iPhone 8, budget Androids) and everything newer is narrower — 18:9,
   19.5:9, 20:9, 21:9. The narrowest TABLET is 16:10 = 0.625, then 0.70 for a modern iPad and
   0.75 for a 4:3 one. Nothing real sits between.

   A PORTRAIT TABLET IS NOT PORTRAIT. Measured at 627x835 (4:3): the own stack wanted 1.25 x
   624 = 780px of an 835px viewport, leaving 55px for a partner stack that needs 207 — the own
   board was drawn straight over the partner board and over two of the three end-of-game
   buttons, and nothing scrolled because the body is pinned, so it failed silently. Above the
   cut-off the two boards go side by side instead, which is what the tall landscape layout
   already does.

   RANGE SYNTAX, and deliberately. The boundary has to be STRICT on one side: 9/16 is a real
   viewport (360x640), so `max-aspect-ratio` and `min-aspect-ratio` would both match it and
   two templates would apply at once. The same syntax closes the identical hairline at 600px,
   where `min-height` and `max-height` both matched exactly 600. Needs Chrome 104 / Safari
   16.4 / Firefox 102 or newer. */

@media (aspect-ratio > 9/16) and (height >= 600px) {
    :is(.round-app, .analysis-app).bug cg-resize {
        display: block;
        position: absolute;
        right: -9px;
        bottom: -9px;
        width: 22px;
        height: 22px;
        cursor: nwse-resize;
        z-index: 10;
    }
    :is(.round-app, .analysis-app).bug cg-resize::before,
    :is(.round-app, .analysis-app).bug cg-resize::after {
        background: #000;
        content: '';
        position: absolute;
        width: 12px;
        height: 1px;
        left: 0;
    }
    :is(.round-app, .analysis-app).bug cg-resize::before {
        width: 5px;
        transform: translate(7px, 8px) rotate(-45deg);
    }
    :is(.round-app, .analysis-app).bug cg-resize::after {
        transform: translate(1px, 6px) rotate(-45deg);
    }
    :is(.round-app, .analysis-app).bug cg-resize:hover {
        border-radius: 50%;
        background: hsla(22, 100%, 42%, 0.5);
    }
    /* ONLY THE HANDLE BEING DRAGGED. `site.css` hangs the dragging look off `body.resizing`, which
       matches every handle on the page: harmless where there is one board, and wrong here, where
       both corners lit up green whichever one was grabbed. `cgCtrl.ts` now marks the handle
       itself; the body class stays, because it also suppresses text selection and that does have
       to be page-wide. The first rule undoes the shared one for the handle that is NOT being
       dragged — it is more specific than `.resizing cg-resize`, and narrower than the `:hover`
       rule above, which keeps working. */
    body.resizing :is(.round-app, .analysis-app).bug cg-resize {
        border-radius: 0;
        background: none;
    }
    body.resizing :is(.round-app, .analysis-app).bug cg-resize.resizing {
        border-radius: 50%;
        background: hsla(88, 62%, 37%, 0.5);
    }
}

/* *****************************************************************************************
 * analysis.css
 ******************************************************************************************* */
/* THE ANALYSIS PAGE, arranged as the round page is.
   ------------------------------------------------------------------------------------
   What this replaces: a flat seven-column grid

       board A | gauge | pocketA | pocketB | gaugePartner | d | board B

   with the two boards pinned to the OUTER edges and both pockets, the movelist, the
   engine panel and misc-info crammed into the centre gutter between them. A player
   compares the two boards constantly and had the whole page between them, while the tools
   they read sat in a column too narrow to read. Boards were `30vw * zoom` — width-derived,
   the model the round page abandoned — and measured 425.3px against a much smaller
   partner, neither filling its height.

   REPLACED, not adjusted. Those columns encode the old arrangement, down to a `0.8em`
   spacer column named `d`, and there is no sequence of edits from them to boards-adjacent
   that passes through a working layout.

   The page's elements are now two stacks — pocket / board / pocket in block flow, the
   same shape the round page uses — plus a gauge beside each and one tools column. Each
   gauge sits immediately RIGHT of the board it reports on, so it can never be read as
   belonging to the other one; the cost is that the left gauge sits in the seam, which is
   accepted because a gauge is a thin bar that belongs to the board on its left. */

/* Landscape: main board, its gauge, other board, its gauge, tools.
   ONE rule for both landscape modes — the round page splits desktop from short landscape
   because its vertical budget differs, and this page has no such split to make yet. */
@media (aspect-ratio > 9/16) {
    /* THE ZONE VOCABULARY, WRITTEN ONCE FOR BOTH PAGES.
       ------------------------------------------------------------------------------------
       Every landscape arrangement either page can be in, as a value rather than as a rule, so
       that the round page and the analysis page cannot drift into meaning different things by
       the same area name. They did drift: this page used to declare three rows — one zoneA and
       one zoneB — and spell `drop-tablist` while meaning by it what the round page means by
       `drop-tablist-b`. The class is chosen in toolsPlacement.ts against zone A's test or zone
       B's, so a page whose names disagree asks one zone's question and answers it in the
       other's space. Measured at 996x730: zone B correctly refused a 31.5px tab list into
       3.3px of room while zone A dropped it anyway, and it was drawn across board A's pocket.

       Read them as pictures of the strip beside the boards emptying from the bottom up:

         strip   every part still beside the board, on its own row
         a4      the bottom row widened to take the space the PARTNER board frees
         a34     the bottom two widened; a234 the bottom three
         a3      the bottom row's part has gone to zone B, the one above it to zone A
         a23     the same, one row further up

       The names are positions, never occupants — `zoneTools4` is "the fourth row beside the
       board", and what sits there is the round page's tab bar and this page's tab list. Both
       pages therefore state the same three rules about it: its slot, its zone A home, its zone
       B home. A page with fewer parts simply leaves rows empty, and an empty row collapses.

       Defined on both apps in EVERY landscape mode, because the two pages consume them in
       different ones — the round page's flat grid is the tall-landscape block, and this page
       makes no such split. A variable nothing references costs nothing where it is unused. */
    /* WHERE THE TOOLS LIVE, chosen in squareUnit.ts before either board is sized and published as
       one of these classes. `tools-beside` is the arrangement that has always existed — three
       columns, the tools in the third — and needs no rule of its own beyond being the default.

       THE OTHER THREE ARE TWO-COLUMN LAYOUTS. The third track collapses to nothing rather than
       being removed, so both pages keep one `grid-template-columns` declaration each and only the
       track's value changes: the round page's is written from the published squares and this
       page's from the stacks themselves, and neither has to be restated per home. */
    :is(.round-app, .analysis-app).bug {
        --bug-tools-track: minmax(0, 20vw);
    }
    :is(.round-app, .analysis-app).bug:is(.tools-below, .tools-zonea, .tools-lastresort) {
        --bug-tools-track: 0;
    }

    :is(.round-app, .analysis-app).bug {
        /* TOOLS BELOW: the boards keep a row of their own and every part goes beneath them, each
           spanning the FULL WIDTH of both boards. That width is the whole reason for this home —
           a part put in one of the board columns gets that board's width and no more, which was
           the first form of this template and left the chat at the left board's width with the tab
           bar stranded beside it. Rows with no occupant collapse, so the analysis page's two parts
           take two rows and the round page's four take four. The bar goes LAST, under everything,
           which is where it sits in every other arrangement.

           ZONE A IS DECLARED HERE TOO, though nothing is placed in it in this home. Both zones
           exist in every arrangement: zone A is the space the shorter stack frees, beside the own
           board and above zone B, and stating it keeps the geometry the same picture whichever
           home is in force. It also leaves the door open for a part narrow enough to prefer it
           while the rest are below — which the placement code can already express. Empty, its row
           is zero and costs nothing. */
        --bug-zones-below: 'ownstack stack'
                           'ownstack zoneA2'
                           'zoneB1   zoneB1'
                           'zoneB2   zoneB2'
                           'zoneB3   zoneB3'
                           'zoneB4   zoneB4';
        /* TOOLS IN ZONE A: the parts go under the RIGHT board, in the space it frees by being
           smaller than the left. The own stack spans the zone A rows, which is what makes that
           space exist; the parts stack because the region is narrow — four squares wherever the
           right board is on its floor — and stacking is the only way to use a tall narrow box.

           ZONE B IS DECLARED HERE TOO, and in every arrangement. It is the full width below BOTH
           boards whether or not anything is in it, and zone A stops where it begins: the own stack
           spans rows 1-4 and zone B is what follows, so the boundary is the bottom of the taller
           stack by construction rather than by arithmetic. An empty zone B is a zero-height zone B,
           not an absent one — the first version of this template simply had no zone B rows, and
           zone A's panel grew straight through the space where zone B should have started. */
        --bug-zones-zonea: 'ownstack stack'
                           'ownstack zoneA2'
                           'ownstack zoneA3'
                           'ownstack zoneA4'
                           'ownstack zoneA5'
                           'zoneB1   zoneB1'
                           'zoneB2   zoneB2';
    }

    :is(.round-app, .analysis-app).bug {
        --bug-zones-strip: 'ownstack stack zoneTools1'
                           'ownstack stack zoneTools2'
                           'ownstack stack zoneTools3'
                           'ownstack stack zoneTools4'
                           'zoneB1   zoneB1 zoneB1'
                           'zoneB2   zoneB2 zoneB2';
        --bug-zones-a4: 'ownstack stack  zoneTools1'
                        'ownstack stack  zoneTools2'
                        'ownstack stack  zoneTools3'
                        'ownstack zoneA4 zoneA4'
                        'zoneB1   zoneB1  zoneB1'
                        'zoneB2   zoneB2  zoneB2';
        --bug-zones-a34: 'ownstack stack  zoneTools1'
                         'ownstack stack  zoneTools2'
                         'ownstack zoneA3 zoneA3'
                         'ownstack zoneA4 zoneA4'
                         'zoneB1   zoneB1  zoneB1'
                         'zoneB2   zoneB2  zoneB2';
        --bug-zones-a234: 'ownstack stack  zoneTools1'
                          'ownstack zoneA2 zoneA2'
                          'ownstack zoneA3 zoneA3'
                          'ownstack zoneA4 zoneA4'
                          'zoneB1   zoneB1  zoneB1'
                          'zoneB2   zoneB2  zoneB2';
        /* THE EMPTIED SLOT MOVES UP, above the zone A rows rather than below them, and that is
           not cosmetic: a named area must be a single filled RECTANGLE, so leaving `zoneTools4`
           at the bottom put `stack` in rows 1-2 and again in row 4 with a spanning row between.
           Non-rectangular, so the whole declaration was invalid and dropped — see the round
           page's note on the oscillation that followed. */
        --bug-zones-a3: 'ownstack stack  zoneTools1'
                        'ownstack stack  zoneTools2'
                        'ownstack stack  zoneTools4'
                        'ownstack zoneA3 zoneA3'
                        'zoneB1   zoneB1  zoneB1'
                        'zoneB2   zoneB2  zoneB2';
        --bug-zones-a23: 'ownstack stack  zoneTools1'
                         'ownstack stack  zoneTools4'
                         'ownstack zoneA2 zoneA2'
                         'ownstack zoneA3 zoneA3'
                         'zoneB1   zoneB1  zoneB1'
                         'zoneB2   zoneB2  zoneB2';
    }

    .analysis-app.bug {
        display: grid;
        /* TWO COLUMNS, `'ownstack rightcol'` — the round page's landscape template, with the
           partner's board and the tools merged into the second one exactly as they are
           there. This was three flat columns for a while, which put the same three things
           in the same places but made this page a different SHAPE from the round page for
           no reason anyone could state; every rule about the pair then had to be written
           twice, once against two app columns and once against three.

           Each track holds a stack, and a stack here is board PLUS its gauge — the `* 0.31`
           the round page's identical track does not carry, and one of this page's three
           real differences from it. */
        /* Three tracks: each board sized by its own stack, then the tools taking what is left
           up to this mode's cap. The stacks size themselves from `--bug-stack-sq`, so no board
           width is written here — the track only has to ask the stack how wide it already is.

           `min-content`, NOT `auto`, and the difference is the whole rule that the boards take
           priority over the tools. Both stacks carry `min-width: 0` so that a shrinking board
           can never widen the page; that also makes an `auto` track's floor ZERO rather than the
           item's min-content, because `auto` minimums use the item's automatic minimum size and
           `min-width: 0` is exactly what overrides it. So the two board tracks became fully
           shrinkable while the tools track's `20vw` maximum was honoured first: measured at
           996x730, tracks came back 455.9 | 282.6 | 199.2 with the own stack's own tracks summing
           to 554 — board A overflowed its column by 98px, `overflow: visible`, and painted over
           board B. An explicit `min-content` track function is not the automatic minimum size and
           `min-width` does not reach it, so the board keeps its width and the tools take what is
           left — 554 | 282.6 | 101, the round page's bargain, reached without restating the
           board's arithmetic the way that page has to. */
        grid-template-columns: min-content min-content var(--bug-tools-track);
        /* THE BOARD ROW IS BOUNDED, as the round page's is. `min-content` let the row take whatever
           the merged column's content wanted — invisible while the tools sat in one fixed area
           that could not grow, and the second thing to fail once they are placed individually.
           `1fr` gives the column a definite height to divide, which is what lets a panel scroll
           inside it instead of pushing the row past the viewport. `uleft` keeps its own auto row. */
        grid-template-rows: minmax(0, 1fr) auto;
        /* uleft gets a row of its own. Without one it is auto-placed and opens an IMPLICIT
           COLUMN — measured as a seven-column grid where five were declared, which is what
           pushed the tools off their track. There is no `uboard` row any more: the chart and
           FEN & PGN are tabs of the tools panel now, so nothing is mounted under the boards. */
        grid-template-areas: 'ownstack rightcol'
                             'uleft uleft';
        column-gap: 2vmin;
        grid-row-gap: 0;
        justify-content: center;
        align-content: start;
    }
    /* The merged column's own two tracks, which is nearly all this page states about it:
       `grid-area: rightcol`, `display: grid`, the zero minimums and the clipping all come
       from the shared `.bug-right-column` rule the round page already carries, and the
       round page's own landscape block states exactly these two tracks and nothing more.

       The AREAS are re-stated because they are the second real difference: the round page's
       parts are a chat panel, two preset rows and a tab bar, placed individually so that
       `toolsPlacement.ts` can drop them under a shrunken board; this page has one tools
       block holding its own tab list, so its column is a single row of two areas. */
    /* Only the two track widths, which is all the round page's landscape block states too. The
       areas, the rows, the zero minimums and the clipping come from the shared
       `.bug-right-column` rule. The gauge's share of the stack track is the one real difference. */
    /* ONE GRID FOR THE WHOLE PAGE, which is what lets a part move between the boards and the
       space under them.
       ------------------------------------------------------------------------------------
       `.bug-right-column` is DISSOLVED here, so the partner stack, the tab panels and the tab
       list stop being its children and become grid items of the app alongside the own stack —
       `.bug-parts` already does the same for its own children. Every arrangeable thing is then a
       sibling in one grid, and an area name is all it takes to move one.

       A "drop zone" is therefore an AREA, not an element. Nothing is reparented and no JS moves
       DOM: `toolsPlacement` toggles a class and the template below decides where a part lands.

       THE OWN BOARD IS A COLUMN NOW, which it never was: it used to sit outside the merged
       column entirely. That is what makes a row spanning under BOTH boards expressible, and it is
       why the placement test will have to consider the own board's height too. */
    .analysis-app.bug .bug-right-column {
        display: contents;
    }
    /* THE AREAS ARE NAMED FOR WHERE THEY ARE, NOT FOR WHAT SITS IN THEM.
       ------------------------------------------------------------------------------------
       A part's home is not a property of the part: the tab list is in the tools column now and
       under both boards a moment later, and tomorrow it may be the preset rows instead. Naming a
       cell `tablist` is only true until something else lands in it.

       The regions and their names are the SHARED ONES, declared as `--bug-zones-*` at the top
       of this block — see the note there. This page has two parts where the round page has
       four, so it leaves rows 2 and 3 of the strip empty and an empty row collapses; what it
       does not do any more is invent a shorter vocabulary of its own.

       The boards keep `ownstack` and `stack`: they are not zones, they are the two fixed items
       everything else arranges around, and those names are shared with the round page.

       WHY A SLOT PER ROW rather than one `zoneTools` holding every part. Two items assigned the
       same named area do not stack, they OVERLAP — measured, identical rectangles. A named area
       is one rectangle and holds one item. */
    .analysis-app.bug {
        grid-template-areas: var(--bug-zones-strip);
        /* The first row absorbs the slack; the rest are their content's height, which is zero
           for the rows this page has no part for. Two `1fr` rows split the height evenly and
           drew a five-button bar 276px tall. */
        grid-template-rows: minmax(0, 1fr) auto auto auto auto auto;
    }
    /* Higher specificity than the unscoped `.bug-parts > [role=...]` assignments further down the
       file, so these win wherever they sit relative to them.

       THE SAME SLOT THE ROUND PAGE GIVES THE SAME PART. Its tab panels take `zoneTools1`, the row
       that absorbs the slack, because only the selected one is displayed and whichever is showing
       should have the height. Its tab strip takes `zoneTools4`, the bottom of the strip, and drops
       from there to `zoneA4` and then to `zoneB2`. This page's tab list IS that strip — same part,
       same three rules — so the two pages now differ in which slots are OCCUPIED and in nothing
       else. */
    .analysis-app.bug .bug-parts > [role='tabpanel'] {
        grid-area: zoneTools1;
    }
    .analysis-app.bug .bug-parts > [role='tablist'] {
        grid-area: zoneTools4;
    }
    /* DROPPED, in the round page's two stages. `drop-tablist` is decided by zone A's test in
       toolsPlacement.ts and `drop-tablist-b` by zone B's, so each class must land its part in the
       zone whose question was asked. This page spelled only the first and routed it to a zone-B
       row, which is the disagreement the shared vocabulary above exists to prevent: measured at
       996x730 with board A at full zoom, zone B correctly refused a 31.5px tab list into the
       3.3px left under a 666.7px stack, zone A had the 266.9px board B frees and dropped it
       anyway, and the list was drawn across board A's pocket with `--bug-boards-h` still
       claiming the whole budget.

       Zone A first: there is room under the SHORTER board long before there is room under both. */
    .analysis-app.bug.drop-tablist {
        grid-template-areas: var(--bug-zones-a4);
    }
    .analysis-app.bug.drop-tablist .bug-parts > [role='tablist'] {
        grid-area: zoneA4;
    }
    /* Zone B keeps the base template: the part leaves row 4 for row 6, and the row it vacates
       collapses on its own. The round page's `drop-tablist-b` states no template either, for
       exactly this reason. */
    .analysis-app.bug.drop-tablist-b .bug-parts > [role='tablist'] {
        grid-area: zoneB2;
    }
    /* THE TWO-COLUMN HOMES on this page. Two parts, so zone B's second row and zone A's lower
       slots stay empty and collapse.

       THE BOARD TRACKS STOP BEING `min-content` HERE, and must. This page sizes them from the
       stacks themselves so that the gauge is counted without the arithmetic being restated — which
       is right while a board is the only thing in its column. In these homes the tools share a
       board's column, and a `min-content` track takes the widest of everything in it: measured at
       682x647 in zone A, the tab strip's 240px min-content widened the partner's 221.6px column
       and pushed the app 8px past the viewport. `min-width: 0` does not reach a `min-content`
       track — the same trap the boards themselves hit — so the track has to be stated as a length.
       8.31 is the stack's own width in squares: eight files and the gauge's 0.31. */
    .analysis-app.bug:is(.tools-below, .tools-zonea, .tools-lastresort) {
        grid-template-columns: calc(var(--bug-tall-sq-a) * 8.31) calc(var(--bug-tall-sq-b) * 8.31) 0;
    }

    .analysis-app.bug.tools-below {
        grid-template-areas: var(--bug-zones-below);
        /* THE BOARD ROW IS ITS CONTENT, and the band takes what is left.
           `calc(var(--bug-tall-sq-a) * 10)` was tried and is wrong: a stack is ten rows of board
           and pocket PLUS whatever its seat strips take, and `own-name-outside` gives the username
           a line of its own. Measured at 627x835 with a 40px square: the row came to 400 and the
           stack to 457, so the stack was drawn 57px over the band and the chat was behind the
           clock. The ten-row figure is what the boards are SIZED from, not what they occupy.

           The band is therefore the slack — `TOOLS_MIN_ROWS` squares at full zoom, by construction,
           and more once a board is zoomed down, since the boards give back what they stop using. */
        grid-template-rows: min-content min-content minmax(0, 1fr) min-content min-content min-content;
    }
    .analysis-app.bug.tools-below .bug-parts > [role='tabpanel'] {
        grid-area: zoneB1;
    }
    .analysis-app.bug.tools-below .bug-parts > [role='tablist'] {
        grid-area: zoneB2;
    }
    .analysis-app.bug:is(.tools-zonea, .tools-lastresort) {
        grid-template-areas: var(--bug-zones-zonea);
        /* THE PANEL'S ROW TAKES THE SLACK, as `zoneTools1` does beside the boards. With every part
           row `auto`, none of them absorbs it and the own stack — which spans all five rows —
           distributes the leftover among them, EMPTY ONES INCLUDED: measured on the analysis page,
           rows 2-5 came to 253px while the panel and the tab strip used 144, the other 109 going
           to two rows with nothing in them. */
        /* THE PART ROWS SHARE ZONE A rather than each taking its content's height. `min-content`
           rows let every part draw at whatever size its width implied and the region simply
           overflowed — 220px of panels in 173px of zone A. Sharing, each row has a definite
           height, which is the other half of letting a button be limited by height. */
        grid-template-rows: min-content minmax(0, 1fr) minmax(0, auto) minmax(0, auto) min-content min-content min-content;
    }
    .analysis-app.bug.tools-zonea .bug-parts > [role='tabpanel'] {
        grid-area: zoneA2;
    }

    /* THE LAST RESORT: THE TOOLS TAKE THE BOARD'S COLUMN, AND THE BOARD TAKES ITS TURN IN IT.
       ------------------------------------------------------------------------------------
       Reached only where nothing else fits — no column beside the boards, no band below them, and
       a zone A too small to hold a part. All that is left is the right board's own column, so the
       tools go there and the partner's board becomes a TAB: the strip guarantees exactly one of
       them is displayed, which is what lets them share one area without a rule about who wins.
       NO NEW AREA IS DECLARED, only a new occupant for `stack`. The template and rows are zone
       A's, shared above, because the tab strip still needs a zone A to sit in — and `toolsHome()`
       will not choose this home unless there is one. */
    .analysis-app.bug.tools-lastresort .bug-parts > [role='tabpanel'],
    .analysis-app.bug.tools-lastresort .bug-parts .chatpresets-panel-1,
    .analysis-app.bug.tools-lastresort .bug-parts .chatpresets-panel-2,
    .analysis-app.bug.tools-lastresort .bug-parts > .bug-presets-group,
    .analysis-app.bug.tools-lastresort .bug-parts > .bug-gameover {
        grid-area: stack;
    }
    /* The strip itself is the one thing that must stay visible while any of them is, so it is the
       one thing that does NOT share the column. */
    .analysis-app.bug.tools-lastresort .bug-parts > .bug-round-tools-bar,
    .analysis-app.bug.tools-lastresort .bug-parts > [role='tablist'] {
        grid-area: zoneA2;
    }

    /* The tab strip flows on into zone B — full width under both boards, where it sits in every
       other arrangement — leaving the whole of zone A to the panel above it. */
    .analysis-app.bug.tools-zonea .bug-parts > [role='tablist'] {
        grid-area: zoneA3;
    }
    .analysis-app.bug.tools-zonea.strip-in-zoneb .bug-parts > [role='tablist'] {
        grid-area: zoneB1;
    }

    /* THE SPECTATOR LIST, HIDDEN HERE AS THE ROUND PAGE HIDES IT — and it is not cosmetic.
       ------------------------------------------------------------------------------------
       `<under-left id="spectators">` is a child of the app on this page, where the round page's
       is a sibling, and it asks for `grid-area: uleft` — an area this template has not named
       since the page was flattened. An item whose named area does not exist is AUTO-PLACED, and
       auto-placement past the last explicit column MINTS IMPLICIT ONES.

       The cost is not those tracks, which are 0px wide; it is their GAPS. Measured at 997x750:
       five columns where three were declared, so four `column-gap`s at 15px instead of two, and
       the extra 30px came off the only track that yields — leaving the tools 6.55px against the
       34.3px half-square they are promised. An empty, invisible element had eaten the whole
       guarantee before either board was asked to give anything up.

       `display: none` rather than a zone of its own, because that is the answer the round page
       already reached for the same element in this same mode — see its note, which also records
       that a real home in the tools column is the thing on this screen most worth revisiting.
       Two pages hiding it for one reason is the parity; two pages inventing separate homes for
       it would be the drift this block exists to end.

       Landscape only. Portrait names a `uleft` area and places it there, so the unscoped rule
       further down this file stays right for that mode. */
    .analysis-app.bug > under-left {
        display: none;
    }
}

/* PORTRAIT, BUILT LIKE THE ROUND PAGE'S.
   ------------------------------------------------------------------------------------
   What this replaces: three full-width rows — partner stack, then tools, then the player's
   own stack — which is the arrangement the round page abandoned and for the same reasons.
   The partner board is square and a fifth of the viewport, so a full-width row for it wastes
   everything to its right; the tools then took a whole row of their own between the two
   boards; and the player's own board, the one thing a reader is looking at, was pushed to
   1030px down an 835px viewport. Measured rows: 240 / 268 / 511.

   The round page's answer, reused: the partner board and the tools are ONE block that takes
   the region above, and inside it the tools occupy the space beside the small board rather
   than below it. The player's own stack is the last row and sits flush on the bottom edge.
   `.bug-right-column` is that block — the round page's element, its class, and its
   `grid-area: stack` / `align-self: start` rules for the stack inside it apply unchanged. */
@media (aspect-ratio <= 9/16) {
    /* PINNED TO THE VIEWPORT, exactly as the round page pins itself, and for the same
       reason: the app's `minmax(0, 1fr)` row can only distribute space that exists, so an
       app sized by its content has no slack to give and the own stack floats wherever the
       tools leave it. The cap has to be at every level between the body and the app, or the
       lowest bare `fr` wins — a bare `1fr` is `minmax(auto, 1fr)`, whose minimum is
       min-content. */
    body[data-variant='bughouse'][data-view='analysis'] {
        height: 100vh;
        overflow: hidden;
    }
    body[data-variant='bughouse'][data-view='analysis'] #main-wrap {
        height: 100%;
        grid-template-rows: minmax(0, 1fr);
    }
    .analysis-app.bug {
        display: grid;
        /* One column. "The player's own board is full width" is then structural rather than
           something each area has to spell out. */
        grid-template-columns: minmax(0, 1fr);
        /* The merged block takes the region above and gets the slack with it, so the own
           stack below stays flush with the bottom of the viewport whatever the two boards
           work out to. */
        grid-template-rows: minmax(0, 1fr) min-content min-content;
        grid-template-areas: 'rightcol'
                             'ownstack'
                             'uleft';
        /* The app is exactly the width the full-width board can draw, and the sub-square
           remainder is spent OUTSIDE it — on the app rather than on any box within it, so
           every child starts at the same x and the leftover falls in the page margins where
           nobody reads it as a gap. The round page's rule, verbatim. */
        width: calc(var(--bug-portrait-sq) * 8);
        margin-inline: auto;
        row-gap: 0;
    }
    /* A REAL BOX HERE, unlike landscape. Two tracks: the partner board's eight squares, then
       whatever is left — which is where the tools go. That leftover is the whole point; it is
       the space a square board a fifth of the viewport tall cannot use. */
    /* THE DROP CLASS NOW LIVES ON THE APP, so this mode needs the arrangement keyed to it.
       Landscape dissolves `.bug-right-column` and states its areas on the app; portrait keeps that
       column as a real grid, and the shared `.bug-right-column.drop-tablist` rule stopped matching
       the moment `toolsPlacement` began toggling the class on the app instead. Measured: the tab
       list back at 219px in its own column instead of 384px full width.

       The areas are the shared ones, restated against the app's class — `p1`/`p2` name nothing on
       this page, and rows with no items in them collapse to zero. */
    .analysis-app.bug.drop-tablist .bug-right-column {
        grid-template-areas:
            'stack chat'
            'stack p1'
            'stack p2'
            'tablist tablist';
    }
    /* NOTHING else for this page. Portrait's merged column is the round page's declaration for
       declaration, and this mode's own unscoped `.bug-right-column` block below already states
       the two track widths and the zero gap for both pages. The block that stood here restated
       all of it and renamed the areas, which is what stopped anything dropping. */
    /* Its content height, not the height of the row it spans. Stretched — the default for a
       grid item — it would measure the full column and the space beside a small board would
       never exist. The round page states this globally; restated because the analysis rules
       above set an area on the same element. */
    .analysis-app.bug .bug-partner-stack {
        align-self: start;
    }
    /* NO EVAL GAUGE IN PORTRAIT, which is what makes the width above exact. The stack is
       otherwise board PLUS gauge — 8.31 squares against an app of 8 — and the overflow has
       nowhere to go once the body stops scrolling. The gauge is also the part that survives
       being dropped: the partner's was 6.2px wide at this size, unreadable as a bar, and the
       engine's evaluation is a number in the Moves tab regardless. The round page has no
       gauge at all, so this is also what "follow the round page" means here. */
    .analysis-app.bug .bug-own-stack,
    .analysis-app.bug .bug-partner-stack {
        grid-template-columns: calc(var(--bug-stack-sq) * 8);
    }
    .analysis-app.bug #gauge,
    .analysis-app.bug #gaugePartner {
        display: none;
    }
    /* AND NO BOARD LETTER, for the same reason and by the same arithmetic. The letter lives in
       the gauge's column; portrait declares a ONE-column stack, so leaving it placed at
       `grid-column: 2` creates an implicit second track and the stack stops being exactly its
       eight squares — measured as an "A" at x=388 in a 386px viewport, i.e. off the screen,
       dragging the pinned layout wider than the mode allows.

       Portrait therefore has no answer to "which board is this" yet. It cannot reuse the
       landscape one: there is no gauge column to sit under, and the strip rows are full — the
       own stack's bottom strip is 5 squares of pocket plus a player bar that runs to the right
       edge, with nothing spare. Anything that fits has to overlap the board or the strip, which
       is a decision about portrait rather than a detail of this label. */
    .analysis-app.bug .bug-own-stack > .board-label,
    .analysis-app.bug .bug-partner-stack > .board-label {
        display: none;
    }
}

/* The areas, stated once for both orientations. THE BOARD TRACKS ABOVE ARE A PUBLISHED
   UNIT TIMES THE FILE COUNT, with nothing multiplied in afterwards — that rule is the
   whole reason the round page's boards match, and breaking it here would reintroduce the
   defect it exists to prevent: a unit is a whole number of device pixels per square, so
   multiplying it by a zoom fraction un-quantises it, chessgroundx floors the product
   again and keeps the difference. The zoom is already inside --bug-tall-sq-a/-b. */
/* Descendant, not child: both now sit inside `.bug-right-column`. In landscape that
   wrapper is `display: contents`, so they are still grid ITEMS of the app and take these
   areas — but `display: contents` changes layout, never the DOM, so a `>` combinator
   stopped matching and both boards lost their area. */
.analysis-app.bug .bug-own-stack { grid-area: ownstack; }
/* The partner stack is deliberately NOT given an area here. It takes `grid-area: stack` from the
   shared `.bug-partner-stack` rule — the same area the round page's takes — which is what lets the
   shared arrangements place a dropped part beside it and under it. Naming it `partnerstack` was
   what forced this page to restate the merged column's whole template, and that restatement is
   what made the space under a short partner board unreachable. */

/* A STACK IS A TWO-COLUMN GRID HERE: pocket / board / pocket down the first column, and
   the gauge parked in the SECOND column on the board's row.

   Scoped to the analysis page on purpose. The round page's stacks are block flow — "strip,
   board, strip, in block flow" — and that is load-bearing there; this only reshapes them
   where a gauge has to be placed.

   The gauge needs no height of its own. Sitting in the board's row it stretches to exactly
   the board, which is the whole reason for putting it inside: as a sibling of the stack it
   spanned all three rows and overhung the board by a pocket at each end — measured 550.17px
   against a 438.55px board.

   The 0.31 is measured, not chosen: the gauge was a flat `--gauge-gap: 17px` from site.css
   against a 54.8203px square on p1, and 16.99 / 54.8203 = 0.30994. Fixing it as a RATIO is
   what makes the gauge follow the board through zoom and viewport changes, where a constant
   px would drift further from the board's proportions at every step. */
.analysis-app.bug .bug-own-stack,
.analysis-app.bug .bug-partner-stack {
    display: grid;
    grid-auto-rows: auto;
    justify-content: start;
}
/* EACH STACK KNOWS ITS OWN SQUARE, and everything inside it is sized from that one
   variable: the board column, the gauge column and the pockets alike.

   Declared on the STACK, which is what makes it usable by the pockets below. The stacks are
   keyed by ROLE — yours, your partner's — while the pockets inside them are keyed by board
   IDENTITY: `.pocket-top` is board A's pocket whichever stack board A ended up in, and this
   page puts a board-B player's board B in the own stack. Inheriting the square from the
   container is what lets the two agree without either knowing the other's mapping. It is
   the move `.own-seat` / `.partner-seat` make on the round page, for the same reason. */
.analysis-app.bug .bug-own-stack { --bug-stack-sq: var(--bug-tall-sq-a); --bug-stack-allow: var(--bug-tall-allow-a); }
.analysis-app.bug .bug-partner-stack { --bug-stack-sq: var(--bug-tall-sq-b); --bug-stack-allow: var(--bug-tall-allow-b); }
@media (aspect-ratio <= 9/16) {
    .analysis-app.bug .bug-own-stack { --bug-stack-sq: var(--bug-portrait-sq); }
    .analysis-app.bug .bug-partner-stack { --bug-stack-sq: var(--bug-portrait-partner-sq); }
}
/* THE BOARD COLUMN MUST BE A DEFINITE WIDTH, not max-content. `.cg-wrap` takes its height
   from percentage padding against its own width, so with an indefinite column it has no
   width to resolve against and collapses — measured as a 0x0 board the first time these
   stacks became grids. The gauge column is the ratio; the gauge itself just fills it. */
.analysis-app.bug .bug-own-stack,
.analysis-app.bug .bug-partner-stack {
    grid-template-columns: calc(var(--bug-stack-sq) * 8) calc(var(--bug-stack-sq) * 0.31);
}
/* Pockets and board keep the first column; only the gauge is placed by hand.

   `grid-area: auto` FIRST, and it is the load-bearing half. Every one of these children
   still carries a `grid-area` naming a line of the OLD full-page grid — `.pocket-top`,
   `.pocket-bot`, and the boards. Inside a stack those names match no line, so each child
   landed in its own implicit track: measured as a stack whose rows were `0px 438.553px`
   with the pocket, the board and the gauge all piled into row 2 on top of each other.
   Clearing the shorthand lets ordinary auto-placement put pocket, board, pocket down the
   column in DOM order. */
.analysis-app.bug .bug-own-stack > *,
.analysis-app.bug .bug-partner-stack > * {
    grid-area: auto;
    grid-column: 1;
}
/* ID-SCOPED, and it has to be. `#mainboard { grid-area: board }` (site.css:453) and
   `#bugboard { grid-area: boardPartner }` place the boards by NAMED LINE for the old
   full-page grid, and an id selector outranks anything above. Left standing inside these
   stacks the name matches no line here, so each board landed in an implicit column of zero
   width — measured as a four-column stack, `438.553px 16.9907px 0px 0px`, with the board
   0x0 because `.cg-wrap` takes its height from percentage padding against a width it never
   got. */
.analysis-app.bug .bug-own-stack > #mainboard,
.analysis-app.bug .bug-own-stack > #bugboard,
.analysis-app.bug .bug-partner-stack > #mainboard,
.analysis-app.bug .bug-partner-stack > #bugboard {
    grid-area: auto;
    grid-column: 1;
    grid-row: 2;
}
.analysis-app.bug .bug-own-stack > #gauge,
.analysis-app.bug .bug-own-stack > #gaugePartner,
.analysis-app.bug .bug-partner-stack > #gauge,
.analysis-app.bug .bug-partner-stack > #gaugePartner {
    grid-column: 2;
    grid-row: 2;
    align-self: stretch;
    height: auto;
}
/* The gauge fills the column the stack gave it, so the ratio is written once per stack
   above rather than repeated here. `width` is overridden because analysis.css pins both
   gauges to a flat `--gauge-gap`. */
.analysis-app.bug .bug-own-stack > #gauge,
.analysis-app.bug .bug-own-stack > #gaugePartner,
.analysis-app.bug .bug-partner-stack > #gauge,
.analysis-app.bug .bug-partner-stack > #gaugePartner {
    width: 100%;
}

/* THE BOARD'S LETTER, under its gauge.
   Row 3 is the bottom strip's row, and the label takes the gauge's column of it — so it sits
   directly below the gauge, hard against the board's bottom edge, and inside the width the
   stack already reserved. It costs the layout NOTHING: the row and the column both exist
   already, and `justify-self` keeps the letter from stretching the 0.31-square track.

   Sized from `--bug-stack-sq` like everything else in the stack, so it follows the board
   through zoom and through the three modes instead of drifting at a fixed px. Dim by default
   because it is a permanent caption, not a reading: it should be findable when asked for and
   invisible the rest of the time. */
.analysis-app.bug .bug-own-stack > .board-label,
.analysis-app.bug .bug-partner-stack > .board-label {
    grid-column: 2;
    grid-row: 3;
    justify-self: center;
    align-self: start;
    padding-top: calc(var(--bug-stack-sq) * 0.06);
    font-size: calc(var(--bug-stack-sq) * 0.26);
    line-height: 1;
    font-weight: bold;
    color: var(--text-dim-color);
    user-select: none;
}

/* THE POCKETS, one board square per piece.
   ------------------------------------------------------------------------------------
   Nothing on this page sized them before, and the three modes failed in three different
   ways — which is why only one of them looked passable.

   DESKTOP LANDSCAPE was right by accident. The round page's `.pocket-top .twoboards .pocket`
   is not scoped to `.round-app`, so it reaches this page too, and the `--cg-width-a` it reads
   is written by chessgroundx and therefore defined here.

   SHORT LANDSCAPE hit that rule's other copy, whose `--bug-pocket-sq` is declared on
   `.round-app.bug` and so is missing here. The width was invalid at computed-value time and
   dropped, leaving `width: auto` — which inside a definite eight-square column is a pocket
   as wide as the whole board.

   PORTRAIT matched no rule at all and fell through to extensions.css, which sizes a pocket
   from `--cg-width` / `--cg-height`. The two-board page never writes those; it writes the
   `-a` / `-b` pair. So the HEIGHT was dropped too, and a pocket with no height is a pocket
   nobody can see.

   Sized from --bug-stack-sq, not from --cg-width-a/-b. Both would be legal — a pocket does
   not sit in the track it sizes itself from, so reading chessgroundx's measurement here is
   not the forbidden self-reference — but the board column above IS `--bug-stack-sq * 8`, so
   taking the pocket from the same variable makes a pocket piece exactly one board square BY
   CONSTRUCTION, rather than by two independently derived numbers happening to agree.

   A CELL IS A WHOLE SQUARE, not the round page's 0.8. That 0.8 is compaction and its reason
   is stated where it is declared: a round-page pocket shares its row with a name and a clock
   and has to leave them room. Here the pocket has the row to itself, so all three modes get
   the uncompacted geometry — which is the one desktop was already showing. */
.analysis-app.bug .bug-own-stack .twoboards .pocket,
.analysis-app.bug .bug-partner-stack .twoboards .pocket {
    width: calc(var(--pocketLength) * var(--bug-stack-sq));
    height: var(--bug-stack-sq);
    --fs: var(--bug-stack-sq);
    font-size: var(--fs);
}
/* The wrapper is `display: inline` in extensions.css, and an inline box does two things
   here: a width does not apply to it at all, and it generates a LINE BOX whose leading shows
   as a band between a board and the pocket against it. The round page blockifies it in two
   of its three modes for exactly this; stated once here for all three. Also more specific
   than the rule above, which matches this element too — `.cg-wrap` carries `.pocket` as well
   as the inner element does. */
.analysis-app.bug .bug-own-stack .twoboards .cg-wrap.pocket,
.analysis-app.bug .bug-partner-stack .twoboards .cg-wrap.pocket {
    display: block;
    width: max-content;
}
/* THE SEAT STRIPS: a pocket beside its player bar, one at each end of each board.
   ------------------------------------------------------------------------------------
   The round page's elements, class for class — `.seat-strip0` / `.seat-strip1` for which
   end of the board, `.own-seat` / `.partner-seat` for whose board — so the strip skeleton
   above and the whole of the name's sizing machinery apply here with no second copy. The
   analysis strip simply has one fewer occupant: there is no clock in it, because this
   page's clocks are absolutely positioned overlays on the board itself.

   Every parameter comes from --bug-stack-sq, so a strip is one square of the board it
   belongs to and the name's floor is the same fraction of a square the round page uses.
   --bug-name-fs is only a FLOOR: the size that actually renders is `10cqi` capped at
   --bug-name-fs-max, read from the room the name has rather than from the square. */
.analysis-app.bug .bug-own-stack,
.analysis-app.bug .bug-partner-stack {
    --bug-strip-h: var(--bug-stack-sq);
    --bug-name-fs: calc(var(--bug-stack-sq) * 0.218);
}
/* The name absorbs the width the pocket leaves. The shared rule says `flex: 0 0 auto`,
   which is right where the name is an item of a COLUMN — the round page's seat block —
   and wrong here, where it is an item of the strip's own row and would take only its
   text's width. */
.analysis-app.bug .seat-strip0 > round-player0,
.analysis-app.bug .seat-strip1 > round-player1 {
    flex: 1 1 auto;
}
/* THE NAME GOES TO THE END OF THE STRIP THE BOARD IS NOT ON, which is the round page's
   rule — clock against the board, name on the outside — reaching the same arrangement
   from the other direction. There the clock is IN the strip and the order of a column
   settles it. Here the clock is an absolute overlay pinned to the board's edge, so it
   lands in the strip's band nearest the board and the name has to leave that end alone.

   It was `align-self: stretch` from the shared rule, which put the name at the TOP of
   both strips. Harmless above a board, where the clock hangs at the strip's lower edge —
   and a collision below one, where the clock is at the strip's upper edge: measured with
   the bottom name's text box running to x=553.7 under a clock starting at x=536.5, in the
   same 553-571 band. The top strip keeps the top; only the bottom one moves. */
.analysis-app.bug .seat-strip0 > round-player0 {
    align-self: flex-start;
}
.analysis-app.bug .seat-strip1 > round-player1 {
    align-self: flex-end;
}

/* WHETHER THE NAME GETS A LINE OF ITS OWN, asked in CSS rather than measured.
   ------------------------------------------------------------------------------------
   Same question the round page's seatNamePlacement.ts answers: a name squeezed beside
   the pocket gets whatever width the pocket leaves, and a line of its own is the full
   width of the strip — but the line costs height, and at full zoom the stack is already
   exactly the height it is given.

   Here it needs no measuring and no observer, which is this page's standing rule. Both
   sides of the comparison are lengths CSS already holds: the room is what the app's
   height has left over the stack's ten squares, and the cost is bounded by the name's
   own cap. So it is arithmetic — the same `min(1, round(down, ...))` the coordinate gap
   uses to express a floor without a branch — and it cannot feed back into itself, because
   neither --bug-app-h nor --bug-stack-sq moves when a name takes a line.

   The room is what the COORDINATES have not already claimed. Both are drawn from the same
   spare height, and letting each take what the other leaves would be two claims on one
   budget — the shape that had the round page's version oscillating at 12Hz. The gap is
   settled first and this takes the remainder, one way only.

   The cost is the CAP, --bug-name-fs-max, not the rendered size: the rendered size is read
   from a container query and asking for it here would be a length feeding the query it is
   derived from. Over-estimating is the safe direction — too little room costs one seat a
   line it might just have fitted, too much overflows the stack. */
@media (aspect-ratio > 9/16) {
    .analysis-app.bug .bug-own-stack,
    .analysis-app.bug .bug-partner-stack {
        /* The stack's own spare height, per column, keyed exactly as its square is.
           The round page declares this too, but from --bug-tall-sq-a/-b in one mode and
           --bug-sq in the other; --bug-stack-sq is both of those for whichever stack this
           is, so one rule is right in both landscape modes. Portrait declares nothing and
           keeps the 0px default: its stacks are sized from WIDTH, so height left over is
           not a thing they have. */
        --bug-coord-room: calc(10 * var(--bug-stack-allow) - 10 * var(--bug-stack-sq));
    }
}
.analysis-app.bug .bug-own-stack,
.analysis-app.bug .bug-partner-stack {
    /* 1.6 of the font is the line box a name draws in — it carries a presence dot and a
       rating beside the text, so it is well above the type's own line-height. */
    --bug-name-line: calc(var(--bug-name-fs-max) * 1.6);
    --bug-name-room: max(0px, calc(var(--bug-coord-room) - var(--bug-coord-gap)));
    /* Two strips per stack, so the stack pays for the line twice or not at all. */
    --bug-name-outside: min(1, round(down, var(--bug-name-room) / (2 * var(--bug-name-line)), 1));
}
/* PORTRAIT KEEPS EVERY NAME INSIDE ITS STRIP, which is what the round page does and now
   what this page does too — the general arithmetic above already answers 0 here, because
   portrait declares no --bug-coord-room: its stacks are sized from WIDTH, so leftover
   height is not a quantity they have.

   It is not merely that there is no room to spare. Portrait is now PINNED to the viewport
   like the round page, and the own stack is exactly its ten squares — a strip, the board,
   a strip. A name that left its strip would add a line at each end and push the stack past
   the height it was given, which is the one thing a pinned layout cannot absorb. The round
   page reaches the same conclusion by measuring: `spaceFor()` credits its own stack with
   its own height and nothing more, so it can never take a line.

   This overrode it to 1 for a while, on the reasoning that the page scrolled and a line
   therefore cost nothing. That reasoning expired when portrait stopped scrolling. */
/* NO BOX OF ITS OWN: the shared `.bug-parts { display: contents }` applies, so the panels and the
   tab list are grid items of `.bug-right-column` and can be placed individually — which is the
   whole mechanism. This was a flex column occupying a single `tools` area, and a single area is
   exactly what nothing can drop out of. */
.analysis-app.bug .bug-parts { min-width: 0; }
/* THE STRIP CLIPS, AND EACH PART NOW HAS TO SAY SO ITSELF.
   ------------------------------------------------------------------------------------
   `.bug-right-column` carried `overflow: hidden` and `min-width: 0` for exactly this, and the
   flat grid dissolves that element — `display: contents` has no box, so its overflow applies to
   nothing and its former children are grid items with none of their own.

   Nothing noticed while the app's board tracks were `auto`, because the tools took their width
   from the BOARDS and their content always fitted. Once the boards hold their width — which is
   the whole of "boards take priority over the tools" — the tools column is whatever is left, and
   at 996x523 that is 110.8px against a five-tab bar: measured, the last tab reached x=1027 and
   the ENGINE's multipv value x=1021, so the page itself scrolled 31px sideways. The column
   yielding its width is the bargain; the page widening is not, and it is the one thing the
   `minmax(0, 20vw)` cap was chosen to prevent.

   The cost is honest and worth stating: a tab clipped at this width cannot be clicked. The way
   back is the way this page already offers — zoom a board down and the whole bar drops to zoneA4
   or zoneB2, where it has the width of both boards and every tab fits. */
.analysis-app.bug .bug-parts > [role='tabpanel'],
.analysis-app.bug .bug-parts > [role='tablist'] {
    min-width: 0;
    overflow: hidden;
}
/* AND THE TABS THEMSELVES SHRINK RATHER THAN THE BAR LOSING ITS ENDS.
   `site.css` makes a tab `flex: 1 1 0`, which shares the bar equally whenever the bar is wide
   enough — but a flex item's automatic minimum size is its MIN-CONTENT, so each tab still holds
   its whole label and the row overflows once five of them do not fit. The bar is
   `justify-content: center`, so that overflow leaves at BOTH ends: at 996x523 the clip above cut
   "Moves" in half at the left as well as "FEN & PGN" at the right, and a tab you cannot see the
   start of reads as a rendering fault rather than as a narrow column.

   `min-width: 0` lets each tab shrink instead, so all five stay present and clickable with their
   labels truncated — the same trade the round page makes when `labelControls()` drops its button
   labels rather than dropping the buttons. */
.analysis-app.bug .bug-parts > [role='tablist'] > [role='tab'] {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* THE PANELS ARE COLUMNS. The tab widget sets `display: flex` on a panel and nothing
   else, so without this they lay their children out in a ROW: measured with #ceval taking
   301px of a 284px panel and the movelist squeezed to a width of ZERO, which is why the
   Moves tab looked empty apart from the engine header. */
.analysis-app.bug .bug-parts > [role='tabpanel'] {
    flex-flow: column;
    min-width: 0;
    /* THE PANEL TAKES THE HEIGHT ITS ROW GIVES IT, and this has to be said on THIS page only.
       `site.css` gives every `div[role=tabpanel]` `height: var(--panel-height)`; the round page
       never loads `analysis.css`, so that variable is undefined there, the declaration is invalid
       at computed-value time, and height falls back to `auto` — which is why that page has never
       needed this and why its own comment warns against "fixing" the site.css rule. This page
       defines `--panel-height: 240px`, so the height is real.

       It stayed hidden while `.bug-parts` was a flex column, because `flex: 1 1 auto` grew the
       panel past its own height. A grid item has no flex to do that: the third thing to fail. */
    height: auto;
}
/* The movelist fills what the engine panel and the controls leave, the same relationship
   `main.round.bug .movelist-block` has inside the round page's Moves panel.

   THE HEIGHT PAIR IS THE LOAD-BEARING HALF, not the flex. analysis.css pins this element to
   `--movelist-max-height` (8rem) under `max-width: 799px and (orientation: portrait)` — a
   single-board rule that reaches this page too. Releasing `flex` alone left both height
   declarations standing, so the block grew to nothing: measured 97.2px of movelist in a
   313.6px panel with 73px of it simply empty below the move controls. The round page never
   hits this because its movelist is not inside `.movelist-block` under that query. */
.analysis-app.bug .movelist-block {
    flex: 1;
    min-height: 0;
    height: auto;
    max-height: none;
}
/* EACH BOARD'S NUMBERS AS A COLUMN: evaluation on top, the depth it was reached at under it.
   The two columns flank the engine's name and share the row's spare width equally, so the pair
   reads left-to-right in the same order as the boards and the PV columns below.

   `score` needs its flex reset. analysis.css pins it to `flex: 0 0 75px`, which was a WIDTH while
   the score was a direct child of the row; inside a column that basis becomes a HEIGHT, and a
   22px number was being given a 75px tall box. */
.analysis-app.bug .engine-side {
    /* `auto`, not a 0 basis: each column asks for the width its longest reading needs, and the
       engine's name gives way. With a 0 basis the name kept its content width and the columns
       split the remainder — 36px each, which is narrower than the word "knodes/s". */
    flex: 1 1 auto;
    min-width: 0;
    display: flex;
    flex-flow: column;
    align-items: center;
    justify-content: center;
}
.analysis-app.bug .engine-side score {
    flex: 0 0 auto;
    padding-left: 0;
    align-self: center;
}
/* Dim and small on purpose: the depth qualifies the score above it rather than competing with
   it, and under the alternating ladder one of the two is always the older reading. */
.analysis-app.bug .engine-side info {
    font-size: 0.78em;
    line-height: 1.25;
    text-align: center;
    color: var(--text-dim-color);
}
/* Depth on one line, speed on the next, each unbroken. They are two readings, not a sentence:
   left to wrap, a single phrase broke wherever it ran out — measured as "Depth", "17/18," and
   "1362 knodes/s" across three ragged lines, and the panel 97px tall. Nowrap is what makes each
   column ASK for a sensible width above, and it brought the panel to 70px. */
.analysis-app.bug .engine-side info > span {
    display: block;
    white-space: nowrap;
}
/* THE NUMBERS COME FIRST, the engine's name second. `div.info, div.infoBug` in analysis.css is
   `flex: 1 1 auto`, so the name grew to its content and the two number columns — basis 0 — were
   left with what remained: measured at 159px of a 284px panel for the name against 36px for each
   side, which is what forced the depth to wrap three ways. */
.analysis-app.bug .engine .infoBug {
    flex: 0 1 auto;
    min-width: 0;
    overflow: hidden;
    /* Slightly smaller than the readings it sits between. It is a label — which engine, running
       where — and it was taking 159px of a 284px panel while the numbers took 36px each. */
    font-size: 0.85em;
    line-height: 1.3;
}

/* TWO COLUMNS OF PRINCIPAL VARIATIONS, ONE PER BOARD.
   Left column is the own stack's board, right is the partner's — the order `pvPanel()` builds
   them in — so each column stands under the board it evaluates and neither needs a heading to
   say which it is. Position is the label.

   `1fr 1fr` rather than auto columns: the two lists must stay the same width whatever they hold,
   or the column that happens to be evaluating a quiet position would steal room from the other
   every slice, and the whole panel would jitter at the ladder's pace. `min-width: 0` is what
   lets a long variation be clipped by its own column instead of widening the grid. */
.analysis-app.bug .pvcolumns {
    display: grid;
    grid-template-columns: 1fr 1fr;
    column-gap: 1px;
    background-color: var(--border-color);
}
.analysis-app.bug .pvcol {
    min-width: 0;
    background-color: var(--bg-color0);
}
/* A DROP OF A PIECE NOBODY HOLDS YET. The engine searches these because in bughouse the piece
   arrives from the partner's board, so the line is real but conditional: it says "if my partner
   sends me a queen". Only the PIECE is marked — the square it would go to is not in doubt. */
.analysis-app.bug .pv-borrowed {
    color: var(--bad);
    font-weight: bold;
}

/* Each line clips rather than wraps. A wrapped variation makes its row taller than the row
   beside it, and the two columns stop being readable as a pair — the fifth line of one lands
   against the fourth of the other. Clipped, every row is one line tall in both columns. */
.analysis-app.bug .pvcol div.pv {
    display: block;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* THE ONE ENGINE SETTING, UNDER THE LINES IT CONTROLS.
   `div.labelled` (site.css) already makes this a space-between row; what it does not do is
   survive a 284px tools column, where the two-word label wrapped and the row stood 54px tall —
   as much as three PV lines, to control how many PV lines there are.

   So the label is allowed to shrink and the track is what gives way: `flex` on the input lets it
   take whatever the label and the readout leave, and the readout is pinned by its content so the
   `N / 5` never wraps mid-value. Nothing here needs a minimum track width — a slider two
   centimetres wide with five stops is still a slider. */
.analysis-app.bug .pvbox .multipv-setting {
    gap: 1ch;
    padding: 0.4em 0.5em;
    border-bottom: 1px solid var(--border-color);
}
.analysis-app.bug .pvbox .multipv-setting label {
    white-space: nowrap;
    flex: 0 0 auto;
    /* The site's `label` carries 8px of vertical padding, which is what a settings drawer wants
       and this row does not: it made a 19px label 35px tall and the row 47px. */
    padding: 0;
}
.analysis-app.bug .pvbox .multipv-setting input[type='range'] {
    flex: 1 1 auto;
    min-width: 0;
    margin-right: 0;
}
.analysis-app.bug .pvbox .multipv-setting .multipv_range_value {
    flex: 0 0 auto;
    white-space: nowrap;
}
/* THE CHART FOLLOWS THE SHAPE OF ITS PANEL.
   ------------------------------------------------------------------------------------
   `container-type: size` is doing two jobs. It is what lets the aspect-ratio query below
   ask about this box at all — and it is what makes the whole arrangement incapable of
   looping. A size container's dimensions come from OUTSIDE it, so nothing the chart draws
   can change the box, and the box is the only input to the answer. There is no feedback
   path to terminate, which is why nothing here needs a guard.

   The chart fills the panel rather than keeping analysis.css's flat `height: 240px`. That
   height was written for a chart in a full-width row under the board; here the panel is a
   tab of the tools column, and a chart that only ever used 240px of a 312px panel is the
   reason the vertical form has room to be worth switching to. */
.analysis-app.bug .chart-container {
    container-type: size;
    container-name: movechart;
}
.analysis-app.bug #chart-movetime {
    width: 100%;
    height: 100%;
}
/* Taller than wide gets the vertical form. The threshold is 1 rather than something tuned:
   the question is literally which axis has more room for the plies to run along. */
@container movechart (aspect-ratio < 1) {
    .analysis-app.bug #chart-movetime {
        --bug-chart-inverted: 1;
    }
}
@container movechart (aspect-ratio >= 1) {
    .analysis-app.bug #chart-movetime {
        --bug-chart-inverted: 0;
    }
}
/* site.css pins #movelist to `--movelist-max-height` (8rem), which is a scrap of a panel
   this tall. Released here exactly as the round page releases it. */
.analysis-app.bug #movelist {
    height: 100%;
    max-height: unset;
}
.analysis-app.bug > under-left { grid-area: uleft; min-width: 0; }

/* NOTHING MIRRORS THE CLOCKS ANY MORE. `.anal-clock.bug { left: 0; right: initial }` used
   to pull board B's clocks to its LEFT edge, which was right for the old layout: the two
   boards were pinned to opposite edges of the page with everything else between them, so
   each board's clocks faced inward, toward the reader.

   Adjacent boards have no inward. The clocks keep analysis.css's `right: 0` in both stacks,
   so they sit under the same corner of every board and the eye finds them in one place.

   The class this hung on is board IDENTITY — `.bug` is board B whoever played on it — so it
   also put the clocks on the wrong side for a board-B player, whose board B is the LEFT
   stack here. Deleting it fixes that case as well, and it is the same identity-versus-role
   trap the pockets and the boards each hit in their turn. */

#gaugePartner {
    grid-area: gaugePartner;
}

/* FEN & PGN tab panel: mirrors analysis.css's #panel-4 rule for the single-board
   page, keyed by class instead of id since two-board's panel ids are generated */
.fenpgn-panel {
    font-size: 0.9em;
    flex-flow: column;
}

/* *****************************************************************************************
 * round.css pocket-top-partner pocket-bot-partner
 ******************************************************************************************* */

/* A seat strip is one seat's furniture as a single grid item: its pocket beside
   its clock and name. Every layout below places strips and nothing smaller, which
   is what lets flip and switch have exactly one kind of thing to move.

   The pocket and the seat block keep the grid-area declarations they carry from
   site.css; those are simply ignored now that both are flex children rather than
   grid items, so the single-board round page is unaffected by them. */
/* WHERE a thing is decides WHICH area it occupies — not what it is.
   `.bug` is board identity: `.seat-strip0.bug` is board B's top seat whether or not
   board B is the one the viewer is playing on. That was serviceable while both
   boards were siblings in one grid and switching could swap their inline
   grid-area. It is not any more: the viewer's own board and strips are children of
   the round app, and the partner's are grouped in `.bug-partner-stack`, so the
   container an element sits in IS its side, and an inline area naming the other
   side's track resolves against a grid that has no such name. A board-B player
   gets a switch on load, so that mistake was not hypothetical — it auto-placed
   both boards and blanked the page. Switching is now a DOM move between those two
   containers and writes no inline style at all. */
/* Neither stack places its own contents with areas any more: both are containers
   whose children stack in block flow, strip then board then strip, which is the whole
   of the arrangement they need. The areas that used to do this for the viewer's own
   board — clock-top, board, clock-bot — are gone with the rows that carried them. */
.bug-own-stack, .bug-partner-stack {
    min-width: 0;
}
.seat-strip0, .seat-strip1 {
  display: flex;
  flex-direction: row;
  /* Bottom-aligned, which is also what keeps the name clear of the file labels
     overhanging below a board. A mode that turns the strip on its side restores
     centring, because on a column strip this axis is the horizontal one. */
  align-items: flex-end;
  min-width: 0;
}
.seat-strip0 > .pocket-top, .seat-strip0 > .pocket-top-partner,
.seat-strip1 > .pocket-bot, .seat-strip1 > .pocket-bot-partner {
  flex: 0 0 auto;
}
/* the name is the only thing that absorbs a change in the strip's width */
.seat-strip0 > .info-wrap0, .seat-strip1 > .info-wrap1 {
  flex: 1 1 auto;
  min-width: 0;
}

/* --- what a strip looks like inside, stated once for every mode -------------
 *
 * A mode chooses only three things about a strip: where it sits (the grid-area
 * above), how big it is (--bug-strip-h, and the two font parameters), and which
 * axis it runs on (flex-direction, which portrait overrides for board B). The
 * order the three pieces read in — pocket, name, clock — and which of them
 * absorbs slack is the same everywhere, which is the point of decision 1b.
 *
 * Scoped to the two apps rather than to the elements themselves. It was
 * `.round-app.bug` alone while the analysis page had no player bars of its own and
 * had to be left as it was; it has them now, wants exactly this treatment, and the
 * NAME half of this block is written `:is(.round-app, .analysis-app).bug` so there
 * is one copy of it rather than two to keep in step. `:is()` takes the specificity
 * of its most specific argument, so those rules still weigh (0,2,0) — unchanged for
 * the round page, and still enough to out-specify the page-wide
 * `main.bug round-player0`. What stays round-only is what names a round-only
 * element: the clock, the seat block, the strip's own arrangement.
 * That scope also out-specifies the page-wide
 * `main.bug round-player0` below (0,2,1 against 0,1,2), which is why the fixed
 * 0.7vw and 48px there are overridden here rather than deleted — deleting them
 * would change the analysis page too.
 *
 * Defaults are the unsized case, so a mode that sets nothing keeps whatever it
 * had: --bug-strip-h falls back to auto, and an undefined font parameter makes
 * its declaration invalid at computed-value time, which leaves the size
 * inherited exactly as it was before a parameter existed.
 */
:is(.round-app, .analysis-app).bug .seat-strip0, :is(.round-app, .analysis-app).bug .seat-strip1 {
    height: var(--bug-strip-h, auto);
    overflow: hidden;
}
/* The seat block's own children are clock, name, misc-info, and they STACK: the
   clock takes the height above and the name one line beneath it, both across the
   whole width the pocket leaves.

   They used to share that width in a row, and the clock won it. `flex: 0 0 auto`
   takes its natural width first and the name absorbs the shortfall, which on a
   400px strip meant a 250px pocket, a 122.5px clock and 27.5px of name — an
   initial, not a name. Nothing is shared in a column, so both get all 150.

   `flex-direction` on this element is inert in the popped-out state, where it is
   `display: contents` and lays out nothing; the rules below have to hold in both
   states and are written for that. */
.round-app.bug .info-wrap0, .round-app.bug .info-wrap1 {
    display: flex;
    flex-direction: column;
    flex-wrap: nowrap;
    align-items: stretch;
    justify-content: flex-start;
    height: var(--bug-strip-h, auto);
    overflow: hidden;
    /* THE WIDTH THE POCKET LEAVES, TAKEN FROM THE STRIP RATHER THAN FROM THE CONTENTS.
       This box had no width rule at all: as `flex: 0 1 auto` it was sized by its
       children's intrinsic width, which used to work because the clock and the name
       reported real widths.

       They no longer do. `.clock-wrap` is now `container-type: size` and
       `.player-data` is `container-type: inline-size`, and a containment context
       contributes ZERO intrinsic width to its parent — so this box collapsed to 6.3px
       in a 437.3px strip, taking the clock down to a 1.375px font and the name to a
       zero-width `player`. Both children then read their `cqi` from a box that had
       already collapsed, which is the circularity containment exists to forbid, one
       level up from where it was declared.

       It surfaced only after a flip, because the strip's line had already been laid
       out with real widths and nothing forced a re-layout until the blocks moved.
       That is the trap: a collapse whose cause is a stylesheet and whose trigger is
       an unrelated interaction some time later.

       `flex: 1 1 0%` makes the width come from the strip — the parent — which is what
       inline-size containment requires of its container, and it is also what the
       capability asks for in words: the name takes the full width the pocket leaves. */
    flex: 1 1 0%;
    min-width: 0;
}
:is(.round-app, .analysis-app).bug round-player0, :is(.round-app, .analysis-app).bug round-player1 {
    /* One line at the end of the column, sized by its content rather than by what
       is left over — there is nothing to share with now that the clock is above
       it rather than beside it. */
    flex: 0 0 auto;
    min-width: 0;
    /* not the 48px of `main.bug round-player0`, which exceeds a one-square strip */
    height: auto;
    /* the full width of the column; `flex-end` here used to mean the bottom of a
       row, and in a column it would mean the right-hand edge */
    align-self: stretch;
    /* The size itself is set on `.player-data` below, which is where the container
       that measures the room lives. Never vw: this page is routinely used in a
       quarter-tiled window, where the 0.7vw it used to inherit renders under 7px. */
    line-height: 1.15;
    /* One line, truncated. A name that wrapped inside a one-square strip came out
       as two clipped fragments with the second cut mid-word by `break-all`, which
       reads as damage rather than as a long name. The full name stays in the DOM
       either way. The text sits in an inner block, so the truncation itself is on
       `.player-data` below — on this element it would govern nothing. */
    white-space: nowrap;
    word-break: normal;
    max-height: none;
    overflow: hidden;
    display: block;
    padding: 0 0 0 0.35em;
    background: none;
}
/* Where the name's text actually lives. `round-player0` is a wrapper; putting the
   truncation here is what makes it happen — the same reason the popped-out state
   sets `nowrap` on this element and not on its parent.

   A row, so that the name can end at the trailing edge under the clock's digits and
   the presence dot can follow it. The markup is
   `i-side` then `player > (a.user-link, rating)`, with the dot first; `order` moves
   it after the name without touching the DOM, which flip and switch both move. */
:is(.round-app, .analysis-app).bug .player-data {
    display: flex;
    align-items: center;
    justify-content: flex-end;
    gap: 0.25em;
    white-space: nowrap;
    overflow: hidden;
    min-width: 0;
    /* THE ROOM THE NAME HAS, AND THE THING ITS SIZE IS READ FROM.
       This box is the name's slot in both arrangements — the full strip when the name
       has a row of its own, and what the pocket leaves when it does not — so making it
       the query container means one rule covers both without asking which it is in.

       Its width comes from its parent (a block-level flex container fills
       `round-player0`), which is what `inline-size` containment requires. Putting the
       container on `round-player0` instead would NOT work: it is a flex item with
       `flex-basis: auto`, so containment would zero its content contribution and it
       would stop wrapping onto a line of its own. */
    container-type: inline-size;
    /* `.player-data` carries `2px 6px` from site.css, and both halves are wrong here.

       The 6px left the presence dot inside an edge the clock's digits sit flush
       against. The 2px top and bottom is the strip's ENTIRE fixed term: measured at
       exactly 4.00px on two seats whose squares differ by a factor of 2.3, and the
       whole of why the strip looked like a proportion plus a constant. Without it the
       contents are already proportional — 0.4987 of a square against 0.5008 — so the
       reserved name row is one line box at a known size and nothing else.

       site.css is not touched; the override is scoped to this page, the way the
       coordinate gutter and the body margin already are. */
    padding: 0 0 0 0.35em;
}
/* THE SIZE: grow up to the cap where there is room, below it where there is not.

   `cqi` is 1% of `.player-data`'s inline size, so this is the room the name actually
   has, in both arrangements, with no measuring and no observer — which is what this
   page's standing rule requires. 10cqi comes from the case that has to work: portrait's
   partner row is 165.3px wide and should reach the cap, and 16.8 / 1.653 = 10.16.

   The floor is what the old rule produced, which Nikolay accepts at the bottom end —
   but taken through `min()` first, because a floor above the cap would win the clamp
   outright and the ceiling would silently stop applying on a large enough board
   (`--bug-name-fs` passes 16.8px once a square passes 77px).

   Stated on both children, not on the container: a font-size on `.player-data` would be
   the basis for its own `cqi`, and a length that feeds the query it is derived from is
   the circularity this file has been bitten by twice. */
:is(.round-app, .analysis-app).bug .player-data > player,
:is(.round-app, .analysis-app).bug .player-data > i-side {
    font-size: clamp(
        min(var(--bug-name-fs, 0.85em), var(--bug-name-fs-max)),
        10cqi,
        var(--bug-name-fs-max)
    );
}
/* The name and its rating are what gives way when there is not enough room; the
   ellipsis belongs on this box because this is the one holding the text.

   The rating is inside `player`, so it inherits the size above and there is no second
   rule to keep in step when bughouse starts carrying one. */
:is(.round-app, .analysis-app).bug .player-data > player {
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}
/* The link states `white-space: break-spaces` itself, which beats anything inherited
   from the boxes around it — so the name went on wrapping to two lines inside a box
   that had been told not to. Measured: a 25.1px name in a strip that had 16.5px for
   it. `nowrap` has to be said here, on the element holding the text.

   `font-size: inherit` for the same class of reason, and it is what makes the cap
   real: site.css gives `.user-link` a `0.8em` of its own below 800px, which reapplied
   itself under the capped size and drew the name at 13.44px while the presence dot
   beside it — inheriting cleanly — sat at the full 16.8px. A multiplier the cap cannot
   see is the same defect as the `* 2` that was just removed, one floor down. */
:is(.round-app, .analysis-app).bug .player-data a.user-link,
:is(.round-app, .analysis-app).bug .player-data rating {
    white-space: nowrap;
    font-size: inherit;
}
/* After the name, not before it. */
:is(.round-app, .analysis-app).bug .player-data > i-side {
    order: 1;
    flex: 0 0 auto;
}
.round-app.bug .clock-wrap {
    /* The height above the name: all of it, and no more. `1 1 auto` was not enough
       — a flex item's automatic minimum is its content, so the clock kept its
       natural 39.2px and pushed the 16.5px name past the strip's 50px, where the
       strip's own `overflow: hidden` cut it in half. Basis zero and a zero minimum
       make the name the fixed part and the clock the part that gives way. */
    flex: 1 1 0%;
    min-height: 0;
    /* the full width of the column, with the digits still ending at its trailing
       edge — that is the block's own `row-reverse` below, not this */
    align-self: stretch;
    /* the digits sit in the middle of the room they now have rather than clinging
       to one edge of it */
    align-items: center;
    /* row-reverse on both boards, so the digits end at the strip's trailing edge
       and the difference indicator sits over the leading digit. Board B carried
       this globally already; board A had it in short landscape only. */
    flex-direction: row-reverse;
    /* main-start is the visual right edge under row-reverse, so this is what
       anchors the digits there. They no longer compete with the name for width —
       the name has its own line beneath — so this is only about where the digits
       sit within the clock's own box. */
    justify-content: flex-start;
    font-size: var(--bug-clock-fs);
    /* THE ROOM THE CLOCK HAS. Third in the priority order and last to be served: the
       pocket is never reduced, the name takes the width the pocket leaves when it is
       inside the strip, and what is left over is this box.

       `size` rather than `inline-size` because the clock is bounded on both axes — its
       digits are as tall as their font and about 3.3 times as wide — and a clock sized
       from width alone would be six times too tall for the strip. Both of this box's
       axes come from outside it: the width from `align-self: stretch`, the height from
       `flex: 1 1 0%` against a column whose height is the strip's. */
    container-type: size;
}
/* AS LARGE AS THE ROOM ALLOWS, WHICH IS WHAT THE CAPABILITY ALREADY ASKED FOR AND
   `calc(var(--bug-seat-sq) * 0.2)` never delivered. That expression sized the clock from
   the BOARD, so it was blind to its own box: 65.1 x 19.2 in a space 194 x 49 once the
   name had left the strip entirely, and 105 wide in a 218.7px slot with the name below
   it. `.clock-wrap` stretched; the digits did not.

   Two terms, whichever is smaller:
     92cqb — the height. `line-height: 1` below makes the digits' box exactly their font
             size, so the block axis converts one-to-one; 92 rather than 100 keeps the
             difference indicator's border off the strip's edge.
     22cqi — the width. Five digits run about 3.3 times the font size, but a clock under
             ten seconds shows tenths and runs about 4.4, so the divisor is the WIDE form.
             Sizing to the narrow one would make the clock jump at 0:09.9, and the
             capability forbids reserving that width by any other means. */
/* THE DIGITS ARE THE CLOCK. site.css gives `.clock-time.min` a `padding-left: 12px` and
   `.clock-time.sec` a `padding-right: 12px` — 24px that does not scale, inside a box whose
   every other dimension does. Two things went wrong because of it.

   It broke an alignment this page had explicitly asked for. `3c.7` of the seat-strip work set
   out to make the username end where the clock's digits end, and measured the mismatch as 0 —
   but it measured against the clock's BOX. The digits sit 12px inside that box, so the real
   mismatch was 12px the whole time: name row ending at 749.1, digits at 737.1.

   And it made the clock's width `24 + ratio x font` rather than a clean multiple, so no `cqi`
   coefficient could express the fit — the constant is 23% of a small partner clock's width and
   12% of a desktop one. Removing it is what lets the coefficient below be exact.

   Not for the difference indicator, which was the other candidate: that is anchored at the
   clock's leading edge and overlaps the first digit by design, 21.4px against a 12px pad.

   site.css is untouched; the single-board round page keeps its spacing. */
.round-app.bug .clock-time.min { padding-left: 0; }
.round-app.bug .clock-time.sec { padding-right: 0; }

/* AS LARGE AS THE ROOM ALLOWS, IN BOTH AXES, AND NEVER LARGER THAN THE WIDEST FORM WILL FIT.

   THE RATIO IS THE WHOLE OF THE WIDTH BOUND, so it is written as a division by it rather than as
   a coefficient. A coefficient states no claim and cannot be checked; `100cqi / 3.5` says exactly
   what it is — the box divided by how many font-sizes wide the longest thing in it gets.

   `--bug-clock-widest` is MEASURED, and measuring the wrong form is what this replaces. The
   comment here used to argue that the widest displayable form is `0:09.9` at 2.82, and that
   `59:59.9` at 3.40 was unreachable "since tenths appear only under ten seconds, so the minutes
   field is a single digit by then". The minutes field is never a single digit. `printTime()` in
   client/clock.ts pads it unconditionally — `mins = (minutes < 10 ? '0' : '') + minutes` — so the
   form under ten seconds is `00:09.9`, seven glyphs, and it measures the same 3.40 that was
   called impossible. The old `32cqi` was 1/3.1, about 9% too generous, and the last ten seconds
   of a desktop game drew 193.7px of digits into a box of 183.03.

   Measured on the live page at 1914x827, font 57.04px: `60:00` and `09:59` are 2.56, `00:09.9`
   and `59:59.9` are 3.40. `.clock-time.byo` is not in any of these: it carries the increment and
   site.css hides it with `.clock-time.byo:not(.byoyomi)`, and bughouse never sets the byoyomi
   class, so it cannot appear. A variant family that did show it would widen every form here.

   Each ratio is ABOVE its measurement BECAUSE THE TWO ERRORS ARE NOT SYMMETRIC. A ratio above the
   measurement costs a slightly smaller clock at the moment the width binds; a ratio below it puts
   digits outside the strip. Both carry about 3% of headroom for that reason.

   TWO RATIOS, BECAUSE THE PAGE ALREADY KNOWS WHICH FORM IS ON SCREEN. `.clock` is given a `hurry`
   class by client/clock.ts under `time < HURRY && this.byoyomiPeriod === 0`, and printTime() decides
   tenths under `millis < HURRY && this.byoyomiPeriod === 0` — the same constant, operands and guard,
   so the class is an exact CSS-side predicate for "the wide form is showing". Bounding for the
   widest form at ALL times was costing 4.75px of a 57.04px clock for the whole of every game to make
   room for its last ten seconds; the bound now follows the form actually displayed.

   This is not the text measuring itself. Nothing here reads a rendered width: there are two
   constants and a class that already existed. Measuring text to fit a font would be the loop this
   file exists to avoid — the font would set the width and the width would set the font.

   AND IT CANNOT OSCILLATE, which is what makes two states as safe as one. `.clock-wrap` is
   `container-type: size`, so its dimensions come from outside it and its contents cannot influence
   them. A font derived from `cqi`/`cqb` of that box can never change the box, in either state, so
   there is nothing to converge and nothing to guard.

   The size change lands where a change already happens: site.css gives `.clock.hurry` a background
   of MistyRose, or #502826 in the dark theme, so the ten-second boundary is already a deliberate
   visual event. With an increment the clock can rise back above ten seconds, the class clears and
   the size returns — the bound follows the form in both directions, which is the rule working
   rather than an edge case.

   BOTH TERMS STAY, and which one binds is an outcome rather than a decision. The height binds
   wherever the strip is shallow — portrait and short landscape, by a wide margin, in BOTH states, so
   those modes cannot see this at all. On the desktop the width binds under tenths at 52.29 and the
   height binds the rest of the time at 57.04. The capability's rule that height should bind where
   the strip has height to give was written against a width bound too pessimistic to be real; these
   are the true constraints, one per form. */
.round-app.bug .clock {
    /* 60:00 and 09:59, both measured at 2.56 */
    --bug-clock-widest: 2.65;
    font-size: min(92cqb, calc(100cqi / var(--bug-clock-widest)));
}
.round-app.bug .clock.hurry {
    /* 00:09.9 and 59:59.9, both measured at 3.40 — the form under ten seconds */
    --bug-clock-widest: 3.5;
}
/* A STRIP ABOVE ITS BOARD IS THE MIRROR OF ONE BELOW IT.
   The clock belongs against the board and the name on the outside, so a top strip
   reads name-then-clock and a bottom strip clock-then-name. The column above is the
   bottom strip's order; this reverses it for the top one.

   `.seat-strip0` IS the marker, and it needs no maintenance: it is what assigns
   `grid-area: clock-top` / `clockB-top`, so a strip's class is what puts it on its
   side of the board. The DOM moves do not touch it — a flip swaps the blocks INSIDE
   the strips, and a switch swaps top strips with top strips between the columns.
   Verified on the page in portrait and tall landscape: every `seat-strip0` above its
   board, every `seat-strip1` below.

   The block's third child, the misc slot, is empty and rides along at the far end. */
.round-app.bug .seat-strip0 .info-wrap0,
.round-app.bug .seat-strip0 .info-wrap1 {
    flex-direction: column-reverse;
}

/* The same inversion for a name that has left the strip: it leaves upwards from a top
   strip. The strip is a wrapping row in that state, so being first is what puts the
   name on the line above the pocket and the clock. */
.round-app.bug.own-name-outside .own-seat.seat-strip0 round-player0,
.round-app.bug.own-name-outside .own-seat.seat-strip0 round-player1,
.round-app.bug.partner-name-outside .partner-seat.seat-strip0 round-player0,
.round-app.bug.partner-name-outside .partner-seat.seat-strip0 round-player1 {
    order: -1;
}

/* The digits' box, not their size. `.clock` is 2.9em of the wrap, so at a 10px wrap
   it draws at 29px in a line box of 39.2 — taller than the 33.5 the clock has above
   the name, which put 2.8px of leading over the name's row and 2.8px past the top of
   the strip, where the strip's `overflow: hidden` took it. Nothing showed, because
   leading is empty, but the boxes overlapped. `line-height: 1` makes the box the
   glyphs and the overlap goes away without changing what is drawn. */
.round-app.bug .clock {
    line-height: 1;
}
/* the difference matters more than the clock digit it covers */
.round-app.bug .clock-difference {
    font-size: 1.5em;
}

@media (aspect-ratio <= 9/16) {
    /* The player's own strips run above and below a full-width board, so they lay
       their contents in a row: the pocket sets the strip's height and the name and
       clock ride beside it. Stacked, each strip cost the pocket's height PLUS the
       name/clock block — 91px against 57px — and portrait has no such height to
       spare. The partner's strips are not matched by this selector and keep the
       stacked treatment, which is what their narrow column needs. */
    .seat-strip0.bug, .seat-strip1.bug {
        flex-direction: row;
        align-items: center;
        justify-content: space-between;
    }

    /* Same as short landscape: the vertical budget does not accommodate the site
       header. Boards and strips alone take 720 of 835px on a 19.5:9 phone. */
    body[data-variant='bughouse'] header {
        display: none;
    }
    /* Same guarantee short landscape makes: this mode budgets the whole viewport
       for the boards, so the page itself must not scroll. Scoped to the bughouse
       round page only — html keeps overflow:visible, so the viewport takes its
       overflow from body.

       Structural rather than incidental, and it does more than tidy the scrollbar
       away. --bug-portrait-sq is computed from clientWidth, which EXCLUDES a
       scrollbar; a page that overflows therefore publishes a unit 8px wider than
       the space the board actually gets, and the board is clamped and quantised
       down (measured: 384 intended, 373.33 rendered, offset 4.67px). No scrollbar
       means clientWidth always equals innerWidth, so that whole failure mode
       cannot arise rather than merely happening not to. */
    body[data-variant='bughouse'][data-view='round'] {
        height: 100vh;
        overflow: hidden;
    }
    /* The app has to be as tall as the viewport for its own `1fr` slack row to
       have anything to distribute — a fractional track can only share out space
       that exists, the same trap the tools column hit in short landscape. Without
       these the app is content-sized, the slack row collapses to whatever the
       tools panel happens to want, and the player's block floats mid-page instead
       of sitting on the bottom edge. Measured before: slack 15.57px of an intended
       148, and the block ending 132.76px short of the bottom.

       `under-board` used to be hidden here, and the rule is gone because the element
       is: the round page no longer emits it (`two-board/round/round.ts`). It held
       `#janggi-setup-buttons`, which no bughouse game uses, and an empty crosstable
       container that nothing on this page ever filled — the crosstable is built by
       the ONE-board controller, which this page does not run. It cost a 30.7px row
       below the app for 7.71px of nothing, and the three modes each dealt with that
       differently: portrait and tall landscape said `display: none`, short landscape
       let it fall past the fold. Removing the markup settles all three at once.

       The old comment here called the hiding a cost paid in crosstables. It was not:
       there was never a crosstable in that box. Building one is a feature to add,
       not something these rules took away. */
    body[data-variant='bughouse'][data-view='round'] #main-wrap {
        height: 100%;
        /* minmax(0, 1fr) for the same reason as main.round.bug below: a bare `1fr`
           is minmax(AUTO, 1fr), so this row grew to the round page's min-content
           and carried the whole chain past the viewport — main.round and the app
           both inherited 919px in an 835px viewport. The cap has to be at every
           level between the body and the app, or the lowest bare fr wins. */
        grid-template-rows: minmax(0, 1fr);
    }
    body[data-variant='bughouse'] main.round.bug {
        height: 100%;
        /* minmax(0, 1fr), not a bare `1fr`. A bare fr is minmax(AUTO, 1fr), whose
           minimum is the item's min-content — so the round app could force this
           row past the viewport instead of being capped by it, and then its own
           slack row grew to fit rather than absorbing. Seen when the chat presets
           gained a size floor: the app went to 919px in an 835px viewport and the
           slack row to 233px. With a zero minimum the app is capped, its slack row
           absorbs, and the tools column scrolls its own content as intended. */
        grid-template-rows: minmax(0, 1fr) auto auto auto;
    }
    /* Portrait does NOT merge the columns — it keeps its own single-column-plus-
       tools arrangement, and its areas still name clockB-top, boardPartner,
       clockB-bot and tools directly. That is no longer how this mode works: portrait
       uses the merged column like the landscape modes, so the wrapper is a real box
       here too and only its widths are stated below. */
    .round-app.bug {
        /* The partner column is sized from the VIEWPORT, never from --cg-width-a.
           chessgroundx writes --cg-width-a from the measured width of board A,
           and board A is placed in this very column, so the previous rule

             calc((var(--cg-width-a) / var(--files)) * var(--pocketLength))

           was circular — and zero is a stable fixed point. Board A rendered 0
           wide (with all 33 pieces present, so purely a sizing failure) and all
           eight pockets collapsed with it, which makes bughouse unplayable: you
           cannot drop a piece without a pocket. Measured 386x835: --cg-width-a
           0px against a healthy --cg-width-b 389.33px.

           The partner board is a fifth of the window height and, being square,
           that is its width too — but expressed as the device-pixel-aligned
           square that client/two-board/squareUnit.ts publishes, not as a bare
           20vh. Reserving a rounder number than the board can occupy is what
           leaves a line between a board and the pocket beneath it. Deliberately
           no fallback: if the property is missing the track must break loudly
           rather than silently regain the slack. */
        /* Two columns, but only the partner's rows use the second one. The partner
           board is a fifth of the viewport height and square, so it leaves most of
           the width beside it empty — that is where the tools panel goes, rather
           than taking a row of its own below everything and pushing the page past
           the fold. Your own block spans both columns underneath. */
        /* One column. The two-column form existed only to put the tools beside the
           partner board, and the merged column does that inside itself now. With one
           column, "the player's own board is full width" is structural rather than
           something each area has to spell out. */
        grid-template-columns: minmax(0, 1fr);

        /* The app is exactly the width the full-width board can draw, and the
           sub-square remainder is spent OUTSIDE it. `--bug-portrait-sq` is this
           project's own copy of chessgroundx's quantisation, so the two agree by
           construction rather than by luck; see squareUnit.ts.

           On the app rather than on the board's own box, because this is the
           smallest box that contains both the player's stack and the partner's
           column. Sized lower down, the own stack would be centred in the app
           while the partner column still started at its left edge, and the two
           boards would disagree by the half-remainder — trading a 2px gap between
           a board and its pocket for a 1px step between the two boards. Every
           child then starts at the same x and the leftover has nowhere to fall
           except the page margins, which is where nobody reads it as a gap.

           Chessgroundx pins `cg-container` to the RIGHT of its wrap whenever the
           board it computed is narrower, so leftover inside any of these boxes
           shows as a gap down the left — 12px of it while the board's bounds were
           stale, and 2px once they were correct. */
        width: calc(var(--bug-portrait-sq) * 8);
        margin-inline: auto;

        /* Every row is a whole number of that board's own square, so no track
           reserves more than the board or strip inside it can occupy — the
           quantised units are what make that exact. A strip is one square tall,
           the same relation short landscape uses.

           Rows dropped as unoccupied: game-controls, offer, move-controls,
           uboard, uleft. Only eight areas have an element in this mode, and
           `under-board`/`under-left` are siblings of the round app, not children. */
        grid-template-rows:
                              minmax(0, 1fr)

                              min-content;
        /* The merged column takes the whole region above the player's own block, and
           gets the slack with it: its first row is `minmax(0, 1fr)`, so the block
           below stays flush with the bottom of the viewport whatever the two boards
           work out to.

           This is what the old template could not do. Its tools area was column 2
           only, so the corner under the partner board — measured at 165.3 x 175.3 —
           was a `.` that no part could ever reach. Inside the merged column that
           space is simply where a part goes. */
        grid-template-areas:
                              'rightcol'
                              'ownstack';
        column-gap: 0;
    }

    /* Each board is exactly the square unit for its axis, times the file count.
       NOT `width: 100%` with an aspect-ratio: `cg-board` is position:absolute and
       so contributes no layout height, which means this box is sized purely by
       CSS while chessgroundx quantises the board it paints inside it down to whole
       device pixels per file. Any difference between the two is a visible band —
       measured 4.66px between the board and the pocket below it at 386x835.
       Sizing the box from the same quantised unit makes the difference zero.

       This comment used to close after that paragraph and then run on for seven
       more lines to a second terminator, which left everything between them and
       the selector below reading as one invalid selector — so the rule below lost
       both its declarations. It had therefore never applied, and nothing showed
       for it, because the box it sizes was an inline box that ignores a width
       anyway — see `display` below. Two faults hiding each other. (A comment
       cannot quote a terminator: CSS ends the comment at the first one, which is
       how this file grew the fault in the first place.)

       Keyed by ROLE, not by `#mainboard`/`#bugboard`. Those are board A and board
       B, and twoBoardCtrl swaps which of them holds the `board` area per player —
       so sizing by id would hand a board-A player the partner's size on their own
       board. See roundControls.markRoles.

       `display: block` is load-bearing, not tidying. `selection` is an unknown
       element with no display of its own, so it is an INLINE box, and a width does
       not apply to an inline box at all — this one took 386 from its block child
       while the board inside drew 384. Chessgroundx pins its container to the
       right of any slack, so the 2px landed down the left, where it reads as the
       board and the pocket above it not lining up. The same trap is documented on
       `.cg-wrap.pocket` further down this file; it was fixed there and left here.

       Both boards are aligned to the LEFT edge of the app rather than centred
       inside it, so the small partner board lines up with the full-width board
       beneath it. The sub-square remainder is spent on the app itself, which is
       the one box containing both of them — see `.round-app.bug` above. */
    /* `display: block` is no longer set here — it is set once for every mode on the stack
       children near the top of this file, where the comment explaining the inline-box trap
       now lives. */
    .own-board {
        width: calc(var(--bug-portrait-sq) * 8);
        height: calc(var(--bug-portrait-sq) * 8);
    }
    .partner-board {
        width: calc(var(--bug-portrait-partner-sq) * 8);
        height: calc(var(--bug-portrait-partner-sq) * 8);
    }

    /* The panel takes the height left after the boards and strips and scrolls its
       own content, so the page itself does not scroll. min-height:0 is what lets
       a grid item shrink below its content at all.

       The body prefix is for specificity, not decoration. The top-level
       `.bug-round-tools` rule further down this file sets `overflow: hidden`, and
       at equal specificity the later rule wins — so without the prefix this
       `overflow-y` was silently reset and the column could not scroll. It only
       showed once the presets grew tall enough to need it: the column's content
       then forced the grid's slack row from 148px to 233px and pushed the page
       past the fold. Same trap as the short-landscape wrapper rule. */
    /* Portrait no longer keeps a tools block of its own: the parts are placed
       individually by the merged column, as in the landscape modes, which is what
       lets one of them take the space under the partner board. The rules that used
       to stack them in a flex column went with it. */
    /* PORTRAIT SIZES ITS PRESET BUTTONS BY THE SAME RULE AS EVERY OTHER MODE, so there is
       nothing here any more.

       There used to be `--bug-preset-btn: max(floor, (100vw - 32px) / 10)` on this element: a
       tenth of the viewport, on the reasoning that ten buttons is the most a row ever holds and
       the viewport does not move when a part drops. `publishPresetSize()` did not exist then. It
       does now, it runs in portrait like everywhere else — the value it published was sitting
       here all along, shadowed, because a declaration on `.chatpresets` beats an inherited one
       from the column.

       And the two disagreed, with the viewport rule wrong. Portrait puts the presets in a column
       BESIDE the partner stack, not across the page: measured at 274x830, that column is 106.7px
       and a tenth of the viewport gave 24.2px buttons, so each five-button set drew 133px and
       overflowed its panel by 26.3px. The published value, 18.9px, filled the column to 106.7px
       exactly. The premise the rule was measured from — that a preset row spans the screen —
       stopped being true when portrait's parts were merged into the column. */

    /* PORTRAIT KEEPS THE PAGE'S GROUND. The panel colour above is for the landscape modes,
       where the parts move between three places and want to read as one object wherever they
       land. Portrait has one arrangement and is not being changed. */
    .round-app.bug .chatpresets-panel,
    .round-app.bug .bug-presets-group {
        background-color: transparent;
    }

    /* Widths of the merged column's two tracks — the only thing this mode does not
       share. The partner board's eight squares, then whatever is left. */
    .bug-right-column {
        grid-template-columns: calc(var(--bug-portrait-partner-sq) * 8) minmax(0, 1fr);
        column-gap: 0px;
    }
    under-left {
        display: flex;
    }

    /* Pockets carry the same rule short landscape settled on: a pocket piece is
       exactly one board square, and the CELL it sits in is 0.8 of a square, so the
       pieces sit closer together than they would on the board. The row's height is
       what sizes the piece; the cell width is what spaces them.

       0.8 is the spacing constant, not the width. With this variant's five slots
       it happens to come to four board squares — half the width of an 8-file board
       — but a variant with a different --pocketLength keeps the spacing and takes
       whatever width that implies, which is why the length stays a multiplier.

       Sized from the board's OWN measured width rather than from the published
       units, because the units are keyed by role (yours / your partner's) while
       these selectors are keyed by board identity (A / B) — and twoBoardCtrl swaps
       which board holds the `board` area per player, so the two do not correspond.
       `--cg-width-a` is chessgroundx's own quantised measurement of board A, so it
       is exact; and reading it here is not the forbidden self-reference, because a
       pocket does not sit in the track it is sizing itself from. */
    /* ONE SCALE PER SEAT. Both strips are the same arrangement — pocket, name,
       clock, in a row above or below their board — differing only in scale: the
       partner's board is a fifth of the viewport height, your own is the full
       width, so their squares differ by roughly half. Every strip parameter is a
       fraction of its OWN board's square, so the two blocks are proportionally
       identical at different sizes.

       Keyed by ROLE, not by board identity, and inherited down to the pocket
       inside the strip. That is what markSeatRoles() is for: `.bug` means board B,
       so it is the partner's strip for a board-A player and the player's own for a
       board-B player. Inheriting from the strip also removes the need to name
       --cg-width-a/b per pocket, which had the same identity-vs-role mismatch. */
    .own-seat {
      --bug-seat-sq: var(--bug-portrait-sq);
    }
    .partner-seat {
      --bug-seat-sq: var(--bug-portrait-partner-sq);
      /* The partner block is the width of the partner board, so the strip lines up
         with the board it describes rather than spanning a width the board does not
         occupy. Left-aligned with it, and with the full-width board below. */
      width: calc(var(--bug-portrait-partner-sq) * 8);
    }
    .own-seat, .partner-seat {
      --bug-strip-h: var(--bug-seat-sq);
      /* Same 0.8 spacing short landscape settled on: the piece is one board square
         and the cell it sits in is 0.8 of one, so pieces sit closer together than
         they do on the board. --pocketLength stays a multiplier, so a variant with
         a different number of slots keeps the spacing and takes the width it needs. */
      --bug-pocket-sq: calc(var(--bug-seat-sq) * 0.8);
      /* The same fractions short landscape uses, but WITHOUT its `max(…, 0.85em)`
         floor. The floor keeps a name legible when a strip is large; here it would
         break the proportions of the partner block, which is deliberately small.
         Nikolay's call: keep the proportions even where the type gets very small. */
      --bug-clock-fs: calc(var(--bug-seat-sq) * 0.2);
      --bug-name-fs: calc(var(--bug-seat-sq) * 0.218);
    }
    .round-app.bug .twoboards .pocket {
      width: calc(var(--pocketLength) * var(--bug-pocket-sq));
      height: var(--bug-seat-sq);
      --fs: var(--bug-seat-sq);
      font-size: var(--fs);
    }
    /* Only the inner element carries the variant's real role count — chessgroundx
       writes --pocketLength on it, and the .cg-wrap.pocket above would fall back to
       the global default of 5 and mis-size any variant that differs.

       `display: block` is load-bearing, not tidying. The wrap is an inline box by
       default, which means two things: a sizing property does not apply to it at
       all, so max-content was inert; and it generates a LINE BOX, whose leading
       showed up as a gap between a board and its pocket. Measured while the strip
       was rotated: inline gave a 48.67px wrapper and a 28px gap, block 20.67px
       and 0. Kept now the strips are horizontal again — the wrapper should be the
       pocket's size in either arrangement. */
    .round-app.bug .twoboards .cg-wrap.pocket {
      display: block;
      width: max-content;
    }
    .clock-wrap {
      flex-direction: row-reverse;
    }
}

@media (aspect-ratio > 9/16) and (height < 600px) {
    body[data-variant='bughouse'] header {
        display: none;
    }
    /* The wrapper must FILL the width in this mode, and the `min-width: 800px`
       rule above stops it: that rule gives main.round.bug two content-sized
       columns and centres them, which is what the desktop mode wants, but this
       mode is also wider than 800px so it matches too.

       A content-sized wrapper makes this mode's `1fr` tools column inert — a
       fractional track can only distribute space that already exists — so the
       chat falls back to max-content and widens the app the moment a message
       arrives. The centred boards then slide left, out from under the bounds
       chessgroundx memoised at init, and every click lands one file off.

       Measured on a fresh game: one chat line took the app 1211.55 -> 1276 and
       board A 32.22px left, 0.59 of a square. With this rule the app is 1276
       from the start, the tools column resolves to its real 386.33px remainder,
       and the same chat line moves nothing at all.

       This is the requirement "No grid track is sized by late-arriving content"
       already states: the containing wrapper's own track has to fill the
       available width or the fractional column achieves nothing.

       The body prefix is for specificity, not decoration. The `min-width: 800px`
       rule is `main.round.bug` too and sits LATER in this file, so at equal
       specificity it would win and this would silently do nothing — it did,
       first time round. Overriding only the columns here rather than excluding
       this mode from that block, because the rows and areas it also sets are
       still wanted. */
    body[data-variant='bughouse'] main.round.bug {
        /* THE APP'S OWN TRACK FILLS, which `max-content` only did by accident.
           -------------------------------------------------------------------------------
           This rule was written so the wrapper fills the width, and it does whenever the
           app's natural width EXCEEDS the viewport: `minmax(0, max-content)` then clamps to
           what is available. It fails in the other direction, and this mode reaches that
           direction easily. The height is what is scarce here, so a short viewport gives a
           small square unit — measured 636x295: `--bug-sq` 29.34px, each board 8 squares =
           234.7px, and the app's content came to 599.75px inside a 636px viewport.

           The 36.25px left over went to the SECOND track, which holds nothing: the app's
           only siblings here are a zero-height `aside.sidebar-first` and a `display: none`
           `under-left`. Meanwhile the tools column inside the app is a `1fr` of that
           content-sized box, and a fractional track can only distribute space that already
           exists — so it resolved to 115.35px instead of 151.60px, and the movelist's
           content box to 107px. The game-over status line needs 121px, so the movelist grew
           a HORIZONTAL SCROLLBAR. One track, two symptoms.

           `1fr` on the APP's track states what the comment below already required of this
           mode: the containing wrapper's own track has to fill the available width, or the
           fractional column inside it achieves nothing. The second track stays content-sized
           — `max-content` is 0 while nothing is placed there, and still grows if something
           ever is. NOT a second `1fr`, which would split the width evenly and hand half of
           it to the empty column: measured 318px each, worse than what it replaced. */
        grid-template-columns: minmax(0, 1fr) minmax(0, max-content);
        justify-content: stretch;
        /* Zero, as tall landscape already sets for the same reason: the rows after the app
           are empty, and a gap between empty rows is still charged. With `under-board`
           removed the tracks came out `546.719px 0px 0px` and the two remaining gaps still
           added 22.05px, leaving the document 569px against a 551px viewport. The gaps
           separate nothing, so they cost nothing now. */
        row-gap: 0;
    }
    /* This mode budgets the whole viewport height for the boards, so the page
       itself must not scroll. html keeps overflow:visible, so the viewport takes its
       overflow from body.

       THE ANALYSIS VIEW MAKES THE SAME BARGAIN and used to be left out of it. Both pages
       are pinned to the viewport in this mode, and both let the tools be squeezed to half a
       square rather than shrink a board — see `squareUnit.ts`. A squeezed tools column has
       content that does not fit, by design: measured at 899x550 the engine's switch and its
       MultiPV label overhung a 29.6px column and the page scrolled sideways by 71px. The
       round page never showed it because this rule already caught it there. Unusable tools
       are the accepted outcome; a page that scrolls sideways is not. */
    body[data-variant='bughouse'][data-view='round'],
    body[data-variant='bughouse'][data-view='analysis'] {
        height: 100vh;
        overflow: hidden;
    }
    /* The tools column below is 1fr, and a fractional track can only share out
       space that exists. #main-wrap's own track is content-sized here
       (--main-max-width: auto, round.css), so without this the 1fr has nothing
       to distribute and the column stays content-sized exactly as before. */
    body[data-variant='bughouse'][data-view='round'] #main-wrap {
        grid-template-columns: 1fr;
    }
    /* THE ANALYSIS PAGE IS PINNED THE SAME WAY, which it was not until 2026-08-29. It pinned its
       wrapper in PORTRAIT and nowhere else, so in landscape the app had no definite height and
       every `1fr` row inside it resolved against content instead of against the viewport. Nothing
       showed while the tools sat in one fixed area, because that area could not grow; it is the
       first thing to fail the moment the tools are placed as individual items, and it fails
       silently — a row simply takes whatever its content wants.

       Same three declarations the portrait block already states for this view. */
    body[data-variant='bughouse'][data-view='analysis'] {
        height: 100vh;
        overflow: hidden;
    }
    body[data-variant='bughouse'][data-view='analysis'] #main-wrap {
        height: 100%;
        grid-template-rows: minmax(0, 1fr);
        /* The tools track below is `1fr`, and a fraction can only share out space that exists.
           #main-wrap's own track is content-sized here, so without this the 1fr has nothing to
           distribute — the round page states exactly this, for exactly this reason. */
        grid-template-columns: 1fr;
    }
    /* THIS MODE'S TRACKS, which are NOT the tall-landscape ones. The round page states them
       separately per mode and this page did not: it had one `@media (orientation: landscape)`
       block carrying the tall-landscape shape — a `max-content` second column and a `20vw` tools
       track, centred — and short landscape inherited it. That is why the boards sat in a centred
       block with 45px of dead space at each edge and the tools stopped at 255px.

       Here the second column is `1fr` and the app stretches, so the boards go to the edge and the
       tools take everything left over. Later in the file than the shared landscape block, so these
       win at equal specificity. */
    .analysis-app.bug {
        height: 100%;
        justify-content: stretch;
    }
    /* ONE HEIGHT-DERIVED SQUARE FOR BOTH BOARDS, which is what this mode means on the round
       page and what this page had never done. It sized both stacks from `--bug-tall-sq-a/-b`,
       the DESKTOP units — per board, and scaled by each profile's zoom slider — in every
       landscape mode, because it had a single `@media (orientation: landscape)` block where the
       round page has two.

       What that produced, measured at 1276x551: on one window the own board filled its column at
       54.67px while the partner board sat at 42.00px, 420px of a 551px column, and the 127px of
       slack under it was enough for the tab list to drop — which this mode on the round page
       never does, because there the stack fills the column by construction. On another window
       both boards were 43.34px because that profile's zoom was lower, so two windows in the same
       mode at the same size looked unrelated.

       `--bug-sq` is the same unit the round page uses here: derived from the app's height so ten
       rows fit it exactly, and identical for both boards. Zoom stops reaching the boards in this
       mode as a result — the round page reaches that conclusion too and hides the resize handle
       for it, which is the rule just below. */
    /* THIS MODE'S SQUARES ARE THE SHARED ONES. `--bug-sq` and `--bug-sq-b` are not a second
       geometry, they are an alias — every rule below keeps using them and the values now come
       from the one calculation in `squareUnit.ts`.

       They were computed twice, and the copies had drifted. Measured on a live page: `--bug-sq`
       and `--bug-tall-allow-a` were IDENTICAL to the last decimal, 50.00390625px, because they
       are the same formula — the header is hidden here, so `availableStackHeight()` already
       returns the whole viewport. Only the right board differed, 20.67 against 32.67, and that
       gap was this mode's copy still charging the tools' minimum against the boards: the model
       tall landscape abandoned. Raising `TOOLS_MIN_SQUARES` from 0.5 to 2 then silently took
       1.5 squares off this mode's right board, which is what a duplicated formula does when the
       constant it shares moves.

       ZOOM NOW REACHES THESE BOARDS, which is the one behavioural change. The mode used to size
       from an allowance directly and ignore the sliders, which is why it hid the resize handle;
       drawing from the same units as every other mode means the sliders work, so the handle is
       no longer hidden here either. */
    :is(.round-app, .analysis-app).bug {
        --bug-sq: var(--bug-tall-sq-a);
        --bug-sq-b: var(--bug-tall-sq-b);
    }

    .analysis-app.bug .bug-own-stack {
        --bug-stack-sq: var(--bug-sq);
        --bug-stack-allow: var(--bug-tall-allow-a);
    }
    .analysis-app.bug .bug-partner-stack {
        --bug-stack-sq: var(--bug-sq-b);
        --bug-stack-allow: var(--bug-tall-allow-b);
    }
    /* This mode gives the tools the whole leftover rather than a capped share. */
    .analysis-app.bug {
        grid-template-columns: auto auto minmax(0, 1fr);
    }
    .round-app.bug {
        /* Tracks are multiples of --bug-sq, the device-pixel-aligned square that
           client/two-board/squareUnit.ts publishes before the boards are built:
           a board is 8 squares, so 4 per column across its two columns, and a
           pocket row is exactly 1 square. Sizing the slot to what chessgroundx
           will actually render is what removes the leftover that used to show as
           lines between the boards and under them.

           Deliberately no fallback value: if --bug-sq is missing the var() is
           invalid at computed-value time and this whole declaration is dropped,
           which fails loudly instead of silently reverting to the old geometry.

           The last track is 1fr, not auto, so late-arriving chat content can no
           longer widen the grid and slide the centred boards sideways. */
        /* Rank labels are absolutely positioned and overhang their board to the
           right by -1 * --ranks-right (15px: a 3px lead-in then a 12px glyph
           box, chessground.css `coords.side`). With the boards now flush, the
           left board's labels would land on the right board, so one gutter
           track separates them. The right board's labels are left to overhang
           into the chat, which is harmless. A dedicated track rather than
           column-gap, because a gap would fall inside each board's two-column
           span and widen the board slot itself. */
        --ranks-gutter: calc(-1 * var(--ranks-right));
        /* Pocket cell width. 0.8 of a board square reproduces exactly the pocket
           this layout drew when it was pinned to four squares — five cells at 0.8
           is four squares — so compaction is this one value. Square cells (1.0)
           are not reachable: they leave the name 37px. */
        /* THE PER-BOARD PARAMETERS ARE NOT DECLARED HERE ANY MORE — see the shared per-seat rule.
           They were: `--bug-pocket-sq`, `--bug-strip-h`, `--bug-clock-fs` and `--bug-name-fs`, all
           written as fractions of `--bug-sq` ON THE APP. A custom property substitutes at the
           element that DECLARES it, so all four resolved against the LEFT board's square and were
           then inherited by both stacks — the partner stack's own rebinding of `--bug-sq` comes
           too late to affect a value already computed.

           Measured with a 50.0px left square and a 32.67px right one: the partner's strip was
           50px where its own square is 32.67, leaving 17px of slack that read as a gap between
           board and pocket and gave the username a line of its own; and its pocket was
           5 x (50 x 0.8) = 200px, six squares of a board eight squares wide. Both are the same
           mistake, and tall landscape had already avoided it by keying every one of these to a
           per-seat `--bug-seat-sq`. */
        /* Two columns: the left board, then everything else. The second takes the
           remainder rather than being content-sized — this mode has no width to
           spare, and the rule that the chat gives way before a board does depends
           on that track being the one that absorbs the shortfall. */
        /* FLATTENED ONTO THE SHARED TEMPLATE. This mode used to state a grid of its own: two
           columns — the viewer's stack and a merged `rightcol` holding everything else — over five
           further rows named `game-controls`, `offer`, `move-controls`, `uboard` and `uleft`. Of
           those five, nothing claimed four; they survive in this file only in comments about the
           trouble they once caused. The merged column is the arrangement tall landscape abandoned,
           and keeping it here meant the zones could not exist and the tools had nowhere to go but
           a column that shrinks to nothing.

           Three tracks and the shared zone vocabulary, exactly as the other landscape mode: the
           two boards' own squares, then whatever the tools are allowed. The parts are placed by
           the shared rules further down the file, and `toolsHome()` chooses between the
           arrangements from the viewport, which is a calculation this mode can now reach.

           The tracks are the SAME EXPRESSIONS the other mode uses. `--bug-sq` is an alias of
           `--bug-tall-sq-a` here, so writing the shared names costs nothing and removes the last
           place where one mode's geometry was spelled differently from the other's. */
        grid-template-columns: calc(var(--bug-tall-sq-a) * 8)
                               calc(var(--bug-tall-sq-b) * 8)
                               var(--bug-tools-track);
        grid-template-rows: minmax(0, 1fr) auto auto auto auto auto;
        grid-template-areas: var(--bug-zones-strip);
        /* The gutter that used to be its own track. With only two columns there is
           exactly one gap and it falls between them, which is what the separation
           was always for — the left board's rank labels overhang to its right by
           --ranks-right and must not paint onto the right board. The old comment
           justifying a track over a gap argued that a gap would land inside a
           board's two-column span; each board occupies a single column, so that no
           longer applies. */
        column-gap: var(--ranks-gutter);
    }
    /* The resize handle is not stated per mode any more — see the one decision near the top
       of this file, which hides it everywhere and shows it in tall landscape alone. */
    /* The merged second column, as in tall landscape: the right board's stack
       floated, the parts arranging themselves in what is left. `flow-root` makes
       this a block formatting context so it contains the float instead of
       collapsing around it.

       This mode has no spare height — the stack fills the column — so in practice
       nothing ever reaches the space under the board here. The rules are the same
       as tall landscape's on purpose; only the outcome differs. */
    /* DISSOLVED, as in the other landscape mode. A row spanning both boards can only be spanned
       by an item of the grid that owns both board columns, so the merged column has to stop being
       a box for the zones to exist at all. Its children become grid items of the app and are
       placed by the shared rules. */
    :is(.round-app, .analysis-app).bug .bug-right-column {
        display: contents;
    }
    /* Only what this mode does not share — see the top-level rule for the rest. */
    .bug-right-column {
        /* `--bug-sq-b`, not `--bug-sq`: THE RIGHT BOARD IS THE ONE THAT YIELDS.
           This mode forces both boards to the full height, so when the width cannot hold two
           of them beside the tools' half-square the shortfall has to come from somewhere, and
           the answer is the same as tall landscape's — never the viewer's own board.
           `squareUnit.ts` publishes this unit already quantised; it equals `--bug-sq` whenever
           the width is sufficient, which is most of the time. */
        grid-template-columns: calc(var(--bug-sq-b) * 8) minmax(0, 1fr);
        column-gap: 0px;
    }
    /* EVERYTHING IN THE PARTNER'S STACK FOLLOWS ITS OWN SQUARE. The pocket cell, the strip
       height, the clock and name type are all derived from `--bug-sq` a few rules above, and a
       narrowed board with a full-width pocket beneath it is exactly the mismatch this file
       spends its comments preventing. Rebinding the name inside the stack makes every derived
       value follow with no second copy of any formula — and it is scoped to the subtree, so
       the app's own tracks, which must stay on the left board's unit, are untouched. */
    .bug-partner-stack {
        --bug-sq: var(--bug-sq-b);
    }

    /* A button is 60% of its share of the parts column — the size the presets drew at
       before the fixed track, reproduced: 0.6 of a fifth of 382.3 is 45.9, which is what
       that column measured.

       THE 0.6 IS LOAD-BEARING, not a taste. It is what leaves room for two sets to pair.
       A set is five buttons and four gaps, so at 0.6 it is 241.5 of a 382.3 column:
       too wide to have company beside the board, where the sets stack as two rows of
       five, and narrow enough that two of them fit the 820 a part gets once it has
       dropped, where they pair into one row of ten. Sized to FILL the column instead,
       a set is 382.3 and two of them need 764 — more than any dropped width here — so
       the parts kept their two rows of five under the board and the pairing never
       happened. That was option B, and this is why it was wrong.

       Sized from the COLUMN, never from the part: a part is 382.3px beside the board and
       the whole column wide once it has dropped, so sizing from the part is what drew the
       same button at 41.6px and 24.9px on one screen. `--bug-parts-w` is the column's own
       track, published by partsWidth.ts.

       `.round-app.bug` is not decoration. A media block adds no specificity, so a bare
       `.chatpresets` here would tie with the top-level default and lose to it on document
       order — measured, and the buttons stayed at the floor. Portrait's rule carries the
       same prefix for the same reason.

       The floor still wins where the column is too narrow for it, which is the whole
       purpose of a floor. The `0px` fallback is for the frame before the JS has published
       anything: without it the substitution is invalid and the track breaks. */
    .round-app.bug .chatpresets {
        /* The button no longer takes a size from a published width — see `.chatpresets-set`. */
    }

    /* THE GAP IS A VALUE, NOT A LEFTOVER, so that every row steps by the same pitch.

       A set used to be `flex: 1 1 auto` with `justify-content: space-between`, which spread
       whatever the row had left over across the gaps INSIDE each set. Two rows of the same
       control then stepped by different amounts — measured 38.3 in the column against 53.5
       under the board — so nothing lined up, and the spacing slid 53.52 -> 38.52 -> 26.52 as
       the board was resized under it. Spreading also has nothing to say about the boundary
       BETWEEN two sets, which is why that gap was 0 while every other gap was 53.5.

       Stated as a length instead, the same number serves inside a set, between two sets and
       between two rows. Five buttons and four gaps fill the parts column exactly, so this
       reproduces what `space-between` drew in the column — 38.27 against 38.3 here, 38.64
       against 38.63 in short landscape — while a row of ten now steps by that same pitch and
       its last five land exactly under the five above it. The spare width goes to the leading
       margin, which is what anchors the last set to the trailing edge and keeps the alignment
       stable when the spacing changes.

       `max()` against the base gap because a narrow parts column would otherwise compute a
       spacing at or below zero. */
    .round-app.bug .chatpresets {
    }

    /* A DROPPED PART IS WIDER THAN THE COLUMN AND MUST NOT STEP WIDER.

       Its row of ten sits directly under the rows of five still beside the board, and the
       two only line up if both step by one pitch. Letting the dropped row fill its zone
       gave it its own, larger pitch — measured 81.5px under the board against 74.8px in
       the column — which put every button out of line except the last, the sets being
       trailing-aligned.

       So a dropped part keeps the COLUMN's pitch, and goes below it only where its own row
       cannot hold ten buttons at that pitch: `min()` of the two, floored as ever. That is
       what lets a part drop into a zone narrower than two full sets — the case this whole
       rule exists for — while a zone with room to spare simply leaves the surplus in the
       leading margin instead of spending it between the buttons.

       THE SECOND TERM IS `100cqi`, NOT A PERCENTAGE. It is the width of the zone the part
       dropped into, and it has to be read from an ancestor: the sets are sized by their
       content, so a percentage written on a set would resolve against a width that is
       itself the sum of these very gaps. The panel is the container because the part and
       its sets are both inside it, so the part's gap between two sets and each set's gap
       between two buttons resolve against the same number and cannot disagree. */
    .round-app.bug .chatpresets-panel {
        container-type: inline-size;
    }
    /* When the viewport is too narrow for everything, the chat is what gives way
       — never the boards. A grid item's automatic minimum size is its min-content,
       and this one's presets grid holds that at ~196px, which is enough to push
       the whole grid wider than the viewport. The app is centred, so that overflow
       spills equally to both sides and the left board's first files fall off the
       screen entirely — with `body { overflow: hidden }` above, they cannot even be
       scrolled to. Dropping the floor lets the 1fr track take only what is left,
       so both boards keep their full width and stay on screen, and the boards are
       centred exactly as before whenever there is room to spare.

       Measured on an iPhone SE landscape viewport (667x375, the tightest phone
       tried): the grid was 782px against 665 with board A at x=-59; after this it
       fits exactly, board A sits at x=0, and the chat takes the whole shortfall.
       An iPhone 14 (844x390) never reaches the floor and is unaffected. */
    under-left {
        display: none;
    }
    .pocket-top .twoboards .pocket, .pocket-bot .twoboards .pocket {
      width: calc(var(--pocketLength) * var(--bug-pocket-sq));
      height: var(--bug-sq);
      --fs: calc(var(--cg-height-a) / var(--ranks));
      font-size: var(--fs);
    }
    .pocket-top-partner .twoboards .pocket, .pocket-bot-partner .twoboards .pocket {
      width: calc(var(--pocketLength) * var(--bug-pocket-sq));
      height: var(--bug-sq);
      --fs: calc(20px / var(--ranks));
      font-size: var(--fs);
    }
    /* Only the inner element carries the variant's real role count — chessgroundx
       writes --pocketLength on it. The .cg-wrap.pocket above it would fall back to
       the global default of 5, so it shrink-wraps instead of being sized. */
    .round-app.bug .twoboards .cg-wrap.pocket {
      width: max-content;
    }
    /* A board's file labels overhang below it into the strip. Lift the board so
       they paint over the pocket and clock rather than under them; the boards' own
       boxes end above the strip, so nothing else is covered. They never take
       pointer events, so a drag begun on a letter still reaches the pocket piece
       beneath — stated here rather than relied on from chessground.css. */
    .bug-own-stack > selection#mainboard, .bug-own-stack > selection#bugboard {
        z-index: 2;
    }
    :is(.round-app, .analysis-app).bug .cg-wrap coords {
        pointer-events: none;
    }

    /* THIS MODE'S SPARE HEIGHT — the same expression the desktop mode uses, over this mode's
       own unit. It comes to the quantisation remainder and nothing else, measured 4.3px at
       1276x551, which is below the floor: the gap is zero and the coordinates sit on the
       squares exactly as they do today.

       Stated rather than left to the shared 0px default, because the default would be saying
       "this mode has no room" when what is true is "this mode's room does not reach the
       floor". The arithmetic reaching zero on its own is the whole point — no mode is named
       anywhere in the switch — and a future change that gives this mode height gets the gap
       for nothing. On the STACKS, not on the app: the shared rule declares the default on
       these same elements, and an element's own declaration beats an inherited one. */
    /* AGAINST THE BOARD'S OWN ALLOWANCE, not the app's height. The two differ now: the right
       board can be capped by WIDTH, so it is smaller than the height would allow while still
       being at full size, and measuring against the app credited it with room it cannot use —
       measured at 799x550, 157px of it, which put its coordinates outside the board and its
       username on a line of its own while the left board kept both in. This mode has no zoom,
       so a board is always at its allowance and the room is always zero; the formula is written
       out rather than collapsed to `0px` so that it stays correct if that ever changes. */
    .bug-own-stack {
        --bug-stack-allow: var(--bug-tall-allow-a);
        --bug-coord-room: calc(10 * var(--bug-tall-allow-a) - 10 * var(--bug-tall-sq-a));
    }
    .bug-partner-stack {
        --bug-stack-allow: var(--bug-tall-allow-b);
        --bug-coord-room: calc(10 * var(--bug-tall-allow-b) - 10 * var(--bug-tall-sq-b));
    }
}

@media (aspect-ratio > 9/16) and (height >= 600px) {
    /* The same pinning portrait and short landscape already do, for the same
       reason: --bug-app-h is the viewport height less the header, so the app has
       to actually get that height. Left content-sized, the app takes what its rows
       want and `under-board` reserves a row beneath it — measured here at 72.6px
       below the app, which the boards would then be overflowing the page by.

       `under-board` is gone from the markup entirely now, so the row it reserved is
       not there to pin against in any mode. It cost no crosstable to remove: nothing
       on this page ever drew one in it. A crosstable on a desktop bughouse board is
       still worth having and is still worth revisiting — as something to BUILD. */
    body[data-variant='bughouse'][data-view='round'] {
        height: 100vh;
        overflow: hidden;
    }
    /* Not `height: 100%`. The body is `display: block` and a full viewport tall,
       so 100% is the whole viewport while this wrapper starts below the header —
       it ran to 887px in an 827px viewport, over by exactly the header's 60px.
       --bug-app-h is that height already measured. */
    body[data-variant='bughouse'][data-view='round'] #main-wrap {
        height: var(--bug-app-h);
        grid-template-rows: minmax(0, 1fr);
    }
    /* THE ANALYSIS PAGE, PINNED IDENTICALLY. It pinned its wrapper in portrait only; short
       landscape got the same treatment on 2026-08-29 and this mode needs it for the same reason.
       Left unpinned, the app's `1fr` row resolves against content rather than the viewport, and
       once the tools are placed as individual grid items the panel is what that content is —
       measured here at a 704px app in a 689px viewport, the page scrolling and the tab list below
       the fold.

       `--bug-app-h`, not `height: 100%`: this mode has the site header above the wrapper, so 100%
       would be the whole viewport and overshoot by exactly the header. */
    body[data-variant='bughouse'][data-view='analysis'] {
        height: 100vh;
        overflow: hidden;
    }
    body[data-variant='bughouse'][data-view='analysis'] #main-wrap {
        height: var(--bug-app-h);
        grid-template-rows: minmax(0, 1fr);
    }
    /* AS TALL AS ITS CONTENT, not as tall as the pinned wrapper — the same declaration the round
       page's block above makes, for the same reason. The first row absorbs the slack, so an app
       stretched to the wrapper hands the tools panel every pixel the boards did not use: measured
       at 996x730 with both boards at 60%, a 639px panel beside a 460px board.

       `height: 100%` is kept as the fallback, which is what this rule was, so the frame before
       anything has been measured is unchanged. */
    .analysis-app.bug {
        height: var(--bug-app-content-h, 100%);
    }
    body[data-variant='bughouse'] main.round.bug {
        height: 100%;
        /* Zero, because the three gaps between these four rows charged 16.54px each
           against the app's `1fr` — 49.6px, which is why the app was getting 717.41
           of its 767 and its seat strips ran past its own bottom edge. The other
           three rows are empty in this mode, so the gaps separate nothing. */
        row-gap: 0;
        grid-template-rows: minmax(0, 1fr) auto auto auto;
    }
    /* `under-left` is the spectator strip, and it sits in an auto row of
       main.round.bug, so it takes its height off the app's `1fr` before the app
       sees it — measured at 49.6px, which left the app 717.41px of its 767 and put
       the seat strips 43px past its own bottom edge.

       Short landscape and portrait already hide it. Doing the same here keeps one
       rule for all three modes, but it is the mode where a spectator list is most
       worth having, so this is the thing on this screen most worth revisiting now
       that `under-board` has been removed rather than hidden. Unlike that box, this
       one is real: the round page does receive a spectator list. Giving it a home
       inside the tools column would settle it. */
    body[data-variant='bughouse'] main.round.bug > under-left {
        display: none;
    }
    /* This mode sets no strip parameters: its rows are min-content, so the strip
       sizes to its contents, and the name falls back to the shared 0.85em floor
       rather than the 0.7vw it used to inherit. The mirrored justify-content pair
       that used to sit here went with the mirror itself — both boards now read
       pocket, name, clock, so there is nothing left to mirror. */
    .round-app.bug {
        /* No --board-scaleA/B here any more, deliberately. Every track that used
           them multiplied a quantised unit by a zoom fraction, which is exactly what
           un-quantises it; the zoom now enters before the flooring, in
           client/two-board/squareUnit.ts, and arrives as --bug-tall-sq-a / -b with
           the scale already inside it. Re-introducing a scale variable here would
           re-introduce the defect, because the only thing left to multiply is a
           number that is already final. */
        grid-row-gap: 2vmin;
        /* One column per board, and the two boards adjacent: a player compares
           them constantly, so nothing sits in the gap between them and the tools
           column moves to their right.

           A board is eight of its column's own square, and that square already has
           the column's zoom in it. Each column keeps its own unit, so the two
           sliders stay independent.

           The tools column is a flat fifth of the viewport and deliberately does
           NOT scale with zoom — its width is about reading chat and move lists,
           not board geometry. This is a change: the track it replaces was
           pocket-derived from --cg-width-a and therefore grew with the board. The
           cost is that at low zoom the panel is proportionally dominant.

           `move-controls` and `uboard` are gone: measured on the live page, of
           the ten areas this template declared only eight had an occupant.
           #move-controls now lives inside the Moves panel with the movelist block,
           and under-board is a sibling of the round app rather than a child.

           toolsB is gone too, and with it the last of the areas that held nothing
           worth a track. It carried #offer-dialog — the draw and rematch prompt —
           in a full-width row below both boards, which drew an offer's answer
           nowhere near the control that asked for it. An offer is now a look the
           control itself wears, so the element and its row went together. */
        /* Height-derived, not width-derived. --bug-tall-sq-a is the same ten-row
           square short landscape uses, measured over the viewport less the header,
           so at full zoom the stack — pocket row, board, pocket row — fills exactly
           the height the app is given and the board is as large as it can be. The
           board row stays min-content so a board scaled below full zoom shrinks its
           own row instead of leaving a band between itself and its pockets.

           THE RULE FOR EVERY TRACK THAT SIZES A BOARD, in this mode and the other
           two: the track is a published unit times the file count, and NOTHING is
           multiplied into it afterwards. A published unit is a whole number of
           device pixels per square, which is what makes chessgroundx's own flooring
           a no-op on it — the track and the board then agree exactly. Multiply it
           by anything that is not itself whole and the board floors the product
           again, keeps the difference, and pins itself to the RIGHT of what is
           left; the pocket above it is laid out from the LEFT, so the whole
           remainder opens between the two and reads as panels that do not line up.

           That is not a rounding error. The loss is up to eight device pixels — a
           whole square's worth — and this track lost 6.39 of them: `* 8 *
           var(--board-scaleA)` on a 76px unit at zoom 80 asked for 60.8 device
           pixels per square, got 60, and handed a 486.39 track to a 480 board.
           The zoom now enters before the flooring, in squareUnit.ts.

           A unit is also only as good as the box it is written on: a width does
           not apply to an inline box at all, which is how portrait kept a 2px
           version of the same gap for as long as it has existed. See `.own-board`
           in the portrait block. */
        /* Two columns. The second is `max-content` — the right board and the tools
           together, which is what .bug-right-column holds — rather than a board
           track and a tools track. A grid track is sized by its widest item and
           never by two items side by side, so the merge cannot be expressed as two
           separately placed items; the wrapper is what makes the sum available. */
        /* minmax(0, max-content), not bare max-content. The second column holds a
           wrapping flex column, and a wrap that spills one part into an extra
           column would otherwise widen the app past the viewport — seen at 117px
           over. Capped, the page cannot overflow however the parts wrap; the column
           clips instead, which is the same bargain the tools column always made. */
        /* THREE TRACKS, NOT TWO. The merged column's own two tracks — board B, then the tools
           — are stated here instead, because that column is dissolved below and its children
           are grid items of this app now. The same two numbers, one level up: nothing about
           the widths changes. What changes is that a row can now span from the LEFT board's
           edge to the tools' right edge, which is the whole point of zone B. */
        grid-template-columns: calc(var(--bug-tall-sq-a) * 8)
                               calc(var(--bug-tall-sq-b) * 8)
                               var(--bug-tools-track);
        column-gap: 2vmin;
        /* No row gap: the pockets are meant to sit flush against the board, and a
           gap here is the same leftover that shows as a line between them. */
        grid-row-gap: 0;
        /* Every row explicit. The board row was `min-content`, meaning to let a
           board below full zoom shrink its own row — but min-content is indefinite
           and resolves against the board in it, while that board's height comes
           back from the row. That is the circular sizing the layout spec forbids,
           and unlike the zero fixed point it runs the other way: the boards grew
           to 1528px and 1312px in an 827px viewport, one pass at a time.

           Sized from the LARGER of the two scales, because one row carries both
           boards. At equal zoom — the normal case, and the default — this is
           exactly eight squares and the stack is exactly ten.

           ONE row now, where there were two. The second held only #offer-dialog and
           is gone with it — worth recording that as `auto` it had stretched to 17.4px
           and put the app 60px past the viewport it had just been pinned to, so an
           empty row here was never free. */
        /* All three of board A's rows scale with board A, so its stack stays exactly
           ten of ITS OWN squares whatever the zoom. Left at the unscaled unit the
           strip rows would keep a full-size board's height under a reduced board and
           show as a band between the board and its pockets.

           No `max(scaleA, scaleB)` on the board row any more. That was needed when
           both boards shared this row; board B now lives inside .bug-right-column,
           which spans these rows as one item and sizes itself, so this row carries
           board A alone and taking the larger of the two only inflated it. */
        /* The leftover below full zoom — 156.96px at 80% zoom in a 767px app, measured —
           is spent by the two stacks: the coordinate gap takes what the labels need and
           `seatNamePlacement` sees only what is left when it decides whether a username
           may have its own line. That ordering is deliberate and one-way; the gap is
           computed from this mode's height and its own square, neither of which depends
           on a name, so a name can never move the gap.

           This paragraph described five rows and a `reservedGap` in seatNamePlacement.
           The template has two rows and that identifier never existed. */
        /* Two rows: the viewer's stack, and the tools strip under it. The stack was
           three rows — one per strip and one for the board — which is what a
           container does for the partner's stack without any rows at all. `1fr` so
           the stack has the height to arrange itself in, rather than being measured
           by tracks that had to be told how tall a strip becomes when a name leaves
           it. */
        /* ONE ROW PER TOOLS PART, PLUS ZONE B. The first absorbs the slack — it is the chat's
           row, and the chat is the part that takes whatever the others leave. The rest are
           their content's height. Both stacks span rows 1-4, so the boards keep exactly the
           height they have today while zone B is empty, and give some of it up when it is
           not, which is what a row under both boards has to cost. */
        grid-template-rows: minmax(0, 1fr) auto auto auto auto auto;
        /* THE APP IS AS TALL AS ITS CONTENT, not as tall as the viewport — the taller board
           plus whatever zone B holds, published by toolsPlacement.ts. Stretched to the viewport
           it handed every unused pixel to the `1fr` row above, so the chat ran far past the
           boards and the buttons under them sat below a band of nothing.

           The fallback is the full height, which is what this was before, so the page is
           correct on the frame before anything has been measured. */
        height: var(--bug-app-content-h, 100%);
        /* The coordinate gap is no longer set here. It was a length assigned in this one
           block and read by nothing, beside a comment describing a minmax() row this
           template does not have. Both are gone; the gap is now computed once at the top
           of this file from the room each mode declares, and this mode declares its room
           on the two stacks below. */
        /* NAMED FOR WHERE THEY ARE, NOT FOR WHAT SITS IN THEM — the analysis page's rule,
           now shared. `zoneToolsN` is the strip beside the board on row N; `zoneAN` is that
           same row widened to take the space the PARTNER board frees; `zoneB` is the full
           width under BOTH boards. A part on row N sits in one of the first two, and the drop
           classes below choose which.

           One slot per ROW rather than one `zoneTools` for all of them, because a named area
           is one rectangle and holds one item: two parts given the same name OVERLAP, which
           is measured and is why the analysis page numbers its slots too. */
        grid-template-areas: var(--bug-zones-strip);
    }
    /* The merged second column: the right board's stack floated left, the tab parts
       and the bar arranging themselves in what is left of it.

       `flow-root` rather than `grid`. A grid would place the parts in tracks, which
       is what the old tools container did one level down, and tracks are exactly
       what makes the space under a shrunken board unreachable. Floated, the stack
       is an obstacle of a definite size: a part sits beside it while there is room
       beside it, and once the float has ended — which is what happens as soon as
       the board is scaled down — the parts below it have the column's full width.

       The float is contained rather than escaping, which is the whole reason for
       `flow-root` over a plain block. */
    /* DISSOLVED. Its two tracks are the app's now, and its children — the partner board's
       stack and every tools part — are grid items of the app, placed by the areas above.

       This is the one structural change, and it is what zone B needs: a row under BOTH
       boards can only be spanned by an item of the grid that owns both board columns. While
       the parts were nested one level down, the widest a dropped part could ever be was the
       merged column, which is the partner board plus the tools and never the left board.

       The analysis page already dissolves this same element for the same reason; this makes
       the two pages the same shape rather than two shapes that happen to look alike.

       `display: contents` is the whole rule. It has no box, so its `overflow: hidden` and
       its own tracks stop applying — which is why they are stated on the app above, and why
       partsWidth.ts reads the app's last track when this element reports no tracks at all. */
    /* The round page's part placements and arrangements are shared by both landscape modes
       now — see the block after this one. */


    /* ZONE B: the presets take the full width under BOTH boards.

       The roomiest home they have, and the only one wide enough for a single row, so
       toolsPlacement.ts tries it first and falls back to zone A. The group is the grid item
       here and the two panels are its children.

       WRAPPING CHOOSES THE SHAPE. Each panel is ten buttons wide; either two of them fit the
       row or they do not, and flex wrap answers that on its own — one row of twenty when zone
       B is wide enough, two rows of ten when it is not. Nothing measures, and there is no
       state to keep in step. */
    .round-app.bug.drop-presets-b .bug-presets-group {
        grid-area: zoneB1;
        display: flex;
        flex-wrap: wrap;
        /* ONE GAP EVERYWHERE, so a paired row reads as a single row of twenty rather than two
           blocks that happen to be adjacent. The join used to be computed as "the leftover of a
           row of twenty" — a different number from the gap inside each set, and one that came
           from the published button size. The buttons now share their row through flexible
           tracks, so the join is simply the pitch. */
        column-gap: var(--bug-preset-gap, var(--bug-preset-gap-min));
        row-gap: var(--bug-preset-row-gap);
    }
    /* A DEFINITE BASIS, which a panel needs here and nowhere else: beside the board it is a grid
       item and its row stretches it, while here it is a flex item and would take its width from
       its content — content that stretches back to the panel. Circular, and it resolved to zero:
       0px panels with their buttons hanging 216px out to the left, measured.

       Stated in SETS rather than in a button size. It used to read `10 * var(--bug-preset-btn)`,
       from a number published out of JS; the sets below now carry their own floor basis, so the
       panel simply asks for two of them and the same wrap point falls out of one statement
       instead of the same decision being made twice at two levels. */
    .round-app.bug.drop-presets-b .bug-parts .chatpresets-panel {
        flex: 1 1 min-content;
        min-width: 0;
    }

    /* THE TELLS LEAD AND THE PIECE REQUESTS FOLLOW, in every arrangement. This used to apply only
       to the single row, which meant the same two blocks read in one order paired and the other
       order stacked; one order everywhere is the simpler rule and the one a reader can learn.
       `order` rather than `row-reverse`, which would also flip what the alignment means. */
    .round-app.bug .bug-parts .chatpresets-panel-1 {
        order: 1;
    }


    /* Same policy as short landscape — see the comment there for the 0.6, for why the
       size comes from the column, and for why the selector carries `.round-app.bug`.
       This mode's track is literally `20vw`, so `--bug-parts-w` could have been written
       as a vw expression here; it is not, so that one rule covers both modes and neither
       can drift from the track it is supposed to follow. */
    /* The GROUP carries it as well, so that the panels inside it can state a width in terms of
       the button — which is what zone B needs and what a panel beside the board never did,
       being stretched by its grid row instead. Declared, not inherited from `.chatpresets`:
       properties inherit downwards and the group is above the parts, not below them. */
    .round-app.bug .chatpresets,
    .round-app.bug .bug-presets-group {
        /* The button no longer takes a size from a published width — see `.chatpresets-set`. */
    }

    /* Same pitch rule as short landscape, and for the same reasons — see the comment
       there. Stated per mode rather than once, because the value depends on
       `--bug-preset-btn`, which each mode sets for itself. */
    .round-app.bug .chatpresets {
    }

    /* Same cap on a dropped part's pitch as short landscape, and for the same reasons —
       see the comment there. */
    .round-app.bug .chatpresets-panel {
        container-type: inline-size;
    }
    /* Keyed on the APP, not on the merged column. This mode flattens the page, so the column
       is dissolved and toolsPlacement.ts puts the drop classes on the app instead — the
       element whose template they swap. Short landscape keeps the column and keeps the other
       form; the two selectors can never match the same element. */
    /* Each seat's furniture is sized from ITS OWN board, the way portrait already
       does it with --bug-seat-sq. Sharing one unit across all four seats is what
       left a 35%-zoom board with a full-size board's furniture: a strip 2.9 of its
       own squares tall, and a clock of 136.7px against a 208px board — 70% of the
       whole board's width, which is what crushed that seat's username to 5.8px.

       Own maps to unit A and partner to unit B because the units belong to the
       COLUMNS, not to the boards: --bug-tall-sq-a is the left column's and the
       viewer's own board is always the one in the left column, which is exactly what
       markRoles() marks. A switch moves the boards between columns and re-marks the
       roles, so the pairing holds.

       The unit is taken whole rather than multiplied by anything, for the same
       reason the board tracks are — see the rule stated on .round-app.bug above.
       A strip one square tall is one square of the board beneath it exactly.

       Derived from the published units, never from --cg-width-a/b. Those are what
       chessgroundx measured for a board sitting in a track this value helps size —
       the circular sizing the layout spec forbids, and which in this file has already
       produced both a collapse to 0x0 and a runaway to 1528px. */
    /* THIS MODE'S SPARE HEIGHT, per column, which is what the coordinate gap is drawn from.
       The stack is exactly ten of its column's squares — pocket row, board, pocket row — and
       the app is --bug-app-h, so whatever the two differ by is height nothing has claimed.
       At full zoom that is only the quantisation remainder, measured 6.96px in a 767px app,
       which is below the floor and therefore no gap at all: full zoom looks as it always has.
       Lower zooms leave far more — 66.96px at 91%, 156.96 at 80% — and the labels take the
       16 they want.

       Per COLUMN, keyed exactly as the units are, so two boards at different zooms get
       different gaps and neither is affected by the other's. */
    /* Against each column's ALLOWANCE — its own square at full zoom — rather than the app's
       height. At full zoom that is zero spare by construction, which is the rule: a board at
       100% keeps its coordinates and its username inside, whatever the width had to do to its
       square. Zooming out is then the only thing that creates room, which is what this gap was
       always for. */
    .bug-own-stack {
        --bug-stack-allow: var(--bug-tall-allow-a);
        --bug-coord-room: calc(10 * var(--bug-tall-allow-a) - 10 * var(--bug-tall-sq-a));
    }
    .bug-partner-stack {
        --bug-stack-allow: var(--bug-tall-allow-b);
        --bug-coord-room: calc(10 * var(--bug-tall-allow-b) - 10 * var(--bug-tall-sq-b));
    }

    /* The per-seat parameters are shared by both landscape modes now — see the block below. */

    /* `.round-app.bug` in the selector is required, not habit: the strip's height is
       set by `.round-app.bug .seat-strip0` at (0,3,0), and without the app class this
       rule is (0,2,0) and loses. The symptom was the giveaway — the name moved below
       the clock as asked, the strip stayed one square tall, and since a strip clips
       its overflow the name simply disappeared. */
    .round-app.bug.own-name-outside {
        --bug-own-strip-row: min-content;
    }
    .pocket-top .twoboards .pocket, .pocket-bot .twoboards .pocket {
      width: calc(var(--pocketLength) * (var(--cg-width-a) / var(--files)));
      height: calc(var(--cg-height-a) / var(--ranks));
      --fs: calc(var(--cg-height-a) / var(--ranks));
      font-size: var(--fs);
    }
    .pocket-top-partner .twoboards .pocket, .pocket-bot-partner .twoboards .pocket {
      width: calc(var(--pocketLength) * (var(--cg-width-b) / var(--files)));
      height: calc(var(--cg-height-b) / var(--ranks));
      --fs: calc(var(--cg-height-b) / var(--ranks));
      font-size: var(--fs);
    }
}

/* THE ROUND PAGE'S LANDSCAPE ARRANGEMENTS, SHARED BY BOTH LANDSCAPE MODES.
   ------------------------------------------------------------------------------------
   These were inside the tall-landscape block, which is where the flat grid was built. Short
   landscape has now been flattened onto the same template, so they belong to both: the two modes
   differ in the HEIGHT they have to spend, not in how the parts are arranged in it.

   Nothing here mentions a mode. Every track is a published square and every arrangement is one of
   the `--bug-zones-*` values, both of which already account for the header being hidden in one
   mode and present in the other — `availableStackHeight()` measures it rather than assuming it. */
@media (aspect-ratio > 9/16) {
    /* EVERY PER-BOARD PARAMETER IS KEYED TO ITS OWN SEAT, in both landscape modes.
       ------------------------------------------------------------------------------------
       A custom property substitutes at the element that DECLARES it, so anything written as a
       fraction of `--bug-sq` on the APP resolves against the LEFT board and is inherited by both
       stacks — a partner stack that rebinds `--bug-sq` afterwards cannot change a value already
       computed. Short landscape declared four of them that way and its partner board wore the
       left board's furniture: a 50px strip on a 32.67px square, and a 200px pocket on a board
       261px wide. Declared per seat, each board's furniture follows its own square. */
    .own-seat {
        --bug-seat-sq: var(--bug-tall-sq-a);
    }
    .partner-seat {
        --bug-seat-sq: var(--bug-tall-sq-b);
    }
    .own-seat, .partner-seat {
        --bug-strip-h: var(--bug-seat-sq);
        --bug-clock-fs: calc(var(--bug-seat-sq) * 0.2);
        /* No floor. The floor made the name the one part of the furniture that did NOT scale,
           which is the opposite of what these modes are for. */
        --bug-name-fs: calc(var(--bug-seat-sq) * 0.218);
        --bug-pocket-sq: calc(var(--bug-seat-sq) * 0.8);
    }

    /* THE APP IS AS TALL AS ITS CONTENT, in both landscape modes. `toolsPlacement` publishes the
       taller stack plus whatever zone B holds, capped at the budget; without it the app grows to
       whatever its rows want and the page scrolls, which neither mode may do. Measured in short
       landscape before this was shared: rows totalling 739 in a 503px viewport. */
    :is(.round-app, .analysis-app).bug,
    body[data-variant='bughouse'] main.round.bug {
        height: var(--bug-app-content-h, 100%);
    }
    .round-app.bug .bug-right-column {
        display: contents;
    }

    /* WHICH SLOT EACH PART OCCUPIES BESIDE THE BOARD, top to bottom: chat, the two preset
       rows, the tab bar. These win over the unscoped `.bug-parts > ...` assignments further
       down the file — the ones short landscape and portrait still use — on specificity, so
       both forms can coexist without either being weakened.

       The chat's slot takes every tab panel: only the selected one is displayed, so they
       cannot overlap, and whichever is showing gets the row that absorbs the slack. */
    .round-app.bug .bug-parts > [role='tabpanel'] {
        grid-area: zoneTools1;
    }
    /* The first preset row and the end-of-game controls share a slot and never coexist —
       the presets while the game is on, the controls once it is not. */
    .round-app.bug .bug-parts .chatpresets-panel-1,
    .round-app.bug .bug-parts > .bug-gameover {
        grid-area: zoneTools2;
    }
    .round-app.bug .bug-parts .chatpresets-panel-2 {
        grid-area: zoneTools3;
    }
    .round-app.bug .bug-parts > .bug-round-tools-bar {
        grid-area: zoneTools4;
    }

    /* THE DROPPED ARRANGEMENTS, in the order parts leave the strip beside the board: the tab
       bar first, then the presets from the bottom up. A dropped part's row widens to zoneAN —
       the partner board's column plus the tools — which is exactly the span it had before
       this page was flattened, so nothing about the dropped look changes here.

       The classes are on the APP rather than on `.bug-right-column` because the element whose
       template they swap has to be the element they sit on, and that is the app now.
       toolsPlacement.ts puts them wherever it measures, which is the same element by
       construction — see the note there about `display: contents` measuring zero. */
    .round-app.bug.drop-tablist {
        grid-template-areas: var(--bug-zones-a4);
    }
    .round-app.bug.drop-tablist.drop-p2 {
        grid-template-areas: var(--bug-zones-a34);
    }
    .round-app.bug.drop-tablist.drop-p2.drop-p1 {
        grid-template-areas: var(--bug-zones-a234);
    }
    .round-app.bug.drop-tablist .bug-parts > .bug-round-tools-bar {
        grid-area: zoneA4;
    }

    /* ZONE B FILLS FROM THE BOTTOM, TAB BAR FIRST.

       The bar is the cheapest thing to move — one row of 40px against the presets' two — so it
       is what zone B can afford first, and toolsPlacement.ts only offers the presets the zone
       once the bar is already in it. The bar takes the LOWER slot and the presets land above
       it, which leaves the bar where it has always been: last, under everything.

       What this replaces looked like a swap: the bar dropped into zone A, beside the partner
       board, while the presets jumped past it to the full width below — so on the way down the
       two traded places instead of filling one after the other. */
    .round-app.bug.drop-tablist-b .bug-parts > .bug-round-tools-bar {
        grid-area: zoneB2;
    }

    /* The presets still in zone A while the bar has already gone to zone B.

       THE BAR'S EMPTIED SLOT MOVES UP, above the zone A rows rather than below them, and that
       is not cosmetic: a named area must be a single filled RECTANGLE, so leaving `zoneTools4`
       at the bottom put `stack` in rows 1-2 and again in row 4 with a spanning row between.
       Non-rectangular, so the whole declaration was invalid and dropped — the default template
       applied, `zoneA3` named nothing, its panel fell into an implicit column, and partsWidth
       read that trailing 0px track as the tools width. The button then shrank to its floor,
       which made zone B look affordable, which moved the presets, which made it unaffordable
       again: the layout oscillated between the two every frame.

       The row is empty either way — the bar is in zone B — so putting it above costs nothing
       and keeps every area a rectangle. */
    .round-app.bug.drop-tablist-b.drop-p2 {
        grid-template-areas: var(--bug-zones-a3);
    }
    .round-app.bug.drop-tablist-b.drop-p2.drop-p1 {
        grid-template-areas: var(--bug-zones-a23);
    }
    .round-app.bug.drop-tablist-b.drop-p2 .bug-parts .chatpresets-panel-2 {
        grid-area: zoneA3;
    }
    .round-app.bug.drop-tablist-b.drop-p2.drop-p1 .bug-parts .chatpresets-panel-1,
    .round-app.bug.drop-tablist-b.drop-p2.drop-p1 .bug-parts > .bug-gameover {
        grid-area: zoneA2;
    }
    .round-app.bug.drop-tablist.drop-p2 .bug-parts .chatpresets-panel-2 {
        grid-area: zoneA3;
    }
    .round-app.bug.drop-tablist.drop-p2.drop-p1 .bug-parts .chatpresets-panel-1,
    .round-app.bug.drop-tablist.drop-p2.drop-p1 .bug-parts > .bug-gameover {
        grid-area: zoneA2;
    }

    /* THE TWO-COLUMN HOMES. The drop classes above arrange the tools INSIDE their column; these
       arrange them when there is no column — see `toolsHome()` in squareUnit.ts, which chooses
       between them from the viewport before either board is sized. `toolsPlacement` clears the
       drop classes outside `tools-beside`, so the two sets of rules never apply at once. */
    .round-app.bug.tools-below {
        grid-template-areas: var(--bug-zones-below);
        /* THE BOARD ROW IS ITS CONTENT, and the band takes what is left.
           `calc(var(--bug-tall-sq-a) * 10)` was tried and is wrong: a stack is ten rows of board
           and pocket PLUS whatever its seat strips take, and `own-name-outside` gives the username
           a line of its own. Measured at 627x835 with a 40px square: the row came to 400 and the
           stack to 457, so the stack was drawn 57px over the band and the chat was behind the
           clock. The ten-row figure is what the boards are SIZED from, not what they occupy.

           The band is therefore the slack — `TOOLS_MIN_ROWS` squares at full zoom, by construction,
           and more once a board is zoomed down, since the boards give back what they stop using. */
        grid-template-rows: min-content min-content minmax(0, 1fr) min-content min-content min-content;
    }
    .round-app.bug.tools-below .bug-parts > [role='tabpanel'] {
        grid-area: zoneB1;
    }
    .round-app.bug.tools-below .bug-parts .chatpresets-panel-1,
    .round-app.bug.tools-below .bug-parts > .bug-gameover {
        grid-area: zoneB2;
    }
    /* THE END-OF-GAME CONTROLS PREFER ZONE A when it can hold them: a fixed block of buttons
       neither needs the full width nor loses anything in a narrow column, and moving it out of
       zone B leaves the panel below the rest of the band. `toolsPlacement` measures zone A —
       beside the own board, under the partner's — and sets this class when the block fits. */
    .round-app.bug.tools-below.drop-gameover-a .bug-parts > .bug-gameover {
        grid-area: zoneA2;
    }
    .round-app.bug.tools-below .bug-parts .chatpresets-panel-2 {
        grid-area: zoneB3;
    }
    .round-app.bug.tools-below .bug-parts > .bug-round-tools-bar {
        grid-area: zoneB4;
    }

    .round-app.bug:is(.tools-zonea, .tools-lastresort) {
        grid-template-areas: var(--bug-zones-zonea);
        /* THE PANEL'S ROW TAKES THE SLACK, as `zoneTools1` does beside the boards. With every part
           row `auto`, none of them absorbs it and the own stack — which spans all five rows —
           distributes the leftover among them, EMPTY ONES INCLUDED: measured on the analysis page,
           rows 2-5 came to 253px while the panel and the tab strip used 144, the other 109 going
           to two rows with nothing in them. */
        /* THE PART ROWS SHARE ZONE A rather than each taking its content's height. `min-content`
           rows let every part draw at whatever size its width implied and the region simply
           overflowed — 220px of panels in 173px of zone A. Sharing, each row has a definite
           height, which is the other half of letting a button be limited by height. */
        grid-template-rows: min-content minmax(0, 1fr) minmax(0, auto) minmax(0, auto) min-content min-content min-content;
    }
    .round-app.bug.tools-zonea .bug-parts > [role='tabpanel'] {
        grid-area: zoneA2;
    }

    /* THE LAST RESORT: THE TOOLS TAKE THE BOARD'S COLUMN, AND THE BOARD TAKES ITS TURN IN IT.
       ------------------------------------------------------------------------------------
       Reached only where nothing else fits — no column beside the boards, no band below them, and
       a zone A too small to hold a part. All that is left is the right board's own column, so the
       tools go there and the partner's board becomes a TAB: the strip guarantees exactly one of
       them is displayed, which is what lets them share one area without a rule about who wins.
       NO NEW AREA IS DECLARED, only a new occupant for `stack`. The template and rows are zone
       A's, shared above, because the tab strip still needs a zone A to sit in — and `toolsHome()`
       will not choose this home unless there is one. */
    .round-app.bug.tools-lastresort .bug-parts > [role='tabpanel'],
    .round-app.bug.tools-lastresort .bug-parts .chatpresets-panel-1,
    .round-app.bug.tools-lastresort .bug-parts .chatpresets-panel-2,
    .round-app.bug.tools-lastresort .bug-parts > .bug-presets-group,
    .round-app.bug.tools-lastresort .bug-parts > .bug-gameover {
        grid-area: stack;
    }
    /* The strip itself is the one thing that must stay visible while any of them is, so it is the
       one thing that does NOT share the column. */
    .round-app.bug.tools-lastresort .bug-parts > .bug-round-tools-bar,
    .round-app.bug.tools-lastresort .bug-parts > [role='tablist'] {
        grid-area: zoneA2;
    }

    .round-app.bug.tools-zonea .bug-parts .chatpresets-panel-1,
    .round-app.bug.tools-zonea .bug-parts > .bug-gameover {
        grid-area: zoneA3;
    }
    .round-app.bug.tools-zonea .bug-parts .chatpresets-panel-2 {
        grid-area: zoneA4;
    }
    .round-app.bug.tools-zonea .bug-parts > .bug-round-tools-bar {
        grid-area: zoneA5;
    }
    .round-app.bug.tools-zonea.strip-in-zoneb .bug-parts > .bug-round-tools-bar {
        grid-area: zoneB1;
    }
}


.bug rating {
    display: none;
}

/* What `.bug-round-tools` used to carry, now carried by the column itself.
   That element is gone: it was a grid item pinned to its own track, and a block
   formatting context besides, so nothing inside it could ever reach the space
   under a shrunken board. The parts are placed individually instead.

   The zero minimums are what let this column be driven to nothing: a flex or grid
   item's automatic minimum size would otherwise hold it open, and this column
   yielding is precisely what keeps both boards on screen at narrow widths. */
/* THE MERGED COLUMN, stated once for every mode.
   The partner board's stack and the tools' parts share this container. What the
   modes disagree about is only how wide its two columns are and what separates
   them, so that is all each mode's block still carries.

   The zero minimums are what let this column be driven to nothing: a grid item's
   automatic minimum size would otherwise hold it open, and this column yielding is
   precisely what keeps both boards on screen at narrow widths. */
.bug-right-column {
    grid-area: rightcol;
    display: grid;
    grid-template-rows: minmax(0, 1fr) auto auto auto;
    grid-template-areas: 'stack chat'
                         'stack p1'
                         'stack p2'
                         'stack tablist';
    row-gap: 0;
    overflow: hidden;
    min-width: 0;
    min-height: 0;
}

/* The stack takes its content height, not the height of the rows it spans.
   Stretched — the default for a grid item — it measured the full column at every
   zoom, so the space a smaller board frees never existed and nothing ever dropped. */
.bug-partner-stack {
    grid-area: stack;
    align-self: start;
}

/* The parts form no box of their own here: each is placed individually by the
   column, which is what lets one of them take the space under a shrunken board.
   Portrait is the exception and re-states this. */
.bug-parts {
    display: contents;
}

/* The right board and its two strips as one unit. Block flow stacks them —
   strip, board, strip — which is all the arrangement they need; the strips are
   themselves flex rows and the board is a block. The group exists so the board
   can be ONE thing for the parts to arrange around: three siblings floated
   individually would line up side by side instead of stacking. */
.bug-partner-stack > .seat-strip0,
.bug-partner-stack > .seat-strip1,
.bug-partner-stack > selection {
    width: 100%;
}

/* `.bug-offer-dialog` used to be here, holding #offer-dialog in a `toolsB` row that
   spanned both columns below the boards. It was the last survivor of an element that
   had already been renamed once for outliving its job — it was `.bug-round-tools-part`
   when it wrapped the movelist and the rest of the tools — and it has now outlived the
   second one. An offer is a look on the control that made it: the draw button turns
   green to be accepted, the rematch button is replaced in place by an accept/decline
   pair. Nothing was left for a strip to hold, so the element, its row, and the `toolsB`
   area in all three templates went together.
   THE LOOKS THEMSELVES are further down, beside the buttons they belong to: search
   `#draw.draw-offered`, `#resign.resign-confirm` and `button.rematch.accept`. */

/* The tools column IS the tab widget (client/two-board/common/tabs.ts), not a box
   around one, so `.bug-round-tools` above still supplies grid-area and the flex
   column; only what the widget itself needs is added here.

   Sizing comes from the container in both axes. The zero minimums are what let
   the column be driven to nothing: a flex item's automatic minimum size would
   otherwise hold the widget, its panels and its labels open, and this column
   yielding is precisely what keeps both boards on screen at narrow widths.

   Nothing here sets a height. `site.css` gives every tabpanel
   `height: var(--panel-height)`, but that variable is defined only in
   `analysis.css`, which this page does not load — the declaration is therefore
   invalid at computed-value time and height falls back to `auto`, which is what
   this layout wants. If a fixed height is ever wanted here, set the variable;
   do not change the rule in site.css, which the analysis page depends on.

   The movelist block used to be pinned to board B's height by a rule on the element
   that ended its life as `.bug-offer-dialog` (now removed entirely), back when that
   element wrapped the tools panel's parts. That selector no longer matches now that the movelist is a
   panel's child, and a fixed height is the opposite of what a panel wants, so it
   is gone; `main.round.bug .movelist-block`'s `flex: 1` still applies and fills
   the panel. */
/* `.bug-round-tools` is the page's own element (round.ts) and carries the column
   itself; the widget contributes only #round-tabs-tablist and one panel per tab
   part inside it. Keyed by class rather than by an id, so `round-tabs` names one
   thing: the widget's id prefix. */
/* The bar shares one row between the tablist and the game controls. It takes its
   natural height in the column, and clips: once the tablist has given up all it
   can and the controls still do not fit, the overflow has to be cut off here
   rather than widening the grid — this column yielding is what keeps both boards
   on screen, and nothing added to it may reverse that. */
.bug-round-tools-bar {
    flex: 0 0 auto;
    display: flex;
    /* Wraps rather than squeezing. Side by side is a wide-column arrangement; in a
       narrow column the controls' fixed width would eat most of the row and leave
       the tablist unreadable. Measured on a 697x382 phone landscape, where the
       column is 74.33px: sharing the row gave the tablist 23.67px — 7.89px per
       label — where wrapping gives it the full 74.33 and 24.77 per label, which is
       what mobile had before the controls moved here. The second row costs 40px of
       the column's height, which the panel gives up.

       Content-driven rather than a breakpoint: the tablist's flex-basis below is
       what decides. Where the column can hold that basis plus the controls they
       stay on one line; where it cannot, the controls wrap beneath. */
    flex-flow: row wrap;
    align-items: center;
    min-width: 0;
    overflow: hidden;
}

/* In the bar the tablist is the part that yields: it takes the width the controls
   leave, and `min-width: 0` lets it shrink past its labels, which already clip.
   The 120px basis is the width below which three labels stop being readable, and
   is therefore also the point at which the bar wraps instead. */
#round-tabs-tablist {
    flex: 1 1 120px;
    min-width: 0;
}

/* The controls neither grow into spare width nor shrink under pressure. At the
   narrowest widths they are clipped by the bar at their natural size — a cut-off
   button is still recognisable and still clickable where it is drawn, where one
   scaled to a few pixels would be neither. */
.bug-round-tools-bar > #game-controls,
.bug-round-tools-bar > .btn-controls {
    flex: 0 0 auto;
}

/* Each button is two thirds of a board square rather than the width of its glyph,
   which left draw at 20px and resign at 29px. These are targets a player reaches
   for under time pressure, so the size is stated rather than inherited from a
   symbol. `site.css` gives them `flex: 1`, which would share the row instead —
   overridden here so the pair is a fixed 2 x (2/3) squares wide.

   The square is taken from the board chessgroundx actually rendered, not from
   --bug-sq. Those agree in short landscape — both were 54.67 when measured —
   because that mode sizes its boards as 8 x --bug-sq. They do not agree anywhere
   else: --bug-sq is defined as a tenth of the viewport height, and the
   `min-height: 600px` mode sizes its boards from 30vw instead, where --bug-sq
   measured 91.33 against a real square of 40.67. Buttons keyed to --bug-sq came
   out 60.89 wide there — 2.25x too large, taking 60% of a 203px column. */
.bug-round-tools-bar .btn-controls button {
    /* THE ICON CARRIES THE STATED WIDTH, not the button. The button used to be
       `flex: 0 0 calc(var(--bug-own-sq) * 2 / 3)` and that is still exactly what it comes
       out as while the label is hidden — the label is then zero wide and the icon is the
       whole content. What moving it inwards buys is that a shown label makes the button
       wider by the label's width and by NOTHING ELSE, which is what lets the fit test in
       toolsPlacement.ts subtract it back off and ask the same question in both states. A
       button that also changed its padding would answer differently depending on its own
       answer, which is the shape that had this page flickering. */
    flex: 0 0 auto;
    min-width: 0;
    /* A `button` does not inherit the page's font — the UA gives it its own, which came out
       13.33px Arial against the tabs' 14px system stack sitting right beside it. The label has
       to read as one of the row's words, not as a control's caption, so the font comes from
       the bar like everything else in it. */
    font: inherit;
    /* THE ICON AND ITS LABEL CENTRE ON EACH OTHER, which inline layout cannot do here.

       The button's line box is set by a 25.2px glyph, and an inline-block `<i>` takes that
       glyph's baseline as its own — so `vertical-align: middle` on the label aligned it to a
       baseline sitting near the bottom of the icon's box rather than to the box. Measured in
       tall landscape: the icon centred on the bar's centre line at 706.7 and the label at
       711.9, while the tab text it shares the row with sat at 705.8. Six pixels is not much
       to look at and is plainly wrong to read — the row has one line of words in it and one
       of them was low.

       A flex row centres the two on their boxes, and the button being the bar's full height
       then puts the label on the same centre line as the tabs. Nothing here distributes free
       space: the button is `flex: 0 0 auto`, so it is exactly as wide as the icon and label
       make it. */
    display: flex;
    align-items: center;
}

/* --bug-own-sq, not --cg-width-a: the latter is board A whoever is playing on it, so a
   viewer whose own board is B sized these from the PARTNER's board. Measured in portrait
   with the boards that way round: two thirds of the partner's 20.7px square gave 13.8px
   buttons — unusable, and the reason they looked starved of space next to a tab list that
   had taken 339.8 of 367.3.

   The flag glyph is wider than this box, as it has always been — 28.79 against 27.11 — and
   still overhangs it rather than widening the pair unequally. */
.bug-round-tools-bar .btn-controls button > i {
    display: inline-block;
    width: calc(var(--bug-own-sq) * 2 / 3);
    text-align: center;
}

/* WHETHER LABELS ARE ON OFFER AT ALL, published for toolsPlacement.ts to read rather than
   worked out a second time there.

   PORTRAIT NEVER LABELS THEM. The bar is a phone's width, and the room the fit test finds
   is room the row has better uses for — the two icons are understood without captions, and
   the words would be the first thing in the way when anything else has to go in that row.
   That is a decision about the mode, so it is stated here, where the mode already lives,
   and not as arithmetic that happens to come out against them at these widths.

   Read rather than re-derived, for the same reason `owner()` asks the element how it is
   displayed instead of asking which mode is on: one call site works in every mode and
   cannot disagree with the stylesheet about which of them is in force. */
.bug-round-tools-bar {
    --bug-controls-labels: 1;
}
@media (aspect-ratio <= 9/16) {
    .bug-round-tools-bar {
        --bug-controls-labels: 0;
    }
}

/* THE LABEL, PRESENT BUT UNMEASURED-WIDTH until there is room for both.
   ------------------------------------------------------------------------------------
   Hidden by width rather than by `display: none`, because the decision needs to know what
   it WOULD take: a `display: none` element reports a zero `scrollWidth` and the question
   could never be asked. Clipped to nothing, it still reports the width it wants.

   The leading space is padding rather than margin so that it lives INSIDE the clipped box:
   as margin it would widen the button by 0.35em even while the label showed nothing, and
   the two states would then differ by more than the label itself. */
.bug-round-tools-bar .control-label {
    /* inline-block, because an INLINE element reports `scrollWidth: 0` whatever it contains —
       so the width the label wants, which is the whole basis of the decision, read as nothing
       and the labels showed wherever they were asked to. */
    display: inline-block;
    max-width: 0;
    overflow: hidden;
    white-space: nowrap;
}

/* BOTH SPACES ARE INLINE CONTENT, not padding, and that is the whole point of them being
   pseudo-elements: the leading one separates the label from its icon, the trailing one
   separates the pair of buttons.

   PADDING CANNOT BE CLIPPED. It was padding first, and `max-width: 0` does not remove it —
   the used width becomes the padding's own 17.49px, the clip region is the padding box, and
   the text starts after the leading padding: 12.6px of `Draw` showed through a label that was
   supposed to be hidden, which is exactly the trailing padding's width. Resign, having none,
   stayed invisible and hid the bug.

   As content they are inside the box that `max-width: 0` collapses, so they cost nothing at
   all when the label is hidden — and they are inside the width the fit test measures, so the
   space between the buttons is accounted for without the test having to know it exists. As
   margin, or as a `column-gap` on the strip, they would be neither: still there while the
   labels were hidden, and a term the calculation would have to add by hand. */
.bug-round-tools-bar .control-label::before {
    content: '';
    display: inline-block;
    width: 0.35em;
}
/* Trailing space only where there is another button after it — the last one is against the
   end of the bar and wants nothing there. */
.bug-round-tools-bar .btn-controls button:not(:last-child) .control-label::after {
    content: '';
    display: inline-block;
    width: 0.9em;
}
/* No `.round-app.bug` prefix: the class goes on whichever element owns the arrangement, which
   is the app where the column has been flattened and the column itself everywhere else — so a
   selector naming one of them silently misses in the other. `.bug-round-tools-bar` in the
   selector is what scopes this to bughouse; the owner is not. */
.controls-labelled .bug-round-tools-bar .control-label {
    max-width: none;
}

/* The open panel takes the height the tablist bar leaves. This used to be the job
   of a #round-tabs-tabpanels element between the column and the panels, which the
   widget no longer builds: a tab's parts are mounted individually so that a later
   layout can put one of them somewhere else entirely. `.bug-round-tools` is
   already `display: flex; flex-flow: column`, so the panels are now its direct
   flex items and claim that height themselves — only one is ever displayed, so
   only one ever claims it.

   The zero minimums are not tidiness. A flex item's automatic minimum size would
   hold the panel, its content and its labels open, and this column being able to
   yield to nothing is precisely what keeps both boards on screen at narrow
   widths. */
.bug-parts > [role='tabpanel'] {
    flex: 1 1 auto;
    min-width: 0;
    min-height: 0;
    /* both axes stated together: giving only overflow-y would compute overflow-x
       from visible to auto, and the panel would grow a horizontal scrollbar as
       soon as its content is wider than the column — which the Info panel's
       game-info is, at roughly 170px of min-content */
    overflow: hidden auto;
}

#round-tabs-tablist > [role='tab'] {
    min-width: 0;
    overflow: hidden;
    white-space: nowrap;
}

/* *****************************************************************************************
 * extensions.css
 ******************************************************************************************* */
main.round.bug .pocket.top {
  margin-bottom: unset;
}
main.round.bug .pocket.bottom {
  margin-top: unset;
}

/* THE PLAYER'S OWN BOARD SQUARE, as a value CSS can use. --cg-width-a and -b are
   board IDENTITY, and which of them is the viewer's own depends on the switch —
   the same role-versus-identity problem the pockets had. markRoles() already puts
   `own-board` on whichever board that is, so `:has()` turns that class into a
   value here, without JavaScript measuring anything or a third source of truth.
   It stays correct through a switch because the class does. */
.round-app.bug:has(#mainboard.own-board) {
  --bug-own-sq: calc(var(--cg-width-a) / var(--files));
}
.round-app.bug:has(#bugboard.own-board) {
  --bug-own-sq: calc(var(--cg-width-b) / var(--files));
}

/* The group carries the button floor as well as the part does. Custom properties inherit
   downwards and the group sits ABOVE the parts, so a floor declared only on `.chatpresets` is
   invisible to it — which makes every expression built from it invalid there. Not
   hypothetical: the panels' flex basis in zone B is stated in buttons, and without this it
   computed to nothing and drew 0px panels with their buttons hanging out to the left. */
.chatpresets,
.bug-presets-group {
  /* The size a preset button actually draws at. The floor below is the minimum; a
     mode that has room may raise it. Kept as its own variable so that raising it is
     a one-line statement per mode rather than a rewrite of the track. */
  /* NOT DECLARED HERE ANY MORE. `--bug-preset-btn` is published on the APP by
     `publishPresetSize()`, and a declaration on this element — a descendant — would shadow it for
     everything inside. The floor below is the fallback the tracks use until that first publish,
     which is also what they used before there was anything to publish. */
  /* A preset button had no floor at all: `width: 60%` of a cell whose track was
     `auto`, so it was whatever the column made it — measured 45.92px on the
     desktop and 13.23px in portrait, against WCAG 2.2's 24px minimum target.
     0.55 of the player's own board square reproduces roughly 60% of the desktop
     size (27.5px) while scaling with the page rather than being a per-mode
     constant: 26.4px in portrait, 30.1 in short landscape, 32.5 on the desktop.
     The track floor is the button floor divided by the 60% the button occupies. */
  --bug-preset-btn-min: calc(var(--bug-own-sq) * 0.55);
  /* AND A CEILING: A PRESET PIECE IS NEVER DRAWN LARGER THAN THE PIECE ON THE BOARD.
     That is the whole rule, and it is why the number is 1 and not a matter of taste. A preset
     button is a picture of a piece the player is asking their partner for; drawn bigger than the
     real one it stops reading as a reference to the board and starts competing with it. Measured
     on a 2495px-wide window: 97.4px buttons against a 67.6px square, 1.44 times the size.
     Sizing from spare HEIGHT is what makes a ceiling necessary at all. The old cap of 0.7 squares
     existed to stop two sets in areas of different widths from drawing at different sizes; that
     job now belongs to `publishPresetSize()`, which publishes ONE size for every set, and the cap
     was left declared but wired to nothing — 0.7 was also below what a comfortable window already
     produced, which is why it read as a maximum that fought the layout rather than bounding it.
     This one binds in one place only: the widest window. Everywhere else the height runs out
     first, or the floor wins. */
  --bug-preset-btn-max: calc(var(--bug-own-sq) * 1);
  /* THE SMALLEST GAP BETWEEN TWO BUTTONS. The gap actually drawn is published on the column by
     `publishPresetGap()` — this is the floor it can never go under, and the value every row is
     WRAPPED at, which is what keeps the wrap from depending on the gap the wrap produces. */
  --bug-preset-gap-min: 3px;
  /* The spacing between two ROWS of buttons, which is a different quantity from the pitch
     above and deliberately does not follow it. It is what has always separated one preset
     part from the next — the padding below — so making every vertical gap this value is
     what lets four stacked rows read as four evenly spaced rows. */
  --bug-preset-row-gap: 5px;

  /* The part holds two sets and lets them wrap against each other: side by side
     when it is wide enough for both, stacked when it is not. Nothing computes a
     width — the wrap decides, so a part that drops into the width under the board
     shows its ten buttons on one row and the same part beside the board shows the
     two rows of five it has always shown. */
  display: flex;
  flex-wrap: wrap;
  /* HORIZONTALLY, THE SAME GAP THE SETS USE INTERNALLY, so a row is spaced identically
     wherever the boundary between two buttons happens to fall.

     This box had no gap at all until now, in any mode, which meant two sets sharing a row
     ABUTTED: measured as a gap of 0 where every other pair on that row was held 53.5px
     apart on the desktop. Portrait had the same defect at 0 against 3 and simply got away
     with it.

     VERTICALLY IT IS THE OTHER VALUE, and the axes must not share one. The pitch is a
     horizontal quantity — it exists so that buttons on a row line up with buttons on
     another row — and applying it between ROWS makes the two sets inside a part sit
     further apart than two parts sit from each other: measured 38.27 between rows 1-2 and
     3-4 against 5 between rows 2-3, so four stacked rows read as two pairs rather than as
     four evenly spaced rows.

     The 5px is what already separates one part from the next, so it is the value every
     vertical gap should be. Named rather than repeated, and used for the padding as well,
     so the gap between two rows of one part and the gap between two parts cannot drift
     apart again. */
  column-gap: var(--bug-preset-gap, var(--bug-preset-gap-min));
  row-gap: var(--bug-preset-row-gap);
  /* WHERE A ROW'S LEFTOVER GOES, published by `publishPresetGap()`: centred while every row holds
     the same number of buttons, right-aligned as soon as they do not. A row only has a leftover
     when it lost the gap vote to a shorter row — see the gap rule — and that is exactly the case
     where the two have to line up. Centred is the fallback because a lone block is the common
     shape. */
  justify-content: var(--bug-preset-align, center);
  padding-top: var(--bug-preset-row-gap);
}


/* WHAT THE CHAT IS OWED, IN LINES — the unit it is actually made of.
   ------------------------------------------------------------------------------------
   The chat reported `min-height: 210px` and this file said nothing about it. That was NOT a flex
   automatic minimum, as first assumed: `site.css` carries `.chat { min-height: 15em }` and this
   element is `class="bugroundchat chat"`, so 210px was 15em at a 14px font. A site-wide figure in
   ems of whatever font the chat happens to have, for a box whose height here is decided by a grid
   row.

   `publishPresetSize()` has to know this number: the preset buttons grow into the height the
   tools have SPARE, and spare means "after the chat". Left undeclared, that code carried its own
   figure — three squares of the viewer's board — which disagreed with the real one and was
   SMALLER: 150px reserved on a window whose chat already wanted 210, so the buttons could grow
   into 60px the chat would take straight back.

   Lines, not squares. A chat is legible at a number of lines of text plus its input; it does not
   become more readable because the board got bigger, and a figure in squares gave 200px on one
   window and 150px on another for content that needs the same in both. This is the opposite call
   from `TOOLS_MIN_SQUARES`, deliberately: a preset button has a visual relationship to a board
   square and a paragraph of chat does not.

   A LINE IS A MESSAGE, AND A MESSAGE IS NOT `1lh`. The first attempt reserved
   `6 * 1lh + 2.5rem` on `.bugroundchat` and both terms were wrong. `1lh` resolved against the
   chat's own font — 15.96px — while a rendered `li.message` box measures 22px and advances 27px:
   it has a smaller font of its own (`max(0.9em, 12px)`), 6px of padding under the text, and 5px
   of margin to the next one, and its `line-height` was `normal`, so nothing anywhere said what a
   line was. The input was guessed at `2.5rem` = 40px and is 25px. Reserved 135.7px bought 5.05
   lines and 15px of nothing, which is why the total looked right and the chat did not.

   So the parts are declared HERE and consumed BOTH by the message rule and by the reservation
   below, which is what stops them drifting apart again. The input is not declared at all: it is a
   real element of a definite height, so the code MEASURES it rather than restating it — a guessed
   constant was exactly the bug. */
.bugroundchat {
    --bug-chat-min-lines: 4;
    /* `max(0.9em, 12px)` is what `li.message` already used; repeated here because the advance
       below has to be computed from the same number. Both uses resolve `em` against the list's
       font, so they cannot disagree. */
    --bug-chat-msg-fs: max(0.9em, 12px);
    --bug-chat-msg-lh: 1.35;
    --bug-chat-msg-pad-b: 6px;
    --bug-chat-msg-gap: 5px;
    /* What one message actually occupies, top of one to top of the next. */
    --bug-chat-msg-advance: calc(
        var(--bug-chat-msg-fs) * var(--bug-chat-msg-lh) + var(--bug-chat-msg-pad-b) +
            var(--bug-chat-msg-gap)
    );
    /* AND THE SITE-WIDE 15em IS REFUSED. `site.css .chat` forces a minimum of 15em — 192.3px at
       this font — on a box that here is a grid item in a row the layout has already sized. A
       minimum a row cannot grant does not shrink the row, it OVERFLOWS it: measured 192px of chat
       in a 133px zone A row and 191px in a 129px zone B row, the bottom 59-62px drawn beneath the
       preset rows below, with `elementsFromPoint` at the chat input returning `.chatpresets`. The
       input could not be clicked at all in either window.
       Zero, not a smaller number: the row is the authority on how tall the chat is, and the list
       inside it already scrolls. What four messages are worth is a claim on the SPARE height, made
       in `publishPresetSize()` from the numbers above — a budget, never a floor on the box. */
    min-height: 0;
}

/* Declared, so `--bug-chat-msg-advance` above describes what is really drawn. `margin-top` goes to
   zero because adjacent margins collapse: 5px above and 5px below did not add 10px between two
   messages, so counting either would have been wrong. */
.bugroundchat li.message {
    font-size: var(--bug-chat-msg-fs);
    line-height: var(--bug-chat-msg-lh);
    padding-bottom: var(--bug-chat-msg-pad-b);
    margin-top: 0;
    margin-bottom: var(--bug-chat-msg-gap);
}

/* AND IT IS NOT A `min-height`. Nothing here forces a height, deliberately.

   It was `min-height: 4 * advance` on the list, and a minimum the row cannot grant does not shrink
   the row — it OVERFLOWS it. Measured on two windows: a chat of 191px in a zone B row of 129px and
   192px in one of 133px, its bottom 59-62px drawn under the preset rows that follow, with
   `elementsFromPoint` at the chat input returning `.chatpresets`. The input could not be clicked at
   all.

   What the four messages are FOR is budgeting: `publishPresetSize()` must not hand the presets
   height the chat needs. That is a claim on the SPARE height, not a floor on the rendered box, so
   it belongs in the numbers above — which the code reads — and nowhere in the box model. Where the
   row cannot seat four messages the list simply scrolls, as a chat log should. */

/* The chain that carries a bounded height down to the buttons: a panel is given a definite height
   by its grid row, `.chatpresets` fills it, and the sets share it. Every link needs `min-height: 0`
   or the flex default — never shrink below content — reasserts the height the row was meant to
   cap. */
.chatpresets-panel > .chatpresets {
  height: 100%;
  min-height: 0;
  align-content: stretch;
}

/* A set is five buttons that never break apart, and it is PIECE-ALIGNED: column i
   is piece i, so "need a knight" sits directly above "don't give a knight"
   whenever the two sets are stacked. That is why the count is --setColumns and not
   a number of columns that happen to fit — an `auto-fit` track count would have
   given the desktop seven columns and destroyed the correspondence.

   Each track is at least the button floor divided by the 60% the button occupies,
   so a track can grow but never squeeze a button below usable. That floor is also
   what decides the wrap: a set cannot shrink below five floored tracks, so two of
   them share a row only where there is room for both at full size. */
.chatpresets-set {
  display: grid;
  /* A track IS a button, and the spacing between them is a gap. The tracks used to
     be `minmax(button / 0.6, 1fr)` with the button at 60% of the track, which put
     the spacing INSIDE each cell: 24.9px buttons in 41.6px tracks, a third of the
     row spent on nothing. It also made the size depend on how much room the row
     had, so a part that had dropped drew 41.6px buttons while its neighbour beside
     the board drew 24.9px ones — the same control at two sizes on one screen.
     Fixed tracks give one size everywhere and pack the row tightly. */
  /* A TRACK GROWS TO FILL, FLOORED AT THE BUTTON MINIMUM — which is what the paragraph above
     always described and what the fixed track never did. Pinned to `--bug-preset-btn`, a set
     was exactly as wide as that number said, and that number came from `--bug-parts-w`: a width
     published from JS, measured from an element, and fed back into the size of the element it
     measured. Three defects came out of that one loop in a single session — buttons at their
     floor beside a 219px panel they left 57px of, buttons at 73px in a 610px row that squeezed
     the chat to nothing, and an oscillation on a wide screen where each state published the
     other's width and the layout flipped between them every frame.

     `minmax(floor, 1fr)` needs none of it. A set is five tracks that cannot go below the floor
     and otherwise share what they are given, so the sets pair when two fit and stack when they
     do not — self-arranging in a 219px column, a 610px row or an 1100px one, with nothing
     published, nothing observed and nothing to go stale. */
  /* A FIXED SIZE, published from the HEIGHT the tools have spare — see `publishPresetSize()`.
     Tracks that flexed with the box were what let a button's size depend on where its part had
     been placed, and every oscillation today came through that door. A track is now the size, so
     a set that moves to a wider region keeps its buttons and simply re-wraps. */
  grid-template-columns: repeat(var(--setColumns), var(--bug-preset-btn, var(--bug-preset-btn-min)));
  /* NOTHING TO ALIGN HERE. The set is `flex: 0 0 auto` and its tracks are fixed, so it is exactly
     as wide as its five buttons and four gaps — measured internal slack, 0.0px. This carried
     `justify-content: flex-end` for a version in which the set could be wider than its content;
     it could not do anything any more, and the row's alignment now lives on `.chatpresets`, which
     is the box that actually has the leftover. */
  /* HEIGHT LIMITS THE BUTTON WHERE HEIGHT IS WHAT IS SCARCE. A track gives the button its width
     and `aspect-ratio` gives it its height, so with nothing else said the height simply follows
     the width and a set that has been given a short box overflows it — measured in zone A at
     682x503: 50px buttons wanting 220px of a 173px region. `justify-items` centres what is left
     in the track when the height binds, so the row stays piece-aligned rather than drifting. */
  align-items: center;
  justify-items: center;
  /* AND THE BOUND HAS TO REACH THE BUTTON. `max-height: 100%` on a button resolves against its
     grid row, and an auto row is sized BY the button — circular, so it constrains nothing: the
     panel was held to 67px while its buttons stayed 50px and drew straight through it. Rows that
     share the set's height give the percentage something real to resolve against, and the set
     takes its height from the panel rather than from its contents. */
  grid-auto-rows: minmax(0, 1fr);
  min-height: 0;
  /* AND THE SET ITSELF HAS TO BE STRETCHED, or the flexible tracks have nothing to share. A grid
     fills its own width; its own width comes from the flex row above it, where the default
     `flex: 0 1 auto` leaves it at min-content — five floored tracks — and the buttons sit at the
     floor however much room the row has.
     The basis is the set at its floor, which is also what decides the wrap: two sets share a row
     exactly when both fit at floor size, and stack otherwise. */
  /* NATURAL WIDTH, so the wrap is decided by what the set ACTUALLY measures. The basis used to be
     the set at its floor, which was right while the tracks could shrink to that floor — they
     cannot now, the size being fixed, so two sets at a 195px basis looked as though they fitted a
     499px row and then overflowed it at their real 499px each. Nothing grows and nothing shrinks:
     the size is settled before the wrap, and the wrap simply asks whether both sets fit. */
  flex: 0 0 auto;
  /* THE SAME PUBLISHED GAP INSIDE A SET AS BETWEEN TWO SETS, which is what makes a row of ten
     read as one row rather than two blocks of five: its nine gaps are one value. */
  gap: var(--bug-preset-gap, var(--bug-preset-gap-min));
  /* NOTHING IS PACKED TO AN EDGE ANY MORE. This was `justify-content: flex-end` with
     `flex: 0 1 auto`, a set at its natural width pushed to one side, and the spare width of the
     row showing as a margin beside it — 57px of a 219px panel, measured. With the tracks
     flexible and the set stretched, there is no spare width to push anywhere: the buttons take
     the row between them, and two sets still pair or stack exactly as the wrap decides, because
     the basis above is the set at its floor. */
}

/* The presets are their own tab part now, a sibling of the chat view rather than
   a child of it, so the `order: 3` that used to place them inside the chat view's
   flex box means nothing here. Chat takes the space that is going; the presets
   take the height their buttons need and no more, and do not scroll — the panel
   default of `flex: 1 1 auto` with `overflow: hidden auto` is right for chat and
   wrong for a fixed grid of buttons.

   `flex-flow: column` is not decoration. A panel is `display: flex`, so its child
   is laid out on the main axis — and a row's main axis is the width, which is
   sized from content. The buttons are `width: 60%` of their grid cell and the
   cells are `auto`, so content-sizing the grid is circular and it collapsed to
   zero: measured 0x0 buttons in a 5px panel. As a column's cross-axis item the
   grid stretches to the full width instead, which is exactly the relationship it
   had inside the chat view's own column before it was pulled out. */
/* ONE PANEL, WHEREVER THE PARTS END UP.
   ------------------------------------------------------------------------------------
   The buttons already carry the surface colour and their containers carried none, so the
   block read as a scatter of tiles on the page's ground rather than as one panel — and it
   read differently in each arrangement, because the arrangement decides how the tiles fall.
   Painting the containers instead makes the ground between the buttons part of the panel, so
   two rows beside the board, two rows under it and one row of twenty are all the same object.

   The GROUP is painted for the same reason and only pays for itself in one arrangement: it
   is `display: contents` everywhere else, and an element with no box paints nothing, so this
   declaration is simply inert until zone B gives it one. There it is what covers the gap
   between the two panels sharing a row — the join that the pitch needs and that would
   otherwise show the page through the middle of the panel. */
.chatpresets-panel,
.bug-presets-group {
    background-color: var(--bg-color0);
}

/* THE TWO PRESET ROWS AS ONE ITEM, and only where that is needed.

   Dissolved by default, in every mode: the panels are placed individually — one per tools
   row beside the board, or one per zone A row once they drop — and this element forms no box,
   so nothing about those arrangements knows it exists. That is why it can be added without
   touching portrait or short landscape.

   It becomes a real box in zone B alone, because that arrangement needs both panels to be a
   SINGLE grid item: a named area is one rectangle and holds one item, so a row of twenty
   buttons under both boards can only exist if the twenty are inside one box. */
.bug-presets-group {
    display: contents;
}

.bug-parts .chatpresets-panel {
  flex: 0 0 auto;
  flex-flow: column;
  overflow: visible;
}

/* Portrait deliberately has nothing here. It used to reverse this pair: master
   made `.bugroundchat.chat` a column-reverse box, and because the presets were
   that box's last child they came out on top. Nobody chose that placement — it
   fell out of the reversal, whose real subject was the chat's own input. When the
   presets moved out of the chat view the effect was restated as `order: -1` to
   keep the page looking the same, which preserved an accident.

   Both are gone, so where these sit is decided by where round.ts mounts them, in
   every layout alike: messages, input, presets underneath. A layout that wants
   them somewhere else should mount them somewhere else rather than reorder them
   in place — reordering in place is exactly what made portrait disagree. */

.chatpresets button.bugchat {
  /* fills its track, because the track is the button's size — but never taller than the row it
     is in: `aspect-ratio` alone makes height follow width, so a button in a short box grows past
     it, and `max-height` lets the ratio shrink the width instead. */
  width: 100%;
  max-height: 100%;
  /* aspect-ratio rather than `padding-top: 60%`: percentage padding resolves
     against the cell width, so it cannot honour a minimum — the box would keep
     its square shape by shrinking rather than by holding the floor. */
  aspect-ratio: 1 / 1;
  color: var(--font-color);
  background-color: var(--bg-color0);
  cursor: pointer;
  border: none;
}

.bugchatpointer {
  cursor: pointer;
}

li.message .bugchat{
  width: 2em;
  height: 2em;
  display: inline-flex;
  color: var(--font-color);
  background-color: var(--bg-color0);
  cursor: pointer;
  border: none;
}

button.bugchat:hover {
  color: #fff;
  background-color: var(--green-hover);
}

button.bugchat:focus {
  outline: none;
}
button.bugchat::-moz-focus-inner {
  border: 0;
}

.bugchat.p {
  background-image: url('images/bugroundchat/P.svg');
  background-size: cover;
}
.bugchat.n{
  background-image: url('images/bugroundchat/N.svg');
  background-size: cover;
}
.bugchat.b{
  background-image: url('images/bugroundchat/B.svg');
  background-size: cover;
}
.bugchat.r{
  background-image: url('images/bugroundchat/R.svg');
  background-size: cover;
}
.bugchat.q{
  background-image: url('images/bugroundchat/Q.svg');
  background-size: cover;
}

.bugchat.a {
  background-image: url('images/bugroundchat/Q.svg');
  background-size: cover;
}
.bugchat.c {
  background-image: url('images/bugroundchat/C.svg');
  background-size: cover;
}
.bugchat.m {
  background-image: url('images/bugroundchat/M.svg');
  background-size: cover;
}
.bugchat.s {
  background-image: url('images/bugroundchat/S.svg');
  background-size: cover;
}

.bugchat.nop{
  background-image: url('images/bugroundchat/DONT.svg'), url('images/bugroundchat/P.svg');
  background-size: cover;
}
.bugchat.non{
  background-image: url('images/bugroundchat/DONT.svg'), url('images/bugroundchat/N.svg');
  background-size: cover;
}
.bugchat.nob{
  background-image: url('images/bugroundchat/DONT.svg'), url('images/bugroundchat/B.svg');
  background-size: cover;
}
.bugchat.nor{
  background-image: url('images/bugroundchat/DONT.svg'), url('images/bugroundchat/R.svg');
  background-size: cover;
}
.bugchat.noq{
  background-image: url('images/bugroundchat/DONT.svg'), url('images/bugroundchat/Q.svg');
  background-size: cover;
}

.bugchat.noa{
  background-image: url('images/bugroundchat/DONT.svg'), url('images/bugroundchat/Q.svg');
  background-size: cover;
}
.bugchat.noc{
  background-image: url('images/bugroundchat/DONT.svg'), url('images/bugroundchat/C.svg');
  background-size: cover;
}
.bugchat.nom{
  background-image: url('images/bugroundchat/DONT.svg'), url('images/bugroundchat/M.svg');
  background-size: cover;
}
.bugchat.nos{
  background-image: url('images/bugroundchat/DONT.svg'), url('images/bugroundchat/S.svg');
  background-size: cover;
}

.bugchat.sit{
  background-image: url('images/bugroundchat/SIT.svg');
  background-size: cover;
}
.bugchat.go{
  background-image: url('images/bugroundchat/GO.svg');
  background-size: cover;
}
.bugchat.trade{
  background-image: url('images/bugroundchat/TRADE.svg');
  background-size: cover;
}
.bugchat.notrade{
  background-image: url('images/bugroundchat/NOTRADE.svg');
  background-size: cover;
}
.bugchat.mate{
  background-image: url('images/bugroundchat/MATE.svg');
  background-size: cover;
}
.bugchat.ok{
  background-image: url('images/bugroundchat/OK.svg');
  background-size: cover;
}
.bugchat.no{
  background-image: url('images/bugroundchat/NO.svg');
  background-size: cover;
}
.bugchat.mb{
  background-image: url('images/bugroundchat/MB.svg');
  background-size: cover;
}
.bugchat.nvm{
  background-image: url('images/bugroundchat/NVM.svg');
  background-size: cover;
}
.bugchat.nice{
  background-image: url('images/bugroundchat/NICE.svg');
  background-size: cover;
}

/* *****************************************************************************************
 * site.css
 ******************************************************************************************* */

#main-wrap.bug {
  display: grid;
  grid-template-columns: minmax(auto, auto);
  grid-template-areas: 'main';
  margin-top: var(--site-header-margin);
}

@media (min-width: 800px) {
  /* Scoped to main.round.bug. `.bug` is carried by #main-wrap.bug and the round
     app as well, and this template describes only the round page's own areas —
     so the wrapper, which declares a single 'main' area, was being given three
     rows and ended 743px below its only child.

     The grid-template-columns this rule used to carry is gone rather than fixed.
     It read `2 * minmax(...) minmax(240px, 400px)`, which is not valid grid
     syntax — `2 *` is not a multiplier — so the parser discarded the whole
     declaration and these columns never applied. It was also incoherent: the
     areas below are main.round's, while the columns were copied from
     `.round-app`'s single-board rule in round.css. Deleting it preserves exactly
     what renders today; inventing valid values would invent new behaviour.

     The `side` row was a hardcoded 743px. Nothing derives that number, and it is
     wrong both ways: too much for an empty aside — which is what this page has
     had since the game-info panel moved into the Info tab — and arbitrary for a
     populated one. It now sizes to its content like the two rows above it. */
  main.round.bug {
    /* Two columns, because that is what the areas below describe. Without this,
       `.round` in site.css supplies THREE — a sidebar/board/panel set written for
       the single-board page — and the bughouse areas only ever name two of them.
       The third track then sat empty on the right and, since it absorbed the
       leftover width, pinned everything to the left edge. `justify-content`
       centres the pair once they are content-sized. */
    grid-template-columns: minmax(0, max-content) minmax(0, max-content);
    justify-content: center;
    grid-template-rows: fit-content(0) fit-content(0) fit-content(0);
    grid-template-areas: 'app app' 'uleft uboard' 'side side';
  }
}

/* Both sides of the merge changed this hunk, and each change is kept: the fork widened
   the query (portrait applies at every width now, not only under 800px, because this
   page's portrait mode is a real layout rather than a phone fallback), and upstream
   scoped the selector to `.round-app.bug` so it stops matching info-wraps on other
   pages. Taking either side alone would have thrown away the other's fix. */
@media (aspect-ratio <= 9/16) {
  .round-app.bug .info-wrap0 {
    flex-direction: column-reverse;
    grid-area: clock-top;
    justify-self: flex-end;
    justify-content: right;
  }
  .round-app.bug .info-wrap1 {
    grid-area: clock-bot;
    justify-self: flex-end;
    justify-content: right;
  }
  .clock-wrap{
    align-self: end;
  }
}

#bugboard {
  grid-area: boardPartner;
  position: relative;
}

.bugseek {
  display: grid;
  grid-template-columns: auto/*team board color name;*/;
  grid-template-rows: auto auto 1em auto auto;
}

td.bugseek1 {
  display: grid;
  grid-template-columns: auto auto auto/*team board color name;*/;
  grid-template-rows: auto auto 1em auto auto;
}

.bugseekteam1 {
  display: flex;
  justify-content: center;
  align-items: center;
  grid-row: 1/3;
}

.bugseekteam2 {
  display: flex;
  justify-content: center;
  align-items: center;
  grid-row: 4/6;
}

.bugseekvs {
  grid-column: 1/4;
}

.bugseekteam1 div,
.bugseekteam2 div {
  color: #878787;
}

.bug-join-button {
  background-color: var(--bg-color2);
  border: none;
  color: var(--font-color);
  text-align: center;
  text-decoration: none;
  font-size: 18px;
  cursor: pointer;
  box-shadow: var(--base-shadow);
  border-radius: 3px;
}

.bug-join-button:hover {
  background-color: var(--bg-color1);
}

.bugwaiting {
  color: var(--bug-waiting);
}


/* THE TOOLS CLIP RATHER THAN PUSH THE PAGE.
   -----------------------------------------------------------------------------------------
   The boards come first — `squareUnit.ts` promises the tools only half a square of the left
   board — so this column can end up narrower than the movelist's own content wants. A flex
   item's `min-width` is `auto`, meaning it refuses to go below min-content, so it did not
   shrink: it overhung its track and the PAGE scrolled sideways instead, measured 43px at
   996x649 with a 31px track and a 101px movelist.

   `min-width: 0` is the same bargain the tools column makes everywhere else in this file
   (`.analysis-app.bug .bug-parts`, and the `minmax(0, max-content)` on the parts track): what
   does not fit is clipped, and the page's own width is never negotiable. An unusable tools
   column is a deliberate outcome here; a horizontally scrolling page is not. */
main.round.bug .movelist-block,
main.round.bug .movelist-block #movelist {
  flex: 1;
  min-width: 0;
}

main.bug.analysis round-player0.bug rating,
main.bug.analysis round-player1.bug rating {
  float: left;
  font-size: 16px;
}

.info-wrap0.bug {
  grid-area: clockB-top;
}

.info-wrap1.bug {
  grid-area: clockB-bot;
}

main.bug round-player0,
main.bug round-player1 {
  font-size: 0.7vw;
  width: 100%;
  line-height: unset;
  height: 48px;
  align-items: center;
}

round-player0.bug {
  grid-area: userB-top;
  /*justify-content: right;*/
}

round-player1.bug {
  grid-area: userB-bot;
}

.clock-wrap.bug {
  flex-direction: row-reverse;
}

/* Wraps a single clock and its difference indicator. The indicator used to be positioned
   absolutely at this box's left edge, which put it over the leading digit whether or not there
   was anywhere else for it to go — measured on the desktop, a 21.44px indicator covering a digit
   with 37.24px of empty space beside the clock it could have stood in.

   IT IS NOW A TWO-COLUMN GRID, and the track sizing is the whole mechanism. The first column is
   `minmax(0, max-content)`: it takes the indicator's full width when the box has room to spare
   and collapses toward zero when it does not, so the track is `min(indicator, room)` by
   construction. The indicator sits at that track's start and keeps its own width, which gives
   all three placements from one rule and with no measured constant:

     room to spare  — the track is the indicator's width, and it ends exactly at the clock's edge
     partial room   — the track is what room there is, and it overhangs the digits by the shortfall
     no room at all — the track is zero, and it lies over the leading digit, as it always did

   Expressing this by arithmetic instead would need the indicator's own width and the free space
   in ONE expression, and CSS resolves `100%` against the element's own box inside `transform` but
   against the containing block in `inset` and `margin`. The two cannot be combined, so it would
   have taken an `em` estimate of the indicator — the same kind of estimate that put the clock's
   digits outside their box in the first place.

   The box spans the wrap rather than shrink-wrapping the digits, because the room the first
   track draws on is the room BESIDE the clock, and a box that ends at the digits has none.
   `justify-content: end` keeps the digits on the strip's trailing edge, which is where the
   capability requires them.

   `overflow: hidden` stays, and now clips at the wrap's edge instead of the clock's. The
   indicator never reaches it: at zero room it starts at the clock's leading edge and runs
   inwards, and every wider case is further from the edge still. */
.clock-holder {
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, max-content) max-content;
  justify-content: end;
  align-items: end;
  flex: 1 1 auto;
  overflow: hidden;
}

.clock-holder > div[id^="difference"] {
  grid-row: 1;
  grid-column: 1;
  justify-self: start;
  width: max-content;
}

.clock-holder > div[id^="clock"] {
  grid-row: 1;
  grid-column: 2;
}

.clock-difference {
  font-size: 1em;
  font-weight: bold;
  line-height: 1;
  padding: 0.1em 0.3em;
  color: var(--font-color);
  background-color: var(--bg-color0);
}

.clock-difference.negative {
  color: var(--bad);
}

.clock-difference.positive {
  color: var(--good);
}

#messages {
  width: 100%;
}

@media (aspect-ratio <= 9/16) {
  main.round.bug #movelist {
    font-size: 1.1em;
  }
}

@media (min-width: 800px) {
  main.round.bug #movelist {
    font-size: 0.68vw; /*  magic number that looks good and resizes itself with container */
  }
}

main.round.bug #movelist {
  display: flex;
  height: 100%;
  max-height: unset;
  overflow-y: auto;
  flex-flow: row wrap;
  position: relative;
  line-height: 1.7;
  align-items: center;
  align-content: flex-start;
  background: var(--bg-color0);
}
move-bug.active {
  font-weight: bold;
}
/* FOUR PAIRS PER ROW, AND THE ARITHMETIC HAS TO CANCEL EXACTLY.
   -----------------------------------------------------------------------------------------
   A row is four counter+move pairs — the four seats in the order they moved, which is what
   aligns the two boards — so each pair is 25% and the pair splits into a fixed counter and
   whatever is left. Written as `counter + (25% - counter)`, the counters cancel and the four
   pairs sum to exactly 100%.

   THE UNIT IS `em`, NOT `ch`, AND THAT IS THE WHOLE FIX. `ch` is the width of a "0" in the
   element's OWN face, and a move cell is not always in the counter's face: `.theoretical`
   moves — every move on the blank analysis board, and every explored line on a real game —
   are italic. Measured at 10.7179px: `3ch` is 18.3755px upright and 17.7009px italic, so an
   italic cell claimed `25% - 17.70` beside a counter that really took 18.38. The 0.675px
   difference three times over is 2.02px, against a 255.2px container — and the fourth pair's
   move wrapped to the next row while its counter stayed behind. Exactly the row this layout
   exists to keep whole.

   Every cell carries the same font-SIZE (only the face differs), so `em` is identical on both
   sides of the subtraction and cannot drift the way `ch` did. It is one variable used twice
   for the same reason: two copies of a number that must agree is how they stop agreeing. */
move-bug {
    line-height: 1.9em;
    float: left;
    flex: 0 0 calc(25% - var(--bug-move-counter-w, 1.72em));
    max-width: calc(25% - var(--bug-move-counter-w, 1.72em));
    padding-left: 6px;
    box-sizing: border-box;
    justify-content: center;
    white-space: nowrap;
}

/* THE MOVELIST FITS ITS BOX BY SIZING THE TYPE TO THE BOX, not by clipping what does not fit.
   -----------------------------------------------------------------------------------------
   A row is four `25% - 3ch` cells, so how much of a move fits is decided by the ratio between
   the container's width and the font — and `1ch` is a font unit, so the counter's `3ch` grows
   with the type and takes the growth out of the cell beside it. Measured on the analysis page's
   247px panel at the inherited 15.4px: cell 35.4px against `Qxg6+` needing 47.2px plus 6px of
   padding. An 18px shortfall, in a cell that gets SMALLER as the font gets bigger.

   `cqi` is what closes it. One percent of the container's INLINE CONTENT BOX — which excludes a
   vertical scrollbar — so the character budget per cell is the same at every width, and the
   moment a scrollbar appears and takes 8px the type follows it down. That is the resize this
   needs, and it needs no JavaScript and no second measurement: the browser does it as part of
   layout, which is also why it cannot get out of step the way a measured value would.

   4.2 is measured, not chosen. At the same 247px panel it gives a 10.7px cell font with 7.1px
   spare on a six-character move (`Ngxf7+`) and still holds a seven-character one; 4.5 leaves
   only 3.2px and fails at seven; 5.5 overflows this game's own moves.

   `min(…, 16px)` caps the growth. The ratio holds at every width, so a wide movelist would
   otherwise keep scaling up long past the point where the cell is roomy — the cap is about the
   type looking right, never about fitting.

   The padding goes with it: 6px was a constant beside a length that scales, so at a small font it
   was eating a fifth of the cell. In `ch` it is the same fraction of a character at every size. */
main.round.bug #movelist,
.analysis-app.bug #movelist {
    container-type: inline-size;
    --bug-move-counter-w: 1.72em;
}
main.round.bug move-bug,
.analysis-app.bug move-bug {
    font-size: min(4.2cqi, 16px);
    padding-left: 0.4ch;
}

move-bug.haschat {
    background-image: url('icons/bugchatmove.svg');
  background-size: 1em 1em;
   background-repeat: no-repeat;
  background-position: right;

}
bugchat img{
  margin-left: 1em;
  width: 1.1em;
  height: 1.1em;
}

.bugchatpopup {
  list-style-type: none;
  display: none;
  min-width: 12rem;
  border-left: 2px solid #3692e7;
  border-radius: 0 3px 3px 3px;
  text-align: left;
  box-shadow: var(--base-shadow);
  z-index: 1000;
  min-height: unset;
  flex: 0 0 100%;
  max-width: 100%;
}

move-bug.active + .bugchatpopup {
  display: block;
}

/* A RESULT LINE MUST WRAP, whatever the names are.
   -----------------------------------------------------------------------------------------
   `movelist.ts` appends this block to the movelist at game over, and it holds the teams:
   "Invalid move • Test-KnibisFers+Test-FersAmazon won". Two usernames joined by `+` with no
   space is ONE token to the line breaker, and with `overflow-wrap: normal` it cannot be
   broken anywhere — measured min-content 121px against a 107px column, which the movelist's
   `overflow-y: auto` then turned into a horizontal scrollbar (a non-visible overflow on one
   axis forces the other to `auto`).

   Widening the column fixes today's numbers — see the wrapper rule above — but not the class
   of failure: a long enough pair of names overflows any width. `anywhere` breaks the token
   instead, and is preferred to `break-word` because it also lets the line box shrink, so the
   block reports a min-content width the column can actually satisfy. */
main.round.bug #movelist .status,
.analysis-app.bug #movelist .status {
    overflow-wrap: anywhere;
}

move-bug.counter {
    color: Gray;
    /* The same length the cell beside it subtracts — see `move-bug` above. 1.72em is the
       3ch this used to be, measured in the upright face the counters are always drawn in. */
    flex: 0 0 var(--bug-move-counter-w, 1.72em);
    padding-left: 0px;
    background-color: var(--bg-color2);
    text-align: center;
}

.pocket-top-partner {
    grid-area: pocket-top-partner;
}
.pocket-bot-partner {
    grid-area: pocket-bot-partner;
}

player.bug {
    text-align: center;
}
.versus.bug {
    line-height: 100%;
}


/* The end-of-game controls: rematch, a new opponent, the analysis board.

   They take the area the presets vacate — the two never coexist, since the presets
   are hidden by the same result that renders these — so they inherit the presets'
   place in the drop order and move under a shrunken board exactly as those did.

   Wrapping flex items, not a column: each button sits beside the next where the
   width allows and stacks where it does not, which is what the preset sets do. The
   element is empty until there is a result, and an empty flex box is zero tall, so
   it costs nothing while the game is on.

   This element wears no class from site.css. It briefly wore `btn-controls after`
   for the button styling, and that brought `grid-area: game-controls` — which threw
   it out of the column entirely, measured at x=1731 — and `flex-flow: column
   nowrap`, at a specificity this file then had to out-rank with a four-class
   selector. The styling is restated below instead, and nothing here argues with
   another stylesheet. */
/* WHILE THE GAME IS ON THIS ELEMENT MUST NOT CATCH A CLICK.
   It shares `grid-area: p1` with the first preset panel on purpose — the end-of-game
   controls take the place the presets vacate, and `.game-over .chatpresets` hides them at
   the same moment. But sharing a cell means overlapping in it, and a grid item stretches
   to its cell whether or not it has anything in it. So for the whole game this element is
   an invisible, empty box lying across all ten buttons of the first preset group, and
   `document.elementFromPoint()` on any of them returned `DIV.bug-gameover`, not the
   button. The second group is in `p2` and was never affected, which is exactly how the
   symptom presented: the first ten dead, the second ten fine.

   `pointer-events` rather than `display: none`, deliberately. It fails safe: if the
   `game-over` class were ever not applied, the end-of-game buttons would still be drawn
   and merely unclickable, where `display: none` would make them vanish entirely. */
.round-app.bug:not(.game-over) .bug-parts > .bug-gameover {
    pointer-events: none;
}
.bug-parts > .bug-gameover {
    grid-area: p1;
    display: flex;
    /* A COLUMN that wraps. Stacked is the arrangement these want — three wide text
       buttons read as a list — so the main axis is vertical and one sits above the
       next. Wrapping is the fallback and only the fallback: a second column appears
       when the height it is given will not hold three, which is the "if needed"
       part. It is the mirror of the preset sets, which want to pair and stack only
       when they must. */
    flex-flow: column wrap;
    /* Both, and they do different jobs. `align-items` makes a button fill its
       column; `align-content` makes the column itself fill the element rather than
       shrink to the widest button — without it the buttons came out 115.4px inside
       a 220.7px space. When the fallback does put them in two columns, the same
       pair splits the width evenly between them. */
    align-items: stretch;
    align-content: stretch;
    gap: 4px;
    /* clear of the tab part above — without it the top button sits flush against
       the chat's bottom edge */
    margin-top: 8px;
    min-width: 0;
}
/* The button styling this element used to inherit from site.css's `.btn-controls`,
   stated here instead. Six declarations, against a class that also carried
   `grid-area: game-controls` and `flex-flow: column nowrap` — the area threw the
   element out of the column entirely and the flow was the stacking these buttons
   must not be fixed in. `flex: 1 1 auto` rather than site.css's `flex: 1`, so a
   button keeps its text's width instead of being forced to an equal share. */
.bug-gameover > button {
    /* 0 0 auto, not 1 1 auto: the main axis is vertical here, so a growing button
       would grow TALLER rather than wider. The width comes from align-items above.
       A child combinator, deliberately: it keeps this rule off anything a future part
       might nest inside this element. */
    flex: 0 0 auto;
    height: 40px;
    border: none;
    color: var(--font-color);
    background-color: var(--bg-color0);
}
.bug-gameover > button:not([disabled]):hover {
    color: #fff;
    background-color: var(--green-hover);
    /* Same gradient trap as the accept state above: without this, hovering the rematch
       button showed no colour change at all, because site.css's `button.rematch`
       background shorthand kept painting over it. */
    background-image: none;
}

/* AN OFFER IS A LOOK ON THE CONTROL THAT MADE IT.
   ------------------------------------------------------------------------------------
   These replace `#offer-dialog`, the full-width strip that used to draw a draw or
   rematch prompt below both boards — an answer nowhere near the question. Two states,
   both on buttons that already exist.

   THE PENDING STATE COSTS NOTHING HERE. A button whose offer is outstanding is rendered
   `disabled`, and site.css already dims a disabled button's icon (`button[disabled] i`)
   and withholds the hover colour (`:not([disabled]):hover`). So "I have offered" is the
   site's own idiom for an inert control rather than a look invented for this page, and
   there is no rule below for it — deliberately, and stated so nobody adds one. */

/* THE ACCEPT STATE: the draw button, unchanged but green.
   --green-switch, not --green-hover. The hover green is what EVERY button on this page
   turns when the pointer is over it, so reusing it would make "a draw is offered to you"
   indistinguishable from "your pointer is here" — and it differs per theme (#89b25b light,
   #537c23 dark) while this state must read the same in both. --green-switch (#629924) is
   defined once for both themes and already means affirmative elsewhere on the site.

   Colour is doing real work here: it is the ONLY thing separating "offer a draw" from
   "end the game now", on a button that takes one press with no confirmation. Nothing else
   about the button may change — `.bug-round-tools-bar .btn-controls button` sizes these at
   two thirds of a board square precisely because they are reached for under time pressure,
   and a state that moved or resized the target would undo that. So this rule sets colour
   and nothing else, and the hover below only deepens it rather than reverting to the
   shared hover green.

   Known limitation, stated rather than hidden: a player who cannot distinguish this green
   sees an ordinary draw button that ends the game. That is this page's standing
   accessibility debt showing up in a new place, not a new decision. */
.round-app.bug .btn-controls button#draw.draw-offered {
    background-color: var(--green-switch);
    color: #fff;
}
.round-app.bug .btn-controls button#draw.draw-offered:hover {
    background-color: var(--green-hover);
    color: #fff;
}

/* THE ARMED RESIGN STATE, and why it is not just a red background.
   ------------------------------------------------------------------------------------
   This is the partner of a player who has asked to resign. Pressing it ENDS THE GAME.
   Pressing the SAME button at rest only asks. Same glyph, same size, same place — so the
   only thing standing between "ask my partner" and "we lose" is how the button looks, and
   a player who misreads it resigns when they meant to ask.

   That is why colour cannot carry this alone. Roughly one man in twelve has some red-green
   deficiency, and the button beside this one goes green to end the game a different way.
   The inset ring is the second signal: it survives greyscale, it survives any colour
   deficiency, and it costs no layout — `box-shadow` paints inside the border box, so the
   button stays exactly two thirds of a board square, which is the one thing the tools bar
   rule will not give up.

   Note this is NOT about telling the flag from the ½ — those glyphs already differ. It is
   about telling ARMED from AT REST on one button whose meaning changes between them.

   The red is stated rather than taken from a variable: `--red-text` is Crimson, a text
   colour, and there is no red background token on this site. Chosen to sit clearly darker
   than --green-switch in luminance as well as differing in hue, so the two lit states do
   not read as the same button in greyscale either. */
.round-app.bug .btn-controls button#resign.resign-confirm {
    background-color: #a02c2c;
    color: #fff;
    box-shadow: inset 0 0 0 2px #ffd9d9;
}
.round-app.bug .btn-controls button#resign.resign-confirm:hover {
    background-color: #c23a3a;
    color: #fff;
}

/* THE REMATCH CONTROL: one button, three labels.
   ACCEPT REMATCH is green for the same reason the draw control's answerable state is —
   green here means "this press ends the waiting" — and it is the same --green-switch, so
   the page has one affirmative colour rather than two that nearly match.

   No non-colour signal is needed on this one, unlike the resign control. This is a WIDE
   TEXT button whose label changes with its state: REMATCH, CANCEL REMATCH, ACCEPT
   REMATCH. The words carry the meaning and the colour only reinforces it, where on the
   icon buttons the colour was carrying it alone.

   The `.rematch-answer` pair that used to live here is gone. It held ACCEPT and DECLINE
   side by side, and DECLINE did nothing that not pressing ACCEPT did not already do —
   while the thing genuinely missing, a way for the offerer to take the offer back, had no
   control at all. The middle state is that control now. */
.bug-gameover > button.rematch.accept {
    background-color: var(--green-switch);
    /* LOAD-BEARING, and the reason the green was invisible at first. site.css:1519 gives
       `button.rematch` a `background` SHORTHAND — `var(--rematch)`, a linear-gradient —
       so setting background-color alone leaves that gradient painted on top of it and the
       button stays grey. Measured: computed backgroundColor was rgb(98,153,36) while the
       button rendered dark, because backgroundImage was still the gradient.
       This is also why `.bug-gameover > button:hover`'s green below was never visible on
       the rematch button, which nobody had noticed. */
    background-image: none;
    color: #fff;
}
.bug-gameover > button.rematch.accept:not([disabled]):hover {
    background-color: var(--green-hover);
    background-image: none;
    color: #fff;
}

/* No presets once the game has a result. Hiding the CONTENT rather than the panel
   is deliberate: the panel is a tab part and the tab widget writes its `display`
   inline when tabs are switched, so a rule on the panel would either lose to that
   inline style or need `!important` to beat it. The panel stays, empty and zero
   tall, and the rows close up on their own. */
.round-app.bug.game-over .chatpresets {
    display: none;
}

/* AND IT MUST NOT STRETCH, or "empty and zero tall" is only true where nothing else
   shares its row.

   The first preset part and the end-of-game controls occupy the SAME area on purpose — the
   controls take the place the presets vacate. So the moment the presets are hidden, that row is
   sized by the controls, and a grid item's default `stretch` made the emptied panel exactly as
   tall as they are: measured 136px over a 128px `.bug-gameover` at the same origin, with the
   panel later in the DOM. It painted its own background across Rematch, New opponent and Analysis
   board, and `elementFromPoint` over the Rematch button returned the panel — so the controls were
   not merely invisible, they were unclickable.

   `start` makes the panel take its content height, which is zero now that the content is gone.
   The second part never showed this: nothing shares its area, so its row collapsed with it. */
.round-app.bug.game-over .chatpresets-panel {
    align-self: start;
}

/* Which area each part occupies in the merged column. Inert in portrait, where
   `.bug-parts` is a flex container and its children are flex items rather than grid
   items, so these names are simply ignored there. */
.bug-parts > [role='tabpanel'] { grid-area: chat; }
.bug-parts .chatpresets-panel-1 { grid-area: p1; }
.bug-parts .chatpresets-panel-2 { grid-area: p2; }
.bug-parts > .bug-round-tools-bar { grid-area: tablist; }
/* The analysis page's tab list is the direct equivalent and drops the same way. Here it is a child
   of `.bug-parts`; the round page's is nested inside its tools bar, so the two selectors can never
   match the same element. */
.bug-parts > [role='tablist'] { grid-area: tablist; }
/* The analysis page's tab list is the direct equivalent and drops the same way. It is a child of
   `.bug-parts` here; the round page's is nested inside its tools bar, so the two selectors cannot
   both match the same element. */
.bug-parts > [role='tablist'] { grid-area: tablist; }

/* The two dropped arrangements, in the order parts leave the strip beside the
   board. A dropped part spans BOTH columns, which is the point: it does not merely
   relocate under the board, it widens to the full column and sits under the parts
   above it as well. toolsPlacement.ts adds each class only when the board's stack
   still fits in what would be left. */
.bug-right-column.drop-tablist {
    grid-template-areas: 'stack chat'
                         'stack p1'
                         'stack p2'
                         'tablist tablist';
}
.bug-right-column.drop-tablist.drop-p2 {
    grid-template-areas: 'stack chat'
                         'stack p1'
                         'p2 p2'
                         'tablist tablist';
}
.bug-right-column.drop-tablist.drop-p2.drop-p1 {
    grid-template-areas: 'stack chat'
                         'p1 p1'
                         'p2 p2'
                         'tablist tablist';
}

/* A DROPPED part always fits its two sets on one row.

   Beside the board the sets are floored — five tracks at the button minimum, about
   348px — and two of them cannot fit the strip, so they stack, which is what the
   presets have always looked like. That floor must stop applying once a part drops,
   or it decides the wrap there too: dropping happens precisely when the board is
   small, and a small board makes this column NARROW (its width is the board plus the
   strip), so the row a part drops into was measured at 672.9px against the 696.8px
   two floored sets demand. The part dropped and its sets stayed stacked anyway, and
   being wider made it TALLER, not shorter.

   At an exact half each, two sets always share the row. The buttons can then come out
   marginally under the design floor on a narrow column — 40.4px against 41.8px — which
   is still far above the 24px WCAG minimum the floor exists to clear.

   This is also what keeps the placement settled: dropping now makes a part shorter,
   so it cannot undo the condition that dropped it, and undropping makes it taller, so
   it cannot undo that either. Stable in both directions. */
/* No drop-state sizing any more. A dropped part used to stretch its sets to half the
   row and fill the cells, which is what made its buttons a different size from its
   neighbour's — and, with the tracks holding a minimum the halved set could not
   honour, made the row overflow: 220px of content in 183.7px. Every set now draws
   the same fixed tracks wherever it is, so dropping changes where a part sits and
   nothing about how it looks. */

/* THE USERNAME ON A LINE OF ITS OWN, below the pocket and the clock. Shared by
   every mode, because every mode now uses the merged column.
   `seatNamePlacement.ts` adds these classes when the stack has the height to
   spend, which is everywhere except at or near full zoom — at full zoom the
   stack is exactly the ten squares it is given and there is nothing spare.

   These lived in the tall-landscape block until portrait joined the merged column,
   at which point portrait set the classes and nothing answered: the name stayed
   between the pocket and the clock while the class said otherwise.

   `display: contents` on the block is what lets the name reach the full width of
   the strip. The name lives inside the clock's block, so on its own it could only
   ever span what that block spans — the strip less the pocket. Dissolving the
   block makes the clock, the name and the misc slot direct flex items of the
   strip, and then a name with a full basis takes a line across the whole of it.
   The block element itself is untouched, which matters: flip swaps it between
   strips, and `display: contents` changes layout, not the DOM.

   The block's own `row-reverse` goes with it, so the clock is placed against the
   trailing edge here with a margin instead — same result, stated for a row that
   is no longer reversed. */
.own-name-outside .own-seat > .info-wrap0,
.own-name-outside .own-seat > .info-wrap1,
.partner-name-outside .partner-seat > .info-wrap0,
.partner-name-outside .partner-seat > .info-wrap1 {
    display: contents;
}
.round-app.bug.own-name-outside .own-seat,
.round-app.bug.partner-name-outside .partner-seat {
    flex-wrap: wrap;
    /* Content height, not one square plus a guess at the name's line. The guess
       was `font-size * 1.3` and the name's real line box is nearer 1.8 of it —
       the name carries a presence dot and a rating alongside the text — so the
       strip clipped the very line it had just made room for. What the line costs
       is the name's business; the strip takes what its two rows need. */
    height: auto;
    align-content: flex-start;
}
/* One line, which is the whole point of giving it a line. The text sits in an
   inner block, so nowrap has to go there — on the outer element it governs
   nothing. */
.round-app.bug.own-name-outside .own-seat .player-data,
.round-app.bug.partner-name-outside .partner-seat .player-data {
    white-space: nowrap;
    overflow: hidden;
}
/* Last on the first line and pushed to its end. `order` as well as the margin:
   the block's misc slot is a sibling now that the block has dissolved, and it
   followed the clock, leaving the clock 19.4px short of the trailing edge. */
/* Descendant, not child. `display: contents` makes the clock a flex ITEM of the
   strip but leaves it a DOM child of the block it came from, and `>` matches the
   DOM — so the child form silently matched nothing and the clock sat wherever it
   fell. `margin-left: auto` is what returns it to the trailing edge now that the
   block's own `row-reverse` has dissolved with the block. */
.round-app.bug.own-name-outside .own-seat .clock-wrap,
.round-app.bug.partner-name-outside .partner-seat .clock-wrap {
    margin-left: auto;
}
.round-app.bug.own-name-outside .own-seat round-player0,
.round-app.bug.own-name-outside .own-seat round-player1,
.round-app.bug.partner-name-outside .partner-seat round-player0,
.round-app.bug.partner-name-outside .partner-seat round-player1 {
    /* It reaches the next line on its own: `nowrap` on the text inside makes its
       min-content the width of the whole name, which cannot fit beside the pocket
       and the clock, so the strip wraps it. Growing then gives it the full width
       of that line. No forced basis and no order — the content decides, which is
       why this keeps working when a name is short enough to fit beside them. */
    flex: 1 1 auto;
    height: auto;
}

/* THE NAME ON A LINE OF ITS OWN — the analysis page's half of the decision above.
   ------------------------------------------------------------------------------------
   The declarations are the round page's, which reach the same arrangement from a class
   the JS toggles; asked here as a style query on the number the stack computed, so the
   two pages agree about what "outside" looks like while deciding it differently.

   Written in the POSITIVE direction, unlike the coordinate treatment below: a browser
   without `@container style()` skips this block and every name stays inside its strip,
   which is a real, complete arrangement and the one the page has at full zoom anyway.
   Written the other way round, the same browser would give every name a line whether or
   not the stack had height for it, and the strip's `overflow: hidden` would eat it. */
@container style(--bug-name-outside: 1) {
    .analysis-app.bug .seat-strip0,
    .analysis-app.bug .seat-strip1 {
        flex-wrap: wrap;
        /* Content height, not the one square --bug-strip-h pins it to: the strip now has
           two rows and takes what they need. The same reasoning as the round page's. */
        height: auto;
        align-content: flex-start;
    }
    /* A full basis, which the round page does not need and this page does. There it is
       the name's own min-content — a nowrap name beside a pocket AND a clock — that
       forces the wrap; here the pocket is five squares of eight and a name would simply
       fit beside it, so the line has to be asked for rather than fallen into. */
    .analysis-app.bug .seat-strip0 > round-player0,
    .analysis-app.bug .seat-strip1 > round-player1 {
        flex: 1 1 100%;
        height: auto;
    }
    /* Outside, not below: the name leaves a TOP strip upwards, so the pocket stays
       against the board in both strips. */
    .analysis-app.bug .seat-strip0 > round-player0 {
        order: -1;
    }
}

/* ===========================================================================================
   COORDINATES — ON THE SQUARES BY DEFAULT, OUTSIDE THE BOARD WHEN THERE IS ROOM

   This is the DEFAULT treatment, deliberately, and the polarity is the whole point of it
   being written this way round. The condition is still "has this board been given room",
   asked once as a style query — but it selects the treatment that needs the query to be
   UNDERSTOOD, not the one that needs it to be ignored.

   A browser that does not support `@container style()` skips the block at the end of this
   file and keeps everything here: coordinates on the squares, in every mode, at every zoom.
   That is a degraded desktop — it loses labels outside the board and keeps an unused band
   under each one — but every label is present and readable. Written the other way round, the
   same browser would keep the OUTSIDE treatment everywhere, and since the gap is zero wherever
   there is no room, `calc(0px * 0.75)` would render every coordinate at zero: they would
   simply vanish on phones, which is where they matter most.

   The failure of a missing feature should be the feature not helping, never the page losing
   something it had before the feature existed.
   =========================================================================================== */
/* ---------------------------------------------------------------------------
   COORDINATES INSIDE THE SQUARES — WHEREVER THERE IS NO ROOM OUTSIDE

   Asked as a question about room, not about which layout this is. The gap above resolves
   to exactly zero when the mode has no spare height, or too little of it to be worth a
   legible label, and that is the whole condition: a board with somewhere to put its
   labels keeps them outside, a board without puts them on the squares.

   This used to be a media query listing portrait and short landscape by name. It no
   longer needs to: their stacks are exactly ten squares of their own unit, so their room
   is the quantisation remainder, which is below the floor and resolves to zero. They
   reach this block by arithmetic. The desktop mode reaches it too, at full zoom, where
   its own remainder is 6.96px — which is the behaviour this change exists to add, and
   the reason the list had to go rather than merely grow a third entry.

   Buying room, not a change of house style: a label sits outside the board wherever the
   room for it exists.

   `style()` compares the COMPUTED value, which only holds because --bug-coord-gap is
   registered with @property at the top of this file. Unregistered it would be compared as
   tokens, `clamp(...)` would never equal `0px`, and this block would silently never
   apply.

   Rank and file labels are absolutely positioned and placed by four variables that
   `extensions.css` sets negative at :root (--ranks-right: -15px, --files-bottom:
   -16px), so by default they hang off the board's edges. Setting them to zero puts them
   on the board instead. The gutters that separate the boards STAY — they are wanted for
   their own sake, not only as room for labels.

   Scoped to this page rather than changed at :root, because every board on the site
   reads those variables: lobby, analysis, editor, puzzles. analysis.css and embed.css
   already override a subset the same way.

   The size follows the board's own square rather than the page, so a label keeps its
   proportion on a board at any zoom. Paired identity-to-identity — #mainboard with
   --cg-width-a — because a board switch moves the ROLE between elements while these
   two always describe the same board.

   The colour is the other square's colour: a label on a dark square is drawn in the
   light square's colour and the other way round. --cg-light and --cg-dark are
   published by boardColors.ts beside --board-image, extracted from the theme's own
   image. No outline or shadow: with the two colours coming from the board itself the
   contrast is already there, and an outline on an 8px glyph reads as a smudge.
   --------------------------------------------------------------------------- */
:is(.round-app, .analysis-app).bug .cg-wrap coords {
    /* also un-hides them on phones, where chessground.css hides them outright for want
       of anywhere to put them — inside the squares there is somewhere */
    display: flex;
    opacity: 1;
}
:is(.round-app, .analysis-app).bug .cg-wrap coords.side {
    right: 0px;
    width: auto;
}
:is(.round-app, .analysis-app).bug .cg-wrap coords.bottom {
    bottom: 0px;
    height: auto;
}
:is(.round-app, .analysis-app).bug .cg-wrap coords coord {
    font-weight: 700;
    line-height: 1;
    padding: 1px 2px;
}
/* Files sit in the bottom-left corner of their square, ranks against the right edge. */
:is(.round-app, .analysis-app).bug .cg-wrap coords.bottom coord {
    text-align: left;
}
:is(.round-app, .analysis-app).bug .cg-wrap coords.side coord {
    text-align: right;
    /* the desktop nudge that centred a label against a board edge; inside the square it
       is placed by its own box */
    transform: none;
}
:is(.round-app, .analysis-app).bug #mainboard coords coord {
    font-size: calc(var(--cg-width-a) / 8 * 0.3);
}
:is(.round-app, .analysis-app).bug #bugboard coords coord {
    font-size: calc(var(--cg-width-b) / 8 * 0.3);
}

/* A label takes the OTHER square's colour, so it always contrasts with what it sits on.
   Which child sits on which colour follows from the direction the row or column is laid
   out in, and chessgroundx already encodes that in these exact selectors — mirrored here
   rather than re-derived, so the two cannot disagree.

   Screen parity is orientation-independent on an 8x8 board: flipping maps (f,r) to
   (9-f,9-r), and 18 minus a number keeps its parity, so the top-left square is light
   whichever way the board faces. Only the DOM direction matters, which is why these
   pair with `forward`/`backward` and not with which colour is at the bottom.

   This is scoped to a page whose boards are always 8x8. An odd-sized board would need
   the opposite mapping on one axis. */

/* THE ONE THING STILL ASKED IN THE POSITIVE DIRECTION, and for a reason worth stating.
   These colours only make sense on a label that is ON a square: they paint it in the OTHER
   square's colour so it always contrasts with what it sits on. A label out in the gap has no
   square under it and wants the page's own colour, which it gets by these simply not applying.

   Gated positively rather than undone in the block below because undoing them is not available
   at a sane cost: the parity selectors carry five classes each, so a restoring rule would have
   to repeat all sixteen of them to outrank the ones it is undoing.

   The failure mode stays safe. A browser that ignores this block leaves the labels inheriting
   the page colour on their squares — lower contrast than intended, still perfectly visible —
   which is the same bargain the rest of this treatment makes. */
@container style(--bug-coord-gap: 0px) {
    /* Files laid out in DOM order: first child is leftmost, on a dark square. */
    :is(.round-app, .analysis-app).bug coords.bottom.forward coord:nth-child(odd),
    :is(.round-app, .analysis-app).bug coords.bottom.backward.black coord:nth-child(odd),
    /* Files laid out reversed: first child is rightmost, on a light square. */
    :is(.round-app, .analysis-app).bug coords.bottom.backward coord:nth-child(even),
    :is(.round-app, .analysis-app).bug coords.bottom.forward.black coord:nth-child(even),
    /* Ranks running top-down: first child is topmost, on a dark square. */
    :is(.round-app, .analysis-app).bug coords.side.backward coord:nth-child(odd),
    :is(.round-app, .analysis-app).bug coords.side.forward.black coord:nth-child(odd),
    /* Ranks running bottom-up: first child is lowest, on a light square. */
    :is(.round-app, .analysis-app).bug coords.side.forward coord:nth-child(even),
    :is(.round-app, .analysis-app).bug coords.side.backward.black coord:nth-child(even) {
        color: var(--cg-light, #f0d9b5);
    }

    :is(.round-app, .analysis-app).bug coords.bottom.forward coord:nth-child(even),
    :is(.round-app, .analysis-app).bug coords.bottom.backward.black coord:nth-child(even),
    :is(.round-app, .analysis-app).bug coords.bottom.backward coord:nth-child(odd),
    :is(.round-app, .analysis-app).bug coords.bottom.forward.black coord:nth-child(odd),
    :is(.round-app, .analysis-app).bug coords.side.backward coord:nth-child(even),
    :is(.round-app, .analysis-app).bug coords.side.forward.black coord:nth-child(even),
    :is(.round-app, .analysis-app).bug coords.side.forward coord:nth-child(odd),
    :is(.round-app, .analysis-app).bug coords.side.backward.black coord:nth-child(odd) {
        color: var(--cg-dark, #b58863);
    }
}

/* ===========================================================================================
   ...AND OUTSIDE THE BOARD WHEREVER ONE HAS BEEN GIVEN ROOM

   Everything here undoes the block above for a board whose gap is not zero. It restores
   chessground's own treatment by hand rather than by reverting to it, because a plain
   override cannot be un-done — `revert` goes to the user agent, not to the earlier author
   rule, and this file declares no cascade layers. Each value below is therefore chessground.css's
   own, copied deliberately; if that file changes one, this has to follow.

   `not style(...)` rather than a second value to match, because the gap is a continuum: it is
   any length from the floor to the overhang, and a style query can only test one exact value.
   Zero is the one value with a meaning of its own, so the question is asked about zero and
   answered in the negative here. --bug-coord-gap is registered with @property at the top of
   this file, which is what makes the comparison a computed length rather than a token match.
   =========================================================================================== */
@container not style(--bug-coord-gap: 0px) {
    /* chessground.css's own 0.8; the block above raises it to 1 to un-hide labels on a phone. */
    :is(.round-app, .analysis-app).bug .cg-wrap coords {
        opacity: 0.8;
    }
    /* THE DISTANCE FROM A BOARD TO ITS OWN LABELS, and the one number the ranks now turn on.

       MEASURED, not chosen. There is no declared gap under a board: the file labels' box is FLUSH
       with the bottom edge — `bottom` is -1 * the gap and `height` is the gap, so the box's top
       edge is the board's edge exactly — and all the air you see is the line box, `line-height:
       normal` over type at 0.75 of the gap. Measured in a replica driven by this stylesheet at
       dpr 1.5, the ink of a file letter sits this far below the board:

           gap 16px (font 12px)     b d f h  2.00px      a c e g  5.00px
           gap 15px (font 11.25px)           3.00px               5.00px
           gap 14px (font 10.5px)            3.33px               5.33px
           gap 13px (font 9.75px)            2.00px               4.00px
           gap 12px (font 9px)               2.33px               4.33px

       2px is the ascender distance at a 16px gap, and both halves of that are deliberate. The gap
       is 16 in practice: the room is about --bug-app-h * (1 - zoom), which comes to 6.96px at full
       zoom — below --bug-coord-floor, so no external label at all — and past 16 within about 2% of
       the slider, so the 12-15 band is a sliver nobody sits in. The ASCENDERS are the reference
       because they are the MINIMUM the files show, and minimal is what is wanted; matching a c e g
       would stand the rank digits 5px off, visibly further out than the file letters that reach
       highest.

       The files are deliberately NOT changed to declare this same 2px. It could be done, but only
       by moving the one thing on this page that already looks right in order to describe it, and
       by spending the reserved gap differently — the box height IS the room the layout set aside,
       so taking a lead out of it makes the letters smaller. The dependency runs one way, files to
       constant, and this comment is where that is recorded. */
    .round-app.bug .cg-wrap {
        --bug-coord-lead: 2px;
    }
    /* Ranks sit against the board they name, at that same distance.

       ANCHORED TO THE BOARD, which is the whole of this change. chessground.css anchors this
       element by its RIGHT edge (`right: var(--ranks-right)`, -15px) inside a fixed 12px box and
       then pushes the digit to the far end of it with `text-align: right` — three values that must
       agree to arrive at a distance. They agreed on 9.979px from the board the digit names and
       1.5px from the board it does not, measured at 1914x825. `left: 100%` IS the board's right
       edge, so adding the lead to it states the wanted distance directly and nothing else
       contributes to it.

       `right: auto` is required, not tidying: leave chessground's anchor in place and both edges
       are constrained, which makes the width below inert.

       `width: max-content` retires the 12px. The box becomes exactly as wide as the digit in it —
       measured 5.021px against a 5.014px advance — so no slack remains for an alignment to push a
       digit across, and no constant remains that could drift from the file placement beside it.

       This page no longer positions from --ranks-right. It is left at its :root value, which every
       other board on the site still reads. */
    :is(.round-app, .analysis-app).bug .cg-wrap coords.side {
        left: calc(100% + var(--bug-coord-lead));
        right: auto;
        width: max-content;
    }
    /* Files sit IN the gap, at the gap's size — the box as well as the type, or a full-size
       letter in a part-size box is a clipped letter. --files-bottom is kept in step so that
       anything else reading it sees the same answer.

       0.75 of the gap reproduces the natural 0.85em almost exactly at the full 16px overhang
       (11.9px against 12), so a board with room to spare looks as it always has. */
    .round-app.bug .cg-wrap {
        --files-bottom: calc(-1 * var(--bug-coord-gap));
    }
    :is(.round-app, .analysis-app).bug .cg-wrap coords.bottom {
        bottom: var(--files-bottom);
        height: var(--bug-coord-gap);
        text-align: center;
    }
    /* The in-square placement undone: no padding crowding the glyph, chessground's own weight
       and line box back. */
    :is(.round-app, .analysis-app).bug .cg-wrap coords coord {
        font-weight: bold;
        line-height: normal;
        padding: 0;
    }
    :is(.round-app, .analysis-app).bug .cg-wrap coords.bottom coord {
        text-align: center;
    }
    /* `left`, where chessground.css has `right`. Right alignment inside the fixed 12px box was the
       last of the three things putting the digit 9.979px from its own board and 1.5px from its
       neighbour; with `max-content` above there is no slack for it to act on anyway, so this is
       belt-and-braces on an 8x8 board where every rank digit is one width. Declared so the intent
       survives a board with a two-character rank.

       The transform is the nudge that centres a rank label against the board's edge.
       chessground.css applies it under `(min-width: 800px) and (min-height: 500px)`, and it is
       restored unconditionally here because this block cannot be reached outside those bounds: a
       gap needs spare height, which only the tall landscape mode has, and that mode is a subset of
       both. Stated so the assumption is visible rather than silently relied upon. */
    :is(.round-app, .analysis-app).bug .cg-wrap coords.side coord {
        text-align: left;
        transform: translateY(39%);
    }
    /* The type, per board and per axis. Id-scoped because the rules it has to outrank are —
       they read --cg-width-a/b, which are board identity — and a class-only selector would
       lose to them however many classes it carried. */
    :is(.round-app, .analysis-app).bug #mainboard coords.side coord,
    :is(.round-app, .analysis-app).bug #bugboard coords.side coord {
        font-size: 0.85em;
    }
    :is(.round-app, .analysis-app).bug #mainboard coords.bottom coord,
    :is(.round-app, .analysis-app).bug #bugboard coords.bottom coord {
        font-size: calc(var(--bug-coord-gap) * 0.75);
    }
}


/* ---------------------------------------------------------------------------
   A NAME THAT HAS LEFT ITS STRIP

   Two things change once the name is on a line of its own, and both are about the
   room that leaving created.
   --------------------------------------------------------------------------- */

/* The strip's LINES share its slack rather than piling it under the last one. The
   strip is a wrapping row in this state: the pocket and clock on one line, the name
   on the next. `align-content: flex-start` left every spare pixel below them — 40px
   of dead space under the name at 80% zoom — while the clock stayed at its content
   height. Stretching the lines hands that space to the pocket-and-clock line, which
   is where the clock then fills it: measured 51.8 to 71.8, dead space 0. */
.round-app.bug.own-name-outside .own-seat,
.round-app.bug.partner-name-outside .partner-seat {
    align-content: stretch;
}
.round-app.bug.own-name-outside .own-seat .clock-wrap,
.round-app.bug.partner-name-outside .partner-seat .clock-wrap {
    align-self: stretch;
}

/* The doubling that used to be here is gone. It compensated for a square-derived font
   being far too small on a full-width row — a patch on the wrong rule rather than a rule
   of its own — and it is what made portrait's own seat 16.74px and its partner 7.21px,
   one at the ceiling by accident and the other nowhere near it. A name on its own row is
   now larger because its row is wider, which the `cqi` rule above says once for both
   arrangements. */

/* The viewer's own stack, the mirror of `.bug-partner-stack`. Block flow: strip,
   board, strip, in DOM order — which is why round.ts mounts them in that order and
   why a flip swaps the blocks INSIDE the strips rather than the strips themselves.

   `align-self: start` so the stack is its content's height inside a row that may be
   taller, exactly as the partner's is inside its column. What that leaves over is
   where a coordinate gap can go, and it belongs to the stack rather than to a track,
   because the partner's stack has no tracks at all. */
.bug-own-stack {
    grid-area: ownstack;
    align-self: start;
    min-width: 0;
}
