/* ==========================================================================
   THE CHROME MENU — one control, top-right, every surface.

   This replaces a corner rail that had grown to four persistent buttons
   (Feedback, Language, Audio, Full screen), each with its own fixed seat and
   its own reservation token so the next one along would not print on top of
   it. Most of that arithmetic is deleted by there being one button.

   Loaded AFTER language-selector.css and feedback.css on purpose: it re-homes
   the controls those sheets position as free-floating pills, and the overrides
   below have to outrank them. They are scoped under `.chrome-menu-panel`, so
   they are also strictly more specific, and neither sheet can win the corner
   back by being edited later.

   PRESENTATION ONLY. Nothing here changes what a control does — the elements
   inside the panel are the same nodes, with the same listeners, that used to
   sit in the corner.
   ========================================================================== */

/* --------------------------------------------------------------------------
   1. THE SEAT

   One rule, no per-surface variants. The battle used to seat the audio puck
   against the command rail with measured JS, and the full-screen trigger with
   a second, different measured solve; both are gone. `--mf-rail-*` already
   carries the safe-area inset, so the notch is handled here too.
   -------------------------------------------------------------------------- */
#audio-volume-control.chrome-menu {
    position: fixed;
    top: var(--mf-rail-top);
    right: var(--mf-rail-edge);
    z-index: 1000;
    display: block;
    width: var(--mf-rail-btn);
    height: var(--mf-rail-btn);
}

/* Above the mulligan veil during a match, as the audio control already was.
   Deliberately NOT above the tutorial's own messages, which must stay
   readable over everything. */
body.game-active #audio-volume-control.chrome-menu {
    z-index: 10002;
}

/* The surfaces that own their whole screen and carry their own exit: the
   Collection's ✕, the Foundry's Lobby control and its own language select,
   the Store's ✕. Chrome would only compete with them.

   `display: none` on the ROOT, never on the controls inside it — those nodes
   are still owned by i18n.js, feedback.js and fullscreen-toggle.js, and
   deleting them to hide a surface is how a "readiness gate" ends up removing a
   feature. Hidden here means unpainted, not absent. */
/* `!important` earns its place here, and only here. mobile.css is deliberately
   the last sheet loaded and reveals the control on tablets with
   `body:not(.game-active):not(.foundry-surface-active) #audio-volume-control`,
   which is specificity (1,2,1) against this rule's (1,1,0) — so on a tablet in
   the Collection the menu would paint straight through this gate. Raising the
   selector only ties, and a tie goes to the later sheet, which is mobile.css.
   Same pattern feedback.css already uses to keep the Feedback pill out of the
   Foundry. */
#audio-volume-control.chrome-menu[data-menu-context='hidden'] {
    display: none !important;
}

/* Context filtering. A match has no lobby chrome to reach and no room for a
   feedback form, so those sections simply are not painted there — the menu
   never offers an option the surface cannot honour. Language is hidden mid-duel
   for the same reason it always was: `lobby.js` already calls
   `i18n.hideSelector()` on entering a match, so showing the group would frame
   an empty box. */
#audio-volume-control.chrome-menu[data-menu-context='battle'] .chrome-menu-section[data-section='language'],
#audio-volume-control.chrome-menu[data-menu-context='battle'] .chrome-menu-section[data-section='feedback'] {
    display: none;
}

/* And the mirror: there is no match to leave from the lobby. The section is
   also empty outside a battle — the exit only exists in one — but hiding it
   explicitly keeps an empty heading from painting for a frame. */
#audio-volume-control.chrome-menu:not([data-menu-context='battle']) .chrome-menu-section[data-section='lobby'] {
    display: none;
}

/* --------------------------------------------------------------------------
   2. THE TRIGGER

   Same physical material as the rest of the game's glass chrome — this is not
   a new vocabulary, it is the existing one with the craft turned up: a lit rim
   that reads as bevel rather than border, an inner top highlight so the disc
   has a light source, and a bloom that only exists on hover so the resting
   state stays quiet.
   -------------------------------------------------------------------------- */
.chrome-menu .audio-settings-toggle {
    position: relative;
    display: flex;
    align-items: center;
    justify-content: center;
    width: var(--mf-rail-btn);
    height: var(--mf-rail-btn);
    padding: 0;
    margin: 0;
    border: 1px solid rgba(238, 219, 241, 0.42);
    border-radius: 999px;
    background:
        radial-gradient(circle at 50% 22%, rgba(214, 180, 255, 0.20) 0%, transparent 62%),
        linear-gradient(180deg, rgba(46, 26, 66, 0.78) 0%, rgba(12, 6, 20, 0.86) 100%);
    backdrop-filter: blur(8px);
    -webkit-backdrop-filter: blur(8px);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.16),
        inset 0 -1px 0 rgba(0, 0, 0, 0.35),
        0 4px 12px rgba(0, 0, 0, 0.34);
    color: #eedbf1;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition:
        transform 160ms cubic-bezier(0.2, 0.8, 0.3, 1),
        border-color 160ms ease,
        box-shadow 160ms ease,
        background 160ms ease;
}

/* THE MOON IS THE BUTTON, not the glyph.

   A crescent inside the icon was invisible at the 34px the rail actually
   renders (see the note in audio-settings.js). So the lunar read lives here
   instead, where there is room for it: a lit arc along the upper-left rim,
   falling off to nothing by the lower right, exactly as a gibbous limb catches
   light. It is a masked ring rather than a border so the lit part can taper —
   a `border-color` gradient is not a thing CSS can express.

   `mask-composite` is supported in every browser Moonfall targets (Safari 16.4
   is the floor); where it somehow is not, the ring simply fills and the button
   reads as a slightly brighter disc, which is a graceful place to land. */
.chrome-menu .audio-settings-toggle::before {
    content: '';
    position: absolute;
    inset: -1px;
    border-radius: inherit;
    padding: 1.5px;
    background: linear-gradient(
        145deg,
        rgba(255, 246, 214, 0.95) 0%,
        rgba(214, 180, 255, 0.55) 32%,
        rgba(167, 121, 255, 0.10) 58%,
        transparent 74%
    );
    -webkit-mask:
        linear-gradient(#000 0 0) content-box,
        linear-gradient(#000 0 0);
    mask:
        linear-gradient(#000 0 0) content-box,
        linear-gradient(#000 0 0);
    -webkit-mask-composite: xor;
    mask-composite: exclude;
    opacity: 0.9;
    pointer-events: none;
    transition: opacity 200ms ease, transform 320ms cubic-bezier(0.2, 0.8, 0.3, 1);
}

.chrome-menu .audio-settings-toggle:hover::before {
    opacity: 1;
    transform: rotate(-18deg);
}

.chrome-menu .audio-settings-toggle[aria-expanded='true']::before {
    opacity: 1;
    transform: rotate(-38deg);
}

/* The bloom is its own layer so it can fade independently of the disc, and so
   it never inflates the button's box — a growing box in the corner would push
   nothing (it is fixed) but would change the hit target under the pointer. */
.chrome-menu .audio-settings-toggle::after {
    content: '';
    position: absolute;
    inset: -30%;
    border-radius: inherit;
    background: radial-gradient(circle, rgba(167, 121, 255, 0.34) 0%, transparent 68%);
    opacity: 0;
    pointer-events: none;
    transition: opacity 200ms ease;
}

.chrome-menu .audio-settings-toggle:hover {
    border-color: rgba(255, 244, 255, 0.92);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.24),
        inset 0 -1px 0 rgba(0, 0, 0, 0.35),
        0 8px 20px rgba(0, 0, 0, 0.44);
    transform: translateY(-1px);
}

.chrome-menu .audio-settings-toggle:hover::after,
.chrome-menu .audio-settings-toggle[aria-expanded='true']::after {
    opacity: 1;
}

.chrome-menu .audio-settings-toggle:active {
    transform: translateY(0) scale(0.94);
    transition-duration: 90ms;
}

.chrome-menu .audio-settings-toggle:focus-visible {
    outline: none;
    box-shadow:
        0 0 0 3px rgba(238, 219, 241, 0.5),
        0 0 0 6px rgba(124, 90, 255, 0.4);
}

/* --- the sigil ----------------------------------------------------------- */

.chrome-menu-sigil {
    width: 62%;
    height: 62%;
    display: block;
    overflow: visible;
    pointer-events: none;
    /* The mark has to hold its own against the lit rim behind it. */
    filter: drop-shadow(0 1px 1px rgba(0, 0, 0, 0.55));
}

.chrome-menu-sigil-moon,
.chrome-menu-sigil-star {
    transform-origin: 50% 50%;
    transition: transform 300ms cubic-bezier(0.2, 0.8, 0.3, 1), opacity 200ms ease;
}

/* Hover tips the moon and lifts the star — the secondary motion that stops the
   mark reading as a static icon. Two elements moving by different amounts is
   what makes it feel like an object rather than a picture. */
.chrome-menu .audio-settings-toggle:hover .chrome-menu-sigil-moon {
    transform: rotate(-14deg);
}

.chrome-menu .audio-settings-toggle:hover .chrome-menu-sigil-star {
    transform: translate(0.6px, -0.8px) scale(1.2);
}

/* Open: the moon turns further and the star brightens to full — the trigger
   stays a moon rather than morphing into an ✕, which would claim that pressing
   it closes the app rather than the menu. */
.chrome-menu .audio-settings-toggle[aria-expanded='true'] .chrome-menu-sigil-moon {
    transform: rotate(-30deg) scale(1.04);
}

.chrome-menu .audio-settings-toggle[aria-expanded='true'] .chrome-menu-sigil-star {
    transform: translate(1px, -1.2px) scale(1.35);
}

/* Everything silenced. A tell, not a readout — see the note in
   audio-settings.js about why the icon no longer tracks the volume level. */
.chrome-menu[data-muted='true'] .chrome-menu-sigil {
    opacity: 0.55;
}

/* --------------------------------------------------------------------------
   2b. THE DOUBLE-PRESS GESTURE'S RECEIPT — forwarded to the menu.

   `main` gained `fullscreen-gesture.js` while this branch was being built: a
   double-press anywhere inert toggles full screen, and it rings the full-screen
   control afterwards. Its own comment says why — "the press rings THIS control
   — the visible thing responsible, and the place they will look next."

   After the collapse that control is INSIDE the panel, so on a closed menu the
   ring plays on a hidden row and the player gets a screen that rearranges
   itself with no receipt at all. The gesture still works (it calls only
   `element()`, `toggle()` and `isSupported()`, all of which survived), but its
   discoverability cue needed re-pointing at what is now the visible thing
   responsible: the menu trigger.

   Written defensively so it is correct in either merge order — the selector
   simply matches nothing until `fullscreen-gesture.js` lands, and the keyframe
   is defined here rather than borrowed so it does not depend on that file. */
.chrome-menu:has(#fullscreen-toggle.fullscreen-toggle-pulsing)::after {
    content: '';
    position: absolute;
    inset: -2px;
    border-radius: 999px;
    border: 2px solid rgba(238, 219, 241, 0.9);
    opacity: 0;
    pointer-events: none;
    animation: chrome-menu-ping 520ms cubic-bezier(0.22, 1, 0.36, 1);
}

@keyframes chrome-menu-ping {
    from { opacity: 0.9; transform: scale(0.92); }
    to { opacity: 0; transform: scale(1.75); }
}

/* And silence the ring on the ROW, where it would draw a rounded rectangle
   around a list item — usually while the panel is closed and nobody can see it. */
.chrome-menu-panel #fullscreen-toggle.fullscreen-toggle-pulsing::after {
    animation: none;
    opacity: 0;
}

@media (prefers-reduced-motion: reduce) {
    .chrome-menu:has(#fullscreen-toggle.fullscreen-toggle-pulsing)::after {
        animation: none;
    }
}

/* --------------------------------------------------------------------------
   3. THE PANEL
   -------------------------------------------------------------------------- */
.chrome-menu-panel {
    width: clamp(238px, 27vmin, 344px);
    gap: 0;
    padding: clamp(0.7rem, 1vmin, 1.05rem);

    background:
        radial-gradient(ellipse at 100% 0%, rgba(126, 76, 190, 0.30) 0%, transparent 58%),
        linear-gradient(180deg, rgba(30, 17, 48, 0.95) 0%, rgba(12, 6, 21, 0.96) 100%);
    border: 1px solid rgba(238, 219, 241, 0.34);
    border-radius: clamp(13px, 1.15vmin, 18px);
    box-shadow:
        inset 0 1px 0 rgba(255, 255, 255, 0.10),
        0 16px 40px rgba(0, 0, 0, 0.6);

    /* A 300px-tall landscape phone page box cannot hold three sections.
       The cap and the scroll live on the SAME element on purpose: a max-height
       on a parent bounds the box while the content keeps overflowing out of it,
       which is how a capped panel ends up with unreachable rows.

       The budget is measured from the TRIGGER'S BOTTOM, not from the top of
       the rail. The panel opens BELOW the trigger, so subtracting only
       `--mf-rail-top` leaves it a full button-height too tall: measured at
       932x300 the panel opened at y=58 and ran 280px to y=338, i.e. 38px off
       the bottom of a 300px page box, with the Language row landing at y=357
       — entirely off-screen and unreachable. */
    max-height: calc(
        100dvh - var(--mf-rail-top) - var(--mf-rail-btn)
        - var(--mf-safe-bottom, 0px) - 1.5rem
    );
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* The hairline that makes the panel read as a forged object rather than a
   rounded rectangle: one lit edge along the top, brightest where the trigger
   is, fading out across the width. */
.chrome-menu-panel::before {
    content: '';
    position: absolute;
    top: 0;
    right: 0;
    left: 0;
    height: 1px;
    border-radius: inherit;
    background: linear-gradient(270deg, rgba(214, 180, 255, 0.85) 0%, rgba(214, 180, 255, 0.12) 55%, transparent 100%);
    pointer-events: none;
}

/* The battle panel is a ROW on main (two channels side by side, to fit a short
   landscape page box). With sections it must return to a column, or the
   headings sit beside their own rows. The short-viewport case is handled by
   the max-height above and by battle having only two sections. */
body.game-active .chrome-menu-panel {
    width: clamp(238px, 27vmin, 344px);
    flex-direction: column;
    align-items: stretch;
    gap: 0;
    padding: clamp(0.7rem, 1vmin, 1.05rem);
}

body.game-active .chrome-menu-panel .audio-channel-row {
    flex: 0 0 auto;
}

/* --- sections ------------------------------------------------------------ */

/* NEVER COMPRESS — SCROLL.

   `.audio-settings-panel` is a column flex container, so its sections are flex
   items and inherit `flex-shrink: 1`. On a 300px-tall landscape phone page box
   the panel hits its `max-height` and the sections duly squashed, taking the
   rows down with them: the full-screen row measured 43.57px against the 44px
   touch floor on every phone profile in all three engines. The panel already
   owns `overflow-y: auto` — this makes it use it. */
.chrome-menu-section {
    flex: 0 0 auto;
}

.chrome-menu-section + .chrome-menu-section {
    margin-top: clamp(0.55rem, 0.9vmin, 0.9rem);
    padding-top: clamp(0.55rem, 0.9vmin, 0.9rem);
    border-top: 1px solid rgba(238, 219, 241, 0.13);
}

/* ONE INK FOR EVERYTHING IN HERE.

   Reported: "all elemetns should have same color. do nto highlight some options
   more than others. do nto have different colored text."

   The headings used to be violet (`rgba(214,180,255,.72)`) against pale-lilac
   rows, and the exit had a red hover. Both were doing the same wrong thing —
   ranking the contents by colour when the player is scanning a short list of
   equals. Hierarchy is carried by SIZE and CASE alone now: the headings are
   small caps at the same colour, held back only by opacity. */
.chrome-menu-heading {
    margin: 0 0 clamp(0.35rem, 0.6vmin, 0.55rem);
    font-size: clamp(0.58rem, 0.82vmin, 0.72rem);
    font-weight: 700;
    letter-spacing: 0.16em;
    text-transform: uppercase;
    color: rgba(238, 219, 241, 0.94);
    opacity: 0.6;
    -webkit-user-select: none;
    user-select: none;
}

.chrome-menu-section-body {
    display: flex;
    flex-direction: column;
    gap: clamp(0.3rem, 0.55vmin, 0.5rem);
}

/* The audio rows come from soundtrack.css with three different inks: the label
   at .92, the percentage at .70, and the mute glyph at whatever the emoji is.
   Inside the panel they join everything else at one colour, so a row does not
   read as "label plus a greyed-out afterthought". */
.chrome-menu-panel .audio-channel-label,
.chrome-menu-panel .audio-channel-pct {
    color: rgba(238, 219, 241, 0.94);
}

/* The drawn speaker inherits the row's ink. Sized from the same token as the
   other row icons so the column of marks reads as one set. */
.chrome-menu-panel .audio-mute-svg {
    width: clamp(15px, 1.5vmin, 19px);
    height: clamp(15px, 1.5vmin, 19px);
    display: block;
}

.chrome-menu-panel .audio-mute-btn {
    color: rgba(238, 219, 241, 0.94);
}

/* THE FLAG IS THE LAST THING COLOURING ITSELF.

   `i18n.js` prefixes the language button with a flag emoji, and an emoji paints
   its own colours — it cannot take the panel's ink. With the mute glyphs drawn
   it was the only coloured mark left in a column the owner asked to be one
   colour. Hidden rather than removed from i18n: the flag is still the right
   affordance in the DROPDOWN, where a player is scanning eight languages and
   the shapes help. It is the collapsed BUTTON, sitting in a list of five plain
   rows, where it stood out. */
.chrome-menu-panel .language-btn .language-flag {
    display: none;
}

/* AND THE NAME COMES BACK.

   `mobile.css` hides `.language-btn-name` on a phone — correct for the
   free-floating corner pill it was written for, which had to stay narrow
   enough not to collide with the mute toggle beside it. That pill no longer
   exists: after the fold the selector is a full-width row in this panel.

   Left alone, that rule and the one above hid one half of the label each, so
   the row painted a chevron and nothing else. Reported: "the selected language
   has a bug in some cases. e.g. i ve seen it on mobile look empty, even tho
   actually english is already selected." The selection was never wrong — the
   name was invisible.

   SPECIFICITY, NOT ORDER. mobile.css is deliberately the last sheet loaded, so
   a tie would go to it. Three classes against one wins regardless of load
   order, which is what keeps this correct if the sheets are ever reordered. */
.chrome-menu-panel .language-btn .language-btn-name {
    display: inline;
}

/* A SECTION WITH NOTHING IN IT DOES NOT PAINT ITS HEADING.

   iOS SAFARI HAS NO ELEMENT FULLSCREEN API. `fullscreen-toggle.js` checks for
   it and correctly declines to build a control that could not work — the same
   fact `chrome-menu.js` documents, where one `&&` over that always-false
   adopter once cost the iPhone its Language and Feedback rows. What nothing
   handled is the hole it leaves HERE: the Display section still renders, so
   every iPhone has been shown a "DISPLAY" heading with nothing underneath it,
   at every viewport, since the four corner controls were folded into a menu.

   Measured with Playwright WebKit under `isMobile: true`, which is what
   emulates iOS: `document.fullscreenEnabled` is undefined, `#fullscreen-toggle`
   is never created, and the section body matches `:empty`. Under WebKit WITHOUT
   `isMobile` the API is present and the button exists — which is exactly why
   the offline `phone-fit-webkit` project could not see this and the real
   page-box run could.

   Stated as a general rule rather than a Display special case: any section
   whose body ends up empty — a control a browser cannot support, a future row
   behind a capability check — stops advertising itself. It is deliberately NOT
   inside the short-landscape block, because this is about a missing capability,
   not a small screen. (`:has()` is already a dependency of this file, in the
   double-press receipt above.) */
.chrome-menu-panel .chrome-menu-section:has(> .chrome-menu-section-body:empty) {
    display: none;
}

/* --------------------------------------------------------------------------
   4. ROWS — the adopted controls

   Full screen and Feedback arrive as a corner puck and a lobby pill. They are
   re-cut here into one row shape so the panel reads as a single object rather
   than three donated controls in a box.
   -------------------------------------------------------------------------- */
.chrome-menu-panel .chrome-menu-row {
    position: relative;
    flex: 0 0 auto;
    display: flex;
    align-items: center;
    justify-content: flex-start;
    gap: clamp(0.5rem, 0.8vmin, 0.75rem);
    width: 100%;
    min-height: 40px;
    /* Touch hardware gets the full 44px target; see the coarse-pointer block
       at the end of this file. */
    padding: 0 clamp(0.5rem, 0.8vmin, 0.7rem);
    border: 1px solid transparent;
    border-radius: clamp(8px, 0.8vmin, 12px);
    background: rgba(238, 219, 241, 0.055);
    box-shadow: none;
    color: rgba(238, 219, 241, 0.94);
    font-family: inherit;
    font-size: clamp(0.76rem, 1.05vmin, 0.98rem);
    font-weight: 600;
    letter-spacing: 0.01em;
    text-align: left;
    cursor: pointer;
    -webkit-tap-highlight-color: transparent;
    transition: background 150ms ease, border-color 150ms ease, transform 150ms cubic-bezier(0.2, 0.8, 0.3, 1);
}

.chrome-menu-panel .chrome-menu-row:hover {
    background: rgba(238, 219, 241, 0.13);
    border-color: rgba(238, 219, 241, 0.3);
    transform: translateX(2px);
}

.chrome-menu-panel .chrome-menu-row:active {
    transform: translateX(2px) scale(0.985);
}

.chrome-menu-panel .chrome-menu-row:focus-visible {
    outline: none;
    border-color: rgba(238, 219, 241, 0.6);
    box-shadow: 0 0 0 3px rgba(124, 90, 255, 0.45);
}

/* The two adopted controls arrive with their own sizing — the full-screen
   trigger is a 34-60px circle, the feedback pill is an inline-flex capsule
   that the phone sheet shrinks to an icon-only square. Both are overridden to
   the row shape, including the width floors, which would otherwise leave a
   44px stub in a 300px-wide panel. */
.chrome-menu-panel #fullscreen-toggle,
.chrome-menu-panel #feedback-link,
.chrome-menu-panel #skip-tutorial-btn,
.chrome-menu-panel #debug-end-match-btn,
.chrome-menu-panel #debug-win-match-btn,
.chrome-menu-panel #debug-reveal-hand-btn {
    position: static;
    inset: auto;
    z-index: auto;
    width: 100%;
    min-width: 0;
    height: auto;
    margin: 0;
    backdrop-filter: none;
    -webkit-backdrop-filter: none;
}

/* THE DEV CONCEDE AND DEV WIN (DEBUG_MODE only) are rows now, not slabs pinned
   into the command rail — see `adoptDebugEndMatch()` in chrome-menu.js for why
   they left. They keep their dashed debug skins (red concede, gold win), because
   the one thing they must never do is read as real HUD; only their geometry
   becomes a row. `transform: none` is an explicit reset rather than a fix for
   anything: game-board.css still carries `transition: all 0.2s ease` on these
   buttons, so any transform arriving from a future `.chrome-menu-row` rule would
   ANIMATE into place inside a list, and the row's own hover slide is declared
   below. */
.chrome-menu-panel #debug-end-match-btn,
.chrome-menu-panel #debug-win-match-btn,
.chrome-menu-panel #debug-reveal-hand-btn {
    justify-content: flex-start;
    display: flex;
    align-items: center;
    gap: clamp(0.3rem, 0.5vmin, 0.5rem);
    min-height: 40px;
    padding: 0 clamp(0.5rem, 0.8vmin, 0.7rem);
    border-radius: clamp(8px, 0.8vmin, 12px);
    font-size: clamp(0.76rem, 1.05vmin, 0.98rem);
    letter-spacing: 0.01em;
    transform: none;
}

.chrome-menu-panel #debug-end-match-btn::before,
.chrome-menu-panel #debug-win-match-btn::before,
.chrome-menu-panel #debug-reveal-hand-btn::before {
    content: '⚑';
    flex: 0 0 auto;
    width: clamp(15px, 1.5vmin, 19px);
    font-size: 1.05em;
    line-height: 1;
    text-align: center;
    opacity: 0.85;
}

/* THE DEV REVEAL IS A DESKTOP ROW. It exists to try a revealed card's hover by
   hand, and a phone has no hover; more to the point the in-match tray on the
   shortest landscape page box (844x280) fits the two dev rows with ~7px to
   spare, and a third row would put the bottom of the menu behind a scroll
   nobody can see (`debug-end-match.real.spec.js` measures exactly this). On a
   coarse pointer the row is not rendered at all, so the DEBUG build's phone
   tray lays out as it did before this control existed. The tap path of a
   revealed card is proven by the mobile seats of enemy-reveal-hover.spec.js. */
@media (pointer: coarse) {
    .chrome-menu-panel #debug-reveal-hand-btn {
        display: none;
    }
}

.chrome-menu-panel #debug-win-match-btn::before {
    content: '★';
}

.chrome-menu-panel #debug-end-match-btn:hover,
.chrome-menu-panel #debug-win-match-btn:hover {
    transform: translateX(2px);
}

.chrome-menu-panel #fullscreen-toggle .fullscreen-toggle-glyph,
.chrome-menu-panel .feedback-link-icon {
    flex: 0 0 auto;
    width: clamp(15px, 1.5vmin, 19px);
    height: clamp(15px, 1.5vmin, 19px);
    opacity: 0.85;
}

/* The phone sheet visually-hides this label to fit the corner. Inside the
   panel there is a whole row for it, so it comes back — otherwise Feedback is
   an unlabelled icon in a list of labelled rows. */
.chrome-menu-panel .feedback-link-label {
    position: static;
    width: auto;
    height: auto;
    padding: 0;
    margin: 0;
    overflow: visible;
    clip-path: none;
    white-space: nowrap;
}

/* THE IN-MATCH EXIT arrives wearing `.game-command-btn`: a 48px-tall slab with
   a 2px border, a gradient, its own drop shadow and a `translateY` transition
   built for the command rail. Inside the panel it has to be the same row shape
   as everything else, so every one of those is overridden — including the
   transform, which would otherwise fight the row's own hover slide. */
.chrome-menu-panel #skip-tutorial-btn {
    /* `.game-command-btn` centres its label, which is right for a slab on the
       command rail and wrong in a list: every other row starts at the same left
       edge, and one centred row breaks the column the eye is reading down. */
    justify-content: flex-start;
    min-height: 40px;
    padding: 0 clamp(0.5rem, 0.8vmin, 0.7rem);
    border: 1px solid transparent;
    border-radius: clamp(8px, 0.8vmin, 12px);
    background: rgba(238, 219, 241, 0.055);
    box-shadow: none;
    color: rgba(238, 219, 241, 0.94);
    font-size: clamp(0.76rem, 1.05vmin, 0.98rem);
    font-weight: 600;
    letter-spacing: 0.01em;
    text-shadow: none;
    transform: none;
}

/* An arrow, so the row carries a mark like every other one. It has to be a
   pseudo-element: lobby.js owns this button's `textContent` — it rewrites it
   between "Exit Match" and "Skip Tutorial" — so any child node appended here
   would be wiped on the next relabel. */
.chrome-menu-panel #skip-tutorial-btn::before {
    content: '←';
    flex: 0 0 auto;
    width: clamp(15px, 1.5vmin, 19px);
    font-size: 1.05em;
    line-height: 1;
    text-align: center;
    opacity: 0.85;
}

/* The exit gets the SAME hover as every other row. It briefly had a red one —
   "destructive action" reasoning — and that is exactly the highlighting the
   owner ruled out. A confirm dialog already guards the action; the row does not
   need to shout. */
.chrome-menu-panel #skip-tutorial-btn:hover {
    border-color: rgba(238, 219, 241, 0.3);
    background: rgba(238, 219, 241, 0.13);
    transform: translateX(2px);
}

/* `#fullscreen-toggle` carries no text node of its own — its state lives in
   `aria-label` and `data-state`, read back from the document after every
   attempt. The visible label is generated here from that same state, so the
   row can never disagree with what the button will actually do. */
.chrome-menu-panel #fullscreen-toggle::after {
    content: attr(aria-label);
    flex: 1 1 auto;
    min-width: 0;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;

    /* EVERY ONE OF THESE IS A RESET, NOT A STYLE, and they are why the label
       survives the merge with `fullscreen-gesture.js`. That branch gives
       `.fullscreen-toggle::after` a life of its own — an absolutely positioned,
       `opacity: 0`, 2px-bordered ring for the double-press receipt. This rule
       out-specifies it on `content`, but a property it does not name is
       inherited from that rule: without these the row's label would become an
       invisible, absolutely positioned, bordered box, i.e. the full-screen row
       would silently lose its text. Found by reading the incoming diff, not by
       a failing test — no test covers a merge that has not happened yet. */
    position: static;
    inset: auto;
    border: 0;
    border-radius: 0;
    opacity: 1;
    transform: none;
    animation: none;
}

/* --------------------------------------------------------------------------
   5. THE ADOPTED LANGUAGE SELECTOR

   i18n.js positions this as a fixed pill in the corner. Inside the panel it is
   an ordinary row, and its dropdown has to open within the panel rather than
   at a viewport-fixed offset.
   -------------------------------------------------------------------------- */
.chrome-menu-panel #language-selector {
    position: static;
    inset: auto;
    z-index: auto;
    display: block;
    width: 100%;
    height: auto;
}

.chrome-menu-panel .language-btn {
    flex: 0 0 auto;
    width: 100%;
    height: auto;
    min-height: 40px;
    padding: 0 clamp(0.5rem, 0.8vmin, 0.7rem);
    border: 1px solid transparent;
    border-radius: clamp(8px, 0.8vmin, 12px);
    background: rgba(238, 219, 241, 0.055);
    box-shadow: none;
    color: rgba(238, 219, 241, 0.94);
    font-size: clamp(0.76rem, 1.05vmin, 0.98rem);
    font-weight: 600;
    justify-content: flex-start;
    gap: clamp(0.5rem, 0.8vmin, 0.75rem);
    transition: background 150ms ease, border-color 150ms ease, transform 150ms cubic-bezier(0.2, 0.8, 0.3, 1);
}

.chrome-menu-panel .language-btn:hover {
    background: rgba(238, 219, 241, 0.13);
    border-color: rgba(238, 219, 241, 0.3);
    transform: translateX(2px);
    box-shadow: none;
}

.chrome-menu-panel .language-dropdown {
    position: static;
    width: 100%;
    /* `svh`, never bare `vh`: on a phone `100vh` is the viewport with the
       browser bars HIDDEN, which is taller than what the player can see. */
    max-height: 42svh;
    margin-top: clamp(0.3rem, 0.55vmin, 0.5rem);
    overflow-y: auto;
    overscroll-behavior: contain;
}

/* --------------------------------------------------------------------------
   6. MOTION

   The panel springs from the trigger's corner rather than fading in place, so
   it reads as unfolding FROM the button that opened it. Sections stagger in
   behind it — the secondary motion that separates a designed panel from a
   div that appeared.
   -------------------------------------------------------------------------- */
.chrome-menu-panel {
    transform-origin: top right;
    animation: chrome-menu-in 220ms cubic-bezier(0.2, 0.9, 0.28, 1);
}

@keyframes chrome-menu-in {
    from { opacity: 0; transform: translateY(-6px) scale(0.94); }
    to { opacity: 1; transform: translateY(0) scale(1); }
}

.chrome-menu-panel .chrome-menu-section {
    animation: chrome-menu-section-in 260ms cubic-bezier(0.2, 0.9, 0.28, 1) backwards;
}

.chrome-menu-panel .chrome-menu-section:nth-child(1) { animation-delay: 40ms; }
.chrome-menu-panel .chrome-menu-section:nth-child(2) { animation-delay: 80ms; }
.chrome-menu-panel .chrome-menu-section:nth-child(3) { animation-delay: 120ms; }
.chrome-menu-panel .chrome-menu-section:nth-child(4) { animation-delay: 160ms; }

@keyframes chrome-menu-section-in {
    from { opacity: 0; transform: translateY(6px); }
    to { opacity: 1; transform: translateY(0); }
}

@media (prefers-reduced-motion: reduce) {
    .chrome-menu-panel,
    .chrome-menu-panel .chrome-menu-section {
        animation: none;
    }

    .chrome-menu .audio-settings-toggle,
    .chrome-menu .audio-settings-toggle::before,
    .chrome-menu .audio-settings-toggle::after,
    .chrome-menu-sigil-moon,
    .chrome-menu-sigil-star,
    .chrome-menu-panel .chrome-menu-row,
    .chrome-menu-panel .language-btn {
        transition: none;
    }

    /* The state still has to be legible without the motion that normally
       carries it, so the open/hover treatments keep their colour and opacity
       changes and lose only the movement. */
    .chrome-menu .audio-settings-toggle:hover,
    .chrome-menu .audio-settings-toggle:hover::before,
    .chrome-menu .audio-settings-toggle[aria-expanded='true']::before,
    .chrome-menu .audio-settings-toggle:active,
    .chrome-menu-panel .chrome-menu-row:hover,
    .chrome-menu-panel .chrome-menu-row:active,
    .chrome-menu-panel .language-btn:hover {
        transform: none;
    }
}

/* --------------------------------------------------------------------------
   7. TOUCH

   44px minimum on every row, and the panel widened slightly so a thumb has
   somewhere to land that is not a row edge.
   -------------------------------------------------------------------------- */
@media (hover: none), (pointer: coarse) {
    .chrome-menu-panel .chrome-menu-row,
    .chrome-menu-panel .language-btn {
        min-height: var(--mf-touch, 44px);
    }

    .chrome-menu-panel .chrome-menu-row:hover,
    .chrome-menu-panel .language-btn:hover {
        transform: none;
    }
}

/* --------------------------------------------------------------------------
   8. THE SHORT-LANDSCAPE TRAY

   Reported: "menu button dropwon doesn't fit screen height on mobile devices.
   even worse when spece is taken by browser bars ... its really odd to have to
   scroll on a dropdown if possible at all, and users wont even know they have
   to do it."

   MEASURED, on the owner's own iPhone 13 page box (844x280) and every other
   landscape profile in `mobileViewports.js`:

       content 436px   ·   visible box 190px   ·   overflow 246px

   Fifty-six per cent of the panel was behind a scroll gesture nothing
   advertised, and it was worse than "scrolling": Full screen, Language and
   Feedback were ENTIRELY below the panel's visible box on every landscape
   phone. A player could not see that those rows existed.

   WHY A DROPDOWN COLUMN IS THE WRONG SHAPE HERE, and what replaces it.

   The panel is anchored under a 34px trigger in the top-right corner, so on a
   280px page box it starts with 190px and spends it on a 300px-wide column —
   while 750px of safe WIDTH sits empty beside it. The fix is to stop stacking
   in the axis that has no room. On a short landscape viewport the panel
   becomes a two-column TRAY: same object, same rows, same adopted nodes, laid
   out across the width the phone actually has.

   GRID, NOT `columns`. Multi-column was the obvious mechanism and it is a
   trap: a multicol container with a definite `max-height` does not scroll its
   overflow, it FRAGMENTS INTO MORE COLUMNS and overflows sideways — so the
   safety net below would have turned a vertical overflow into a horizontal
   one. Grid places deterministically and leaves `overflow-y` behaving the way
   the rest of the panel expects.

   `overflow-y: auto` STAYS. It is a safety net, not the design: a locale with
   a taller row, or a fifth section added later, must scroll rather than be
   clipped. `chrome-menu-fit.spec.js` asserts it never actually engages, so a
   regression is caught as a failing test rather than as a silent scrollbar.

   NOTHING HERE IS A JS CHANGE. `positionPanel()` in audio-settings.js already
   clamps the panel inside the safe box, so a wider panel seats itself flush
   against the right safe edge and unfolds leftward on its own.
   -------------------------------------------------------------------------- */
@media (orientation: landscape) and (max-height: 560px) {
    /* TWO THINGS IN THIS SELECTOR, AND BOTH ARE LOAD-BEARING.

       ID + CLASS, not the bare class: `soundtrack.css` pins the panel with
       `body:not(.foundry-surface-active) .audio-settings-panel { width: min(80vw, 300px) }`
       at (0,2,1) — a 300px column is exactly what this block exists to widen,
       and `.chrome-menu-panel` at (0,1,0) silently lost to it. Measured: the
       tray laid out in two columns as intended and stayed 300px wide anyway.

       `:not([hidden])`, because raising the specificity to win that fight also
       wins a fight it must LOSE. The panel is hidden by
       `.audio-settings-panel[hidden] { display: none }` at (0,2,0), and setting
       `display` here out-ranked it — so the closed panel stayed laid out,
       invisible, on top of its own trigger, and the menu could not be opened at
       all. Twice: the first repair guarded it at (1,2,0), which the
       `body.game-active` variant below then beat at (1,2,1) and broke the match
       panel only. Scoping the LAYOUT to the OPEN panel ends the argument
       instead of trying to out-specify it — a closed panel simply matches
       nothing here, at any specificity. Both failures were caught by
       `chrome-menu-fit.spec.js`, as a click that timed out against
       "<div hidden ...> subtree intercepts pointer events". */
    #audio-settings-panel.chrome-menu-panel:not([hidden]),
    body.game-active #audio-settings-panel.chrome-menu-panel:not([hidden]) {
        /* The width the phone actually has, minus the notch and a margin. */
        /* 34rem, not 32: at 32 the tightest locale ("Efectos de sonido") left
           its slider at 62px against a 60px floor — passing, with 2px of
           margin, which is not a margin. The extra 2rem puts every active
           language back above 75px. */
        width: min(
            34rem,
            calc(100vw - var(--mf-safe-left, 0px) - var(--mf-safe-right, 0px) - 1.25rem)
        );

        display: grid;
        /* NOT `1fr 1fr`. Auto-flow puts Audio and Language in the first column
           and Display and Feedback in the second, and those two halves want
           very different widths: a channel row is [mute 44][label][slider][%],
           four things, while the other column holds two single-label rows. At
           an even split the channel row ran out of room and ellipsised its own
           label — "Sound effects" rendered as "Sound eff…", losing the word
           that tells it apart from the row above. The slider column gets the
           extra width because it is the one with something to fit. */
        grid-template-columns: 1.2fr 1fr;
        gap: 0.45rem 0.85rem;
        align-content: start;

        /* Declared once because the language sub-sheet below insets itself by
           exactly this to cover the tray. Two literals would drift apart the
           first time either is tuned. */
        --mf-tray-pad-y: 0.55rem;
        --mf-tray-pad-x: 0.65rem;
        padding: var(--mf-tray-pad-y) var(--mf-tray-pad-x);

        /* Every pixel between the trigger's bottom and the safe bottom edge.
           The 1.5rem of slack the single-column budget carried was 24px of a
           190px box — it is spent on content here instead. */
        max-height: calc(
            100dvh - var(--mf-rail-top) - var(--mf-rail-btn)
            - var(--mf-safe-bottom, 0px) - 0.5rem
        );
    }

    /* Grid children do not shrink below their content without this, and a
       long localized label would push its column wider than its track.
       (No `break-inside` here: that is a multicol/print property and does
       nothing in a grid — the section-per-cell placement is what keeps a
       section whole.) */
    .chrome-menu-panel .chrome-menu-section {
        min-width: 0;
    }

    /* THE DIVIDER IS WRONG IN A GRID. `+ .chrome-menu-section` draws a rule
       above every section but the first — which in two columns lands above
       Display at the TOP of column two, reading as a stray hairline rather
       than as a separator. The column gap and the headings already do the
       grouping work here. */
    .chrome-menu-panel .chrome-menu-section + .chrome-menu-section {
        margin-top: 0;
        padding-top: 0;
        border-top: 0;
    }

    .chrome-menu-panel .chrome-menu-heading {
        margin-bottom: 0.2rem;
        font-size: 0.56rem;
        letter-spacing: 0.14em;
    }

    .chrome-menu-panel .chrome-menu-section-body {
        gap: 0.3rem;
    }

    /* ONE LINE PER CHANNEL. The label sits ABOVE the slider everywhere else,
       which costs 79px per channel — 158px of a 190px box for the audio
       section alone. Inline, a channel row is the same 44px as every other
       row in the panel and reads as part of the same list.

       The mute button, the slider's 10px hit padding and the percentage are
       all untouched: this moves the label, nothing else. */
    .chrome-menu-panel .audio-channel-main {
        flex-direction: row;
        align-items: center;
        gap: 0.5rem;
    }

    /* ONE LABEL COLUMN FOR BOTH CHANNELS.

       Each row sized its own label to its own text, and the slider takes only
       what is left — so the two bars came out different lengths. Measured at
       844x280 with the tray open: Music 146.8px against Sound effects 86.6px
       (153.9 / 103.4 in Chromium). Two controls that do the same job at
       visibly different lengths read as a layout accident, and the shorter one
       is also the smaller drag target.

       SUBGRID, NOT `display: contents`. Both put the four cells — mute, label,
       slider, percentage — into one shared grid so an `auto` label track sizes
       to the LONGER of the two labels and both sliders match for free. But
       `display: contents` on the ROW removes its box, and the panel counts its
       own rows by exactly that: `chrome-menu-fit.spec.js` reported `audio:0`
       in-flow rows and failed six ways, which is the same shape as the empty
       "LOBBY" heading this file documents further down. Subgrid keeps the row's
       box and only borrows the parent's tracks. Its inner wrapper has nothing
       to count, so that one may collapse.

       Scoped to the tray, where the row is horizontal — the desktop popover
       keeps its stacked layout untouched. */
    .chrome-menu-panel .chrome-menu-section[data-section='audio'] .chrome-menu-section-body {
        display: grid;
        grid-template-columns: auto auto minmax(3.75rem, 1fr) auto;
        column-gap: 0.5rem;
        row-gap: 0.3rem;
    }

    .chrome-menu-panel .chrome-menu-section[data-section='audio'] .audio-channel-row {
        grid-column: 1 / -1;
        grid-template-columns: subgrid;
        align-items: center;
    }

    .chrome-menu-panel .chrome-menu-section[data-section='audio'] .audio-channel-main {
        display: contents;
    }

    /* THE LABEL GETS ITS NATURAL WIDTH; THE SLIDER TAKES WHAT IS LEFT.

       With the slider on a 4.5rem basis it competed with the label for the
       same space and won, so "Sound effects" ellipsised to "Sound ef…" — the
       row lost the word that distinguishes it from the one above it. A basis
       of 0 on the slider means it claims only LEFTOVER space, which is exactly
       the priority these two have: the label is information, the track is a
       control that works at any width down to its floor. */
    .chrome-menu-panel .audio-channel-label {
        flex: 0 1 auto;
        min-width: 0;
    }

    .chrome-menu-panel .audio-slider {
        flex: 1 1 0;
        /* The floor a 0-100 range stops being draggable below. Asserted per
           locale by `chrome-menu-fit.spec.js`, because a long translation
           shrinks this and nothing else. */
        min-width: 3.75rem;

        /* THE 20px THAT MADE A CHANNEL ROW 64px TALL, AND WHY DROPPING IT
           COSTS NO TOUCH TARGET.

           `soundtrack.css` gives the slider `min-height: 44px` for touch AND
           `padding: 10px 0` as extra hit area, with `box-sizing: content-box`
           — so the border box is 44 + 20 = 64. The padding was written for a
           5-9px hairline track with no min-height beside it; once the 44px
           floor arrived the two do the same job, and the row pays for both.

           Zeroing the padding here leaves a 44px hit target, which is the
           floor AGENTS.md and the touch guidelines actually require. The two
           channels come down 20px each — 40px of a 190px box. */
        padding: 0;
    }

    /* THE LANGUAGE LIST IS A SUB-SHEET, NOT A ROW THAT GROWS.

       In flow the open dropdown adds up to 42svh to a panel that has no spare
       pixel, so opening it re-introduced exactly the scrolling this block
       exists to remove — and nested it, a scrolling list inside a scrolling
       panel. Lifted out of flow it covers the tray it opened from, scrolls on
       its own, and the panel behind it does not move at all. */
    .chrome-menu-panel .language-dropdown {
        position: absolute;
        inset: var(--mf-tray-pad-y) var(--mf-tray-pad-x);
        z-index: 2;
        margin-top: 0;
        max-height: none;
        border-radius: inherit;

        /* `width: 100%` from the in-flow rule above OVER-CONSTRAINS an
           absolutely positioned box: left, right and width cannot all be
           honoured, so the browser drops `right` and the list ran 11px past the
           panel's own right edge — off screen entirely on the two profiles
           whose panel already sits flush against it (667x275, 915x336).
           `auto` hands the sizing back to the two insets. */
        width: auto;
    }

    /* THE DEV ROWS TURN THE LOBBY SECTION INTO ONE FULL-WIDTH ROW OF THREE.

       The tray's row heights are SHARED across its two columns, so every extra
       row the Lobby column stacks costs the whole tray 44px — and the tray has
       no spare pixel: at 844x280 it is a 198px box holding 172px of content.
       Measured with the dev rows stacked under Go to Lobby (Chrome, page-box
       projects): the concede alone already ran 21px past the panel's bottom
       edge (239-279 in a box ending at 258), and the win row 65px past it, on
       four of the six landscape page boxes. The Display column's second cell
       sat empty the whole time.

       So when the dev rows are present the Lobby section takes BOTH columns
       and lays its three commands out side by side — exit, concede, win — at
       the same 44px as every other row. The tray is 189px at the tightest box.
       A long localized label wraps to two lines INSIDE the 44px floor rather
       than growing the row, so the fit does not depend on the locale.

       `:has()` on the dev concede, not a body class: the rows only ever enter
       the Lobby body when `adoptDebugEndMatch()` folds them in, which is gated
       on CLIENT_DEBUG_MODE — so in a shipping build this selector matches
       nothing and the production tray is byte-for-byte the one measured by
       chrome-menu-fit.spec.js. The desktop popover keeps stacked rows: it has
       the height, and this block is the landscape tray only. */
    .chrome-menu-panel .chrome-menu-section[data-section='lobby']:has(#debug-end-match-btn) {
        grid-column: 1 / -1;
    }

    .chrome-menu-panel .chrome-menu-section[data-section='lobby']:has(#debug-end-match-btn) .chrome-menu-section-body {
        display: grid;
        grid-template-columns: repeat(3, minmax(0, 1fr));
        column-gap: 0.5rem;
    }

    .chrome-menu-panel .chrome-menu-section[data-section='lobby']:has(#debug-end-match-btn) .chrome-menu-row {
        min-width: 0;
        white-space: normal;
        line-height: 1.15;
        text-align: left;
    }
}
