/* ══════════════════════════════════════════════════════════════════════════
   MOBILE LAYOUT — authoritative phone layer.

   Loaded LAST (after app.css and silk.css) so it wins the cascade cleanly: the
   two older sheets each lay the body out with their own desktop grid, and silk
   (loaded after app) was overriding app's mobile grid — leaving the phone with a
   half-desktop layout that overflowed and collided. This file owns the phone
   layout end to end so there is one place that decides it, and desktop is never
   touched (everything is inside the phone media query).

   Design: viewport-first. The 3D stage fills the screen; the top bar is a single
   tidy row; each bottom-tab panel rises as a FULL-WIDTH bottom sheet over the
   lower ~72% (the stage stays visible above it) and is dismissed by the scrim,
   the ✕, or re-tapping its tab.
   ══════════════════════════════════════════════════════════════════════════ */

@media (max-width: 820px) {

  /* ── Shell: full-screen stage, one top row, nothing below ────────────────
     `!important` here is deliberate and load-bearing — it overrides silk.css's
     base `body { grid-template-rows: 56px 1fr 172px }`, which otherwise keeps a
     third desktop row on the phone. */
  html, body { max-width: 100vw; overflow-x: hidden; }
  body {
    grid-template-columns: 1fr !important;
    /* The top row grows by exactly the notch inset, so the taller bar fits its own row instead
       of spilling over the stage below it. Both halves of this have to move together or the fix
       for the notch becomes a new overlap. */
    grid-template-rows: calc(52px + env(safe-area-inset-top, 0px)) 1fr !important;
    gap: 0 !important;
    padding: 0 !important;
  }
  #main {
    display: block !important;
    position: relative;
    grid-column: 1 !important;
    min-width: 0; max-width: 100vw;
    height: 100%; min-height: 0;
    overflow: hidden;
  }
  .gutter { display: none !important; }
  #center { display: block !important; height: 100%; }
  #viewport-wrap { position: absolute !important; inset: 0 !important; height: 100% !important; border-radius: 0 !important; }

  /* ── Top bar: one row that always fits 375px ─────────────────────────────
     Order across the row: brand · (flex spacer) · undo/redo · New/Open/Save ·
     owl · account. Everything is sized so the account avatar — the profile menu
     — is never pushed off the right edge (the bug that hid the profile). */
  #topbar {
    /* The notch lives here. With viewport-fit=cover the inset is real, so the bar is pushed clear
       of it instead of drawing the account avatar and New/History/Save underneath it. */
    padding: env(safe-area-inset-top, 0px) 8px 0 !important;
    height: calc(52px + env(safe-area-inset-top, 0px)) !important;
    box-sizing: border-box;
    gap: 4px !important;
    border-radius: 0 !important; border: 0 !important;
    min-width: 0; max-width: 100vw;
    overflow: visible;   /* NOT hidden — the account dropdown (position:absolute inside #account) must
                            not be clipped by the bar; the width is already made to fit, so nothing spills */
  }
  #topbar .brand span { display: none !important; }        /* mark only */
  #topbar .brand .mark { width: 24px; height: 24px; }
  #topbar .tb-sep { display: none !important; }
  #topbar .tb { padding: 4px 8px !important; font-size: 12px !important; }
  #topbar .tb-icon { padding: 4px 6px !important; font-size: 15px !important; }
  #topbar .tb-files { gap: 1px !important; }
  #topbar .spacer { flex: 1 1 auto; min-width: 0; }

  /* The onboarding owl (#iconic-fab) lives in the top row — shrink it so it and
     the profile both fit; it is a nicety, the profile must never lose to it. */
  /* 30x30 and 32x32 were a third under a fingertip. The row has 23px of slack at 375, so both go
     to 38 (+14 of it) and 9px is left over — measured, not guessed, because this row losing a
     control to overflow is a far worse failure than a small owl. Narrow phones keep the old sizes
     in the <=360 block below, where that slack does not exist. */
  #iconic-fab { width: 38px !important; height: 38px !important; flex-shrink: 0; }
  #iconic-fab svg, #iconic-fab img { width: 22px; height: 22px; }
  #account #gb-chip, #account .gb-chip { width: 38px !important; height: 38px !important; }

  /* Account → compact avatar chip, always on-screen and tappable (the profile). */
  #account { flex-shrink: 0 !important; margin: 0 2px 0 0 !important; }
  #account .gb-nm, #account .gb-plan, #account .gb-cvt { display: none !important; }
  #account .gb-chip { gap: 0 !important; padding: 2px !important; border-radius: 999px; max-width: 40px; overflow: hidden; }
  #account .gb-av { width: 26px !important; height: 26px !important; }

  /* Account / credits dropdown — pin ONLY THE OUTER popup (#gb-pop) fixed, inside the right edge, so
     it can never open off-screen. CRITICAL: do NOT touch its children (.gb-menu etc.) — positioning
     them too pulled the inner section out on top of the header and jumbled the whole menu into an
     unreadable overlap (that was the "profile is broken" the real phone showed). The children keep
     their normal in-flow layout inside this box. */
  #gb-pop, .gb-pop {
    position: fixed !important;
    top: calc(54px + env(safe-area-inset-top, 0px)) !important;
    right: 6px !important; left: auto !important; bottom: auto !important;
    width: min(300px, calc(100vw - 12px)) !important;
    max-width: calc(100vw - 12px) !important;
    max-height: calc(100dvh - 120px) !important; overflow-y: auto !important;
    box-sizing: border-box;
  }

  /* ── Floating stage chrome: keep it inside the frame ─────────────────────
     The DIRECTOR dock and the camera PIP both overran the right edge (Plan button
     clipped, minimap half off-screen). Pin them inside with a margin, and shrink
     the PIP to a tidy corner tile that never covers the subject. */
  /* THE TOP OF THE STAGE IS THREE ROWS, NOT ONE PILE.
   *
   * On a phone the Director bar is full width, and the tool strip, the View options button and
   * the camera inset all began at the same height underneath it. Measured at 375 x 812: the
   * Director bar occupied y 59-91 across the whole screen, and the first tool button (y 60-98)
   * and the View options button (y 60-98) sat directly beneath it. The browser was asked who
   * receives a tap at each control's centre and answered plainly: `Select` was reached by the
   * Director row, `View options` by the Director's own Plan button. The controls were not
   * broken. They were buried, and a buried control is indistinguishable from a dead one — which
   * is exactly what "none of the buttons work" looks like from the chair.
   *
   * So the band is divided. Row one is the Director bar alone, because it is the one thing that
   * needs the full width. Row two is the tools on the left and View options on the right. Row
   * three is the camera inset, under the button rather than behind it. Nothing shares a row with
   * anything it could cover, and the z-order below makes the stacking deliberate instead of
   * whatever the source order happened to produce. */
  #director-dock {
    top: 6px !important;
    left: 8px !important; right: 8px !important; width: auto !important;
    max-width: calc(100vw - 16px) !important; box-sizing: border-box;
    z-index: 66 !important;   /* above the world veil (60) — see app.css */
  }
  #director-dock .director-hint, #director-dock .dd-hint { min-width: 0; overflow: hidden; text-overflow: ellipsis; }

  /* Plan is 24px tall, which is the smallest real control left on the phone — a third of a
     fingertip, on the button that starts the Director. The bar is 32px, so it can simply fill it. */
  #director-dock .dh-go, #director-dock .dh-stop { height: 30px; padding: 0 14px; }

  /* The height rail's "lifted" caption ran its last 5px under the movement pad's left column — the
     rail sits at the far-left corner and the pad starts at 62px. Pulled flush to the rail, where it
     fits with room to spare. */
  .vh-rest { left: 0; }

  /* THE STRIP HAS TO FIT THE STAGE IT SITS ON.
   * At 375 x 812 it ran from y 98 to y 364 while a bottom sheet opens at y 332 — so the last two
   * tools were underneath the sheet's own grab strip and could not be tapped. A control that
   * exists below the fold of its own container is not a control. Sized so the whole strip clears
   * the tallest sheet, and capped so it can never grow back past it. */
  /* THE STRIP STAYS UPRIGHT, ON THE LEFT, AND IS SIZED SO IT CANNOT COLLIDE.
   *
   * It was laid on its side to stop the bottom two tools ending up underneath the sheet that
   * rises from the bottom. That worked and Ori does not want it: a tool strip belongs down the
   * left edge. So it goes back upright and the collision is solved the other way — by sizing it
   * to the space that actually exists above the sheet, instead of letting it run past it.
   *
   * The height is DERIVED rather than guessed, because the sheet is draggable: --m-sheet-h is
   * whatever the creator has pulled it to (52dvh by default), so the stage above it is the screen
   * less that sheet, less the top bar, the nav and the strip's own offset. Drag the sheet taller
   * and the strip shortens to match; it can never reach it. The overflow is the safety valve for
   * the extreme case, so even then every tool stays reachable rather than being cut off. */
  #vp-tools {
    top: 46px !important; left: 8px !important; right: auto !important;
    flex-direction: column !important;
    gap: 2px !important; padding: 4px !important;
    z-index: 70 !important;   /* above the world veil (60) — see app.css */
  }
  /* The shortening applies ONLY while a sheet is up, because that is the only time the stage is
   * short. With nothing open the strip has the whole left edge and every tool is simply there —
   * which matters on a 568-point phone, where the space above an open sheet cannot hold seven of
   * anything and capping it always would have hidden two tools permanently for no reason. */
  body.m-sheet-open #vp-tools {
    max-height: calc(100dvh - var(--m-sheet-h, 72dvh) - 150px
      - env(safe-area-inset-top, 0px) - env(safe-area-inset-bottom, 0px));
    overflow-y: auto; overscroll-behavior: contain;
    scrollbar-width: none;
  }
  #vp-tools::-webkit-scrollbar { display: none; }
  /* min-height beats height, and the base rule sets one — so it has to be cleared here or the
   * strip silently stays too tall and quietly starts scrolling instead of fitting. */
  /* THE RAIL WAS FITTING BY BEING SHRUNK, AND A SCALE TAKES THE TAP TARGET WITH IT.
   * .vp-toolbar carries transform: scale(.8) for the desktop look. Laid out the buttons are 32x28;
   * rendered through that scale they are 25.6 x 22.4 — half a fingertip, on the seven controls you
   * touch most while building. On a phone the rail is SIZED instead of scaled: same seven tools,
   * same pinned corner, in a box a finger can actually hit. 44px wide is the dimension you aim
   * across coming from the stage; 30 tall keeps all seven on screen with a sheet open. */
  /* THE WELCOME CARD MUST NOT REACH THE TOOL RAIL.
     It is 320px wide pinned to the right. On a 393pt phone that puts its left edge at 57 — right on
     top of the rail, which ends at 62. The page's own audit caught it in the wild and named it:
     "vp-tools <- .iw-card". A greeting that makes the tools unpressable is not a greeting. Given a
     left edge clear of the rail and allowed to size itself, it can no longer reach them.
     And dropped below the top band (header 52, Director bar to 96, view-options to 136) so it sits
     over empty stage instead of over the two controls that were left under it. A greeting belongs
     in the space nothing else is using.

     AND THEN IT LANDED ON THE ONE THING IT WAS INTRODUCING. At 150 the card runs from 150 to
     about 430, and on a fresh phone that is exactly where "Build a world from a reference" and
     its three buttons are — the first screen a new user ever sees, greeted by a tour that hides
     the thing it is greeting them about. Measured on an iPhone 13, first open, no project.
     So it goes to the BOTTOM band instead: under the new-world card, above the tab bar, over the
     move pad — which is the one area on this screen that nothing is asking you to read. */
  #iconic-welcome {
    left: 70px !important; right: 8px !important;
    top: auto !important;
    bottom: calc(58px + env(safe-area-inset-bottom, 0px) + 12px) !important;
  }
  #iconic-welcome .iw-card { width: auto !important; padding: 10px 12px !important; gap: 10px !important; }
  /* The tail pointed up at a button that is no longer above it. */
  #iconic-welcome .iw-card::before { display: none !important; }
  /* AND IT HAS TO FIT THE BAND IT WAS MOVED INTO. Measured on an iPhone 13: the new-world card
     ends at 479 and the tab bar starts at 606, so there are 127 pixels of genuinely free screen
     and the full card is 155 tall — it would still clip "Open the demo apartment". The long
     sentence is what makes it tall, and on a phone it is also the part nobody reads before
     tapping one of the two buttons; the offer and the two answers survive. */
  #iconic-welcome .iw-text { display: none !important; }
  #iconic-welcome .iw-title { font-size: 14px !important; margin-bottom: 7px !important; }
  #iconic-welcome .iw-bot .iconic-bot { width: 38px !important; height: 40px !important; }

  #vp-tools { transform: none !important; }
  /* FOLDING THE RAIL. Folded, every tool hides and the rail is left as the one button that brings
     them back — so the strip it was occupying becomes stage again, and the way back is exactly
     where the rail was. The button keeps a full-width target either way. */
  #vp-fold { display: flex !important; font-size: 17px; line-height: 1; opacity: .75; }
  body.m-rail-folded #vp-tools .vpb:not(#vp-fold) { display: none !important; }
  body.m-rail-folded #vp-tools .vp-sep { display: none !important; }
  body.m-rail-folded #vp-tools { background: rgba(18, 24, 42, 0.55); }
  #vp-tools .vpb {
    width: 44px !important; height: 30px !important;
    min-height: 0 !important; min-width: 0 !important; flex: 0 0 auto;
  }
  #vp-tools .vp-sep { height: 1px !important; margin: 2px 2px !important; background: var(--line-2); flex: 0 0 auto; }

  /* row three: the camera inset, below the row rather than behind it */
  #pip { top: 92px !important; }
  .vp-opt-btn { top: 46px !important; right: 8px !important; z-index: 70 !important; }
  /* NOT full width. Stretched edge to edge it lay straight over the tool strip, and six tools
   * that were perfectly alive reported as unreachable — a popover is allowed to be temporary,
   * not to sit on top of the controls beside it. Right-aligned and narrow enough that the strip
   * on the left stays clear and tappable underneath it. */
  .vp-opt-tray {
    top: 92px !important; right: 8px !important; left: auto !important;
    width: min(258px, calc(100vw - 70px)) !important; z-index: 71 !important;
  }

  /* THE PREVIEW IS ONLY HONEST IF IT IS THE SHOT'S OWN SHAPE.
   *
   * This forced 128x76 — a LANDSCAPE box — on every shot, with !important, so a 9:16 frame
   * previewed inside a wide rectangle. 9:16 is the ratio people shoot in on a phone, which made
   * the one picture in the interface that answers "what will the camera see" the one that could
   * not answer it: reported as the preview not matching what was framed.
   *
   * renderPip() already renders the canvas at the shot's aspect, budgeted by its long edge. The
   * box follows the canvas now instead of overruling it, and the viewport caps keep a tall 9:16
   * preview from running down the screen. */
  #pip {
    top: 92px !important;
    right: 8px !important; left: auto !important;
    width: auto !important; height: auto !important;
    max-width: 42vw !important; max-height: 30vh !important;
    z-index: 67 !important;   /* above the world veil (60) */
  }
  /* The canvas carries the shape; auto on both axes keeps a replaced element's own ratio while
     the caps shrink it, so the preview is never stretched to fit a box it does not fit. */
  #pip canvas {
    display: block;
    width: auto !important; height: auto !important;
    max-width: 42vw !important; max-height: 26vh !important;
  }
  /* The void hatching is drawn on a second canvas ON TOP, so it has to be the same box as the
     first one — it took its width from --pip-w, which is the UNSCALED render width. */
  #pip-void { width: 100% !important; height: 100% !important; inset: 0 !important; }

  /* ── THE NEXT-BEAT CARD SAT UNDER THE TAB BAR ──────────────────────────────
   *
   * The card is pinned to the BOTTOM of the stage (justify-content: flex-end) with 28px of
   * padding, and on a phone the tab bar is drawn over the bottom of that same stage at z-index
   * 550 against the card's 22. Measured at 375x812: the nav starts at y=754, the card ended at
   * 784 and the Generate button's lower 10px were behind it — so the one button the card exists
   * to offer was partly unreachable, on the step where a creator has just locked their shot.
   *
   * It clears the nav instead of fighting it. --m-nav-clear is the same measure the FAB and the
   * zoom cluster already use, so all three stay on one line as the bar changes. */
  #next-beat, .next-beat {
    padding-bottom: calc(var(--m-nav-clear, 66px) + 14px) !important;
    /* A tall card on a short phone should scroll inside the stage rather than run off the top. */
    justify-content: flex-end;
  }
  .nb-card {
    max-height: calc(100% - 8px);
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
  }
  /* Both buttons stay full-size and side by side; wrapping put "Keep editing" on its own line
     and made the card taller, which is the opposite of what a cramped screen needs. */
  .nb-row { flex-wrap: nowrap; }
  .nb-cta, .nb-skip { flex: 1 1 0; text-align: center; white-space: nowrap; }
  /* AND WHILE THE CARD IS UP, THE CONTROLS IT SITS ON STAND DOWN.
   *
   * Lifting the card clear of the tab bar put it straight over two other things: measured at
   * 375x812, the walk pad covered the Generate button the card exists to offer, and the support
   * button overlapped "Keep editing". Nothing fits a 345px card, a 96px five-button pad and a
   * round FAB into the same corner of a 375px screen, so the card wins while it is up — it is a
   * question with both its answers on it, and walking the camera is what "Keep editing" returns
   * you to.
   *
   * Hidden, not dimmed: a half-visible control under a card is still a control people aim at.
   *
   * ASKED OF THE CARD ITSELF, not of a class someone has to remember to clear. A `body.beat-open`
   * would be a second piece of state tracking a first, and every path that dismisses this card —
   * the two buttons, a touch on the stage, a job starting, the camera tool opening — would have to
   * keep it in step forever. `:has()` cannot drift, and silk.css:765 already hides the camera
   * inset behind the world veil exactly this way. */
  body:has(#next-beat:not(.hidden)) #wasd-pad,
  body:has(#next-beat:not(.hidden)) .wasd-pad,
  body:has(#next-beat:not(.hidden)) #assistant-fab,
  body:has(#next-beat:not(.hidden)) .assistant-fab,
  body:has(#next-beat:not(.hidden)) [class*="support-fab"],
  body:has(#next-beat:not(.hidden)) #help-fab,
  /* The rail as a whole: hiding one of its two faces would leave half a control on screen. */
  body:has(#next-beat:not(.hidden)) #fab-rail { display: none !important; }

  /* Support / help FAB — lift it clear of the bottom nav and the sheets.
   *
   * ONLY WHILE IT IS STILL FLOATING FREE, which is what `body >` says. The note below used to
   * read "it floats free in the corner with nothing to overflow", and that stopped being true
   * when the assistant and the handset were collected into #fab-rail — at which point these
   * !important sizes and offsets were overriding the rail's own layout and pulling the button
   * back out of it. The rail positions and sizes what it holds; a button outside it still needs
   * all of this. Expressed as "a child of body" rather than as :not(), because that is the
   * actual condition and it reads as the sentence it means. */
  body > #assistant-fab, body > .assistant-fab,
  body > [class*="support-fab"], body > #help-fab {
    bottom: calc(66px + env(safe-area-inset-bottom, 0px)) !important;
    right: 10px !important; z-index: 545 !important;
    /* Floating free in the corner with nothing to overflow, so it gets a full target. */
    width: 44px !important; height: 44px !important;
  }

  /* The desktop Storyboard dock (bottom strip) is redundant on the phone — the
     Shots tab owns it — and it floated over the stage colliding with the FAB. */
  #storyboard-dock, .storyboard-dock, #btn-dl-storyboard { display: none !important; }

  /* ── Panels → FULL-WIDTH BOTTOM SHEETS ───────────────────────────────────
     Each bottom-tab panel rises from the bottom over the lower ~72% of the
     screen; the stage stays visible above it. Full width, so nothing truncates
     ("Locati…" / "Chara…" are gone). Dismiss via the scrim, the ✕, or the tab. */
  #left, #right, #timeline {
    position: fixed !important;
    left: 0 !important; right: 0 !important; top: auto !important;
    bottom: calc(58px + env(safe-area-inset-bottom, 0px)) !important;
    width: 100vw !important; max-width: 100vw !important;
    /* DRAGGABLE HEIGHT with snap points (peek / half / full) — so you can see the 3D you're building
       AND reach the controls at the same time: drag the grab-handle up for more controls, down to
       reveal more of the stage. --m-sheet-h is set live by the drag (mobile.js) and defaults to HALF,
       so both are visible the moment a panel opens. dvh (not vh) so iOS Safari's address bar can't push
       the lower controls off-screen; capped so it never overlaps the topbar. */
    /* 72dvh, which is what this file's own header always said ("over the lower ~72%") before the
     * value drifted to 52. The difference is not cosmetic: at 52dvh the Build sheet gave the
     * catalogue 266px on a 659px screen — about two rows of a twenty-four folder grid — and
     * everything else sat below the fold at coordinates no finger could reach. Browsing is what
     * this sheet is FOR, and arming an item closes it anyway (scene/placement.js), so the stage
     * is free exactly when it is needed. Still draggable to peek or full. */
    height: var(--m-sheet-h, 72dvh) !important;
    max-height: calc(100dvh - 52px - env(safe-area-inset-top, 0px) - 58px - env(safe-area-inset-bottom, 0px) - var(--m-kb, 0px)) !important;
    min-height: 92px !important;
    margin: 0 !important;
    border: 0 !important; border-top: 1px solid var(--line-2) !important;
    border-radius: 18px 18px 0 0 !important;
    box-shadow: 0 -18px 46px rgba(0,0,0,.55) !important;
    background: var(--panel, #0c1018) !important;
    z-index: 540 !important;
    transform: translateY(110%);
    transition: transform .28s cubic-bezier(.4, 0, .2, 1), height .22s cubic-bezier(.4, 0, .2, 1);
    pointer-events: none;
    display: flex !important; flex-direction: column;
    overflow: hidden;
    padding-top: 26px !important;   /* room for the drag handle strip */
    /* lift above the on-screen keyboard when a field is focused */
    margin-bottom: var(--m-kb, 0px) !important;
  }
  #left.m-open, #right.m-open, #timeline.m-open {
    transform: translateY(0);
    pointer-events: auto;
  }
  /* while dragging the handle, the height must follow the finger instantly (no smoothing) */
  body.m-sheet-dragging #left, body.m-sheet-dragging #right,
  body.m-sheet-dragging #timeline { transition: none !important; }

  /* the draggable grab-handle strip at the top of every sheet (added by mobile.js) */
  /* The grab strip stops short of the ✕. It ran the full width at z-index 11 with the close
   * button at z-index 10 underneath it, so the top two thirds of the ✕ belonged to the strip and
   * a tap on the middle of the button dragged the sheet instead of closing it. Two controls, one
   * place. Now they each have their own. */
  .m-drag {
    position: absolute; top: 0; left: 0; right: 46px; height: 26px; z-index: 11;
    display: grid; place-items: center; cursor: grab; touch-action: none;
    -webkit-tap-highlight-color: transparent;
  }
  .m-drag-pill { width: 42px; height: 5px; border-radius: 999px; background: var(--line-2); opacity: .9; }
  .m-drag:active { cursor: grabbing; }
  .m-drag:active .m-drag-pill { background: var(--accent, #6f78ec); }
  /* the inner body of each panel scrolls; the head stays put */
  #left .panel-head, #right .panel-head, #timeline .tl-head { flex: 0 0 auto; padding-right: 44px; }
  #left > *:not(.panel-head):not(.m-close):not(.m-grip):not(.m-grip-v),
  #right > *:not(.panel-head):not(.m-close):not(.m-grip):not(.m-grip-v) { min-height: 0; }

  /* The side/height drag grips belong to the old side-dock — not a bottom sheet. */
  .m-grip, .m-grip-v { display: none !important; }

  /* The on-screen movement pad drives the 3D stage — while a sheet is open it sat ON TOP of the
     panel's own controls (it covered the Generate button). Hide it whenever a sheet is up. */
  body.m-sheet-open #wasd-pad { display: none !important; }

  /* Build progress / result toasts ("Building your world…", "Built your world") must always sit
     ABOVE the sheet and the nav so the user sees them — never trapped behind a panel. */
  .toast, [class*="toast"], #toasts, #toast-wrap { z-index: 600 !important; }

  /* ✕ close, top-right of the open sheet */
  .m-close {
    position: absolute; top: 8px; right: 8px; z-index: 12; width: 30px; height: 30px; border-radius: 9px;
    display: grid !important; place-items: center; font-size: 15px; color: var(--text-2);
    background: rgba(10, 14, 28, .78); border: 1px solid var(--line);
  }

  /* NO tap-catching scrim. #main establishes a stacking context (position:relative; z-index:1), so a
     scrim appended to <body> (z 535) painted ABOVE the whole sheet even though the sheet's own z-index
     is 540 — the sheet's z is only relative to #main's context (effective z 1), which loses to a
     body-level 535. The transparent scrim then swallowed every tap and closed the panel, so a control
     tap "returned to the view without doing anything". Removing it: taps reach the controls; the sheet
     is dismissed by its ✕ or by re-tapping its tab, and tapping the visible stage above just drives the
     3D as it should. */
  #m-scrim { display: none !important; }

  /* Every sheet's inner body scrolls by touch. The Build rail (#rail) was overflow:hidden — its
     expanded steps and the Generate button below the fold were unreachable, which read as "the
     controls show but don't work". The other panels already scroll; make touch-scrolling explicit. */
  #right #rail, #left #layer-tree, #timeline #shot-strip {
    overflow-y: auto !important;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    touch-action: pan-y;
    min-height: 0;
  }

  /* ONE SCROLLER, NOT TWO — and this is why the phone catalogue was unusable.
   *
   * On desktop the rail holds an inner, absolutely-positioned scroll area whose height comes
   * from `flex: 1` against the rail's own fixed height. On the phone #rail is ITSELF the
   * scroller, so there is no bounded height to divide: the inner area collapsed, and the
   * measurement was brutal — a 131px window holding 4139px of catalogue. Six tiles were
   * reachable out of fifty-four; everything else sat at a y-coordinate past the bottom of the
   * screen. That is the whole of "the objects I summon are not the ones I tapped" and most of
   * "the interface doesn't really work on phones": people were tapping at a grid they could not
   * see, inside a slot two rows tall.
   *
   * Flattened here: the catalogue lays out at its natural height and the sheet scrolls it. */
  #right .rail-scroll-wrap {
    position: static !important;
    flex: none !important;
    height: auto !important;
    min-height: 0;
  }
  #right .rail-scroll {
    position: static !important;
    inset: auto !important;
    height: auto !important;
    max-height: none !important;
    overflow: visible !important;
  }
  /* The desktop scrollbar overlay belongs to the inner scroller that no longer exists. */
  #right .rail-sb { display: none !important; }

  /* ── Bottom tab bar ──────────────────────────────────────────────────────
     Owns the very bottom; the sheets sit on top of it. */
  #m-nav {
    position: fixed; left: 0; right: 0; bottom: 0; z-index: 550;
    height: calc(58px + env(safe-area-inset-bottom, 0px));
    padding-bottom: env(safe-area-inset-bottom, 0px);
    display: flex; align-items: stretch;
    background: rgba(8, 11, 18, .96);
    border-top: 1px solid var(--line-2);
    backdrop-filter: blur(12px);
  }
  /* The switch belongs in the bottom nav on a phone — the top bar has no room and losing the
     account chip to make some is not a trade worth making. */
  #topbar .surface-switch { display: none !important; }
  #m-nav .m-chat {
    flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
    color: var(--dim); text-decoration: none; font-size: 10px;
  }
  #m-nav .m-chat .m-ico svg { width: 21px; height: 21px; }
  #m-nav .m-chat:active { color: var(--text); }

  #m-nav button {
    flex: 1; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
    background: none; border: 0; color: var(--text-2); font-size: 10px; letter-spacing: .2px;
    -webkit-tap-highlight-color: transparent;
  }
  #m-nav button .m-ico svg { width: 22px; height: 22px; display: block; }
  #m-nav button.on { color: var(--accent, #8ea2ff); }
  #m-nav button.on .m-ico svg { stroke: var(--accent, #8ea2ff); }
  /* ── GENERATE, ON THE PHONE: LOADED, THEN WORKING ──────────────────────────────────────
   *
   * THE GOLD WAS A RING, NOT A BUTTON. app.css gives `.ready-gold` a gold background and black
   * ink, and on the phone only half of it arrived: `#m-nav .m-gen { color: #fff }` carries an ID
   * and beats the `.m-gen.ready-gold` colour, so the word stayed white, and
   * `#m-nav .m-gen .m-ico` kept painting the icon chip indigo on top of the gold. What was left
   * read as an outline around a cell rather than a button waiting to be pressed. Reported
   * exactly that way: "it goes gold, but it doesn't turn the whole button gold with black text".
   *
   * So the phone states it at its own specificity: the whole cell, the icon and the word, all
   * gold with black ink, inset from the bar's edges and rounded so it reads as one object. */
  #m-nav .m-gen.ready-gold {
    background: linear-gradient(180deg, #f6d27e 0%, #d9a43c 100%);
    color: #231806;
    border-radius: 12px;
    margin: 4px 6px;
    box-shadow: 0 2px 14px rgba(217, 164, 60, .38), inset 0 1px 0 rgba(255,255,255,.45);
  }
  #m-nav .m-gen.ready-gold .m-ico {
    background: rgba(35, 24, 6, .14);
    box-shadow: none;
  }
  #m-nav .m-gen.ready-gold svg,
  #m-nav .m-gen.ready-gold .m-ico svg { color: #231806; }

  /* AND IT BREATHES, BECAUSE A STILL BUTTON IS NOT AN INVITATION.
   *
   * app.css plays its pulse ONCE on arrival, deliberately: on a desktop this button sits beside
   * the thing you are composing, and something that never stops moving there is a distraction
   * you cannot switch off. The phone is not that. The bar is pinned to the bottom edge, below
   * the work rather than beside it, and the report is that people do not realise a press is
   * being waited for. So here it keeps breathing — slowly, 2.6s, and small enough (1.5%) to be
   * noticed without being nagged at. It stops the instant the render starts, because then the
   * thing to do is wait and the bar below says so. */
  @keyframes m-gen-breathe {
    0%, 100% { transform: scale(1);     box-shadow: 0 2px 14px rgba(217,164,60,.38), inset 0 1px 0 rgba(255,255,255,.45); }
    50%      { transform: scale(1.015); box-shadow: 0 4px 22px rgba(243,201,105,.62), inset 0 1px 0 rgba(255,255,255,.55); }
  }
  #m-nav .m-gen.ready-gold:not(.busy) {
    animation: m-gen-breathe 2.6s ease-in-out infinite;
  }

  /* ── AND THEN IT IS WORKING ────────────────────────────────────────────────────────────
   *
   * Pressing it used to change nothing at all on a phone: no spinner, no progress, no
   * acknowledgement, for the thirty to ninety seconds a frame takes. readygold.js now puts
   * `.busy` and the real `--gen-p` (0..1, from the render's own progress stream) on this button.
   *
   * TWO LAYERS, BECAUSE ONE OF THEM LIES. The fill is the true number and it is what tells you
   * how far along you are — but it holds still for several seconds between ticks, and a bar that
   * has stopped is indistinguishable from a studio that has died. The shimmer over it never
   * stops, so the button is always moving while the picture is being made. The word stays put;
   * people look for "Generate" and moving the label would make the bar jump. */
  #m-nav .m-gen.busy {
    position: relative;
    overflow: hidden;
    border-radius: 12px;
    margin: 4px 6px;
    /* The unfilled part of the track. Dark, so the label reads white over both halves and the
       boundary between done and not-done is the only thing the eye has to find. */
    background: rgba(255, 255, 255, .07);
    color: #fff;
    cursor: default;
  }
  #m-nav .m-gen.busy .m-ico { background: transparent; box-shadow: none; }
  #m-nav .m-gen.busy svg { color: #fff; }
  /* THE TRUE NUMBER, filling from the left. */
  #m-nav .m-gen.busy::before {
    content: ""; position: absolute; inset: 0; z-index: 0;
    transform-origin: left center;
    transform: scaleX(var(--gen-p, 0));
    background: var(--grad);
    transition: transform .45s linear;
  }
  /* AND THE PART THAT NEVER STOPS. The fill holds still for several seconds between ticks of the
     progress stream, and a bar that has stopped is indistinguishable from a studio that has
     died — so something is always moving while the picture is being made. Kept faint: it is a
     sign of life, not a second progress bar competing with the real one. */
  @keyframes m-gen-sheen { from { transform: translateX(-120%); } to { transform: translateX(420%); } }
  #m-nav .m-gen.busy::after {
    content: ""; position: absolute; top: 0; bottom: 0; left: 0; width: 30%; z-index: 1;
    background: linear-gradient(90deg, transparent, rgba(255,255,255,.16), transparent);
    animation: m-gen-sheen 1.6s ease-in-out infinite;
  }
  /* The icon and the word ride above both layers, so they stay legible at every percentage.
     Both are real elements for exactly this reason — see mobile.js, where the label was a bare
     text node that the fill painted straight over. */
  #m-nav .m-gen.busy .m-ico,
  #m-nav .m-gen.busy .m-lab { position: relative; z-index: 2; }

  /* Movement is guidance for most people and a problem for some; the state still reads without
     it, because the colour and the progress fill carry it on their own. */
  @media (prefers-reduced-motion: reduce) {
    #m-nav .m-gen.ready-gold:not(.busy) { animation: none; }
    #m-nav .m-gen.busy::after { animation: none; display: none; }
    #m-nav .m-gen.busy::before { transition: none; }
  }

  /* .m-gen's look already lives in app.css (white on the gradient pill) — all that is missing
     here is the press feedback every other item in this bar has. */
  #m-nav button.m-gen { font-weight: 600; }
  #m-nav button.m-gen:active { opacity: .6; }
  #m-nav button.m-gen[disabled] { opacity: .4; }
}

/* ── THE NARROWEST PHONES STILL IN USE (<= 360px) ───────────────────────────
 * Measured at 320 x 568: the top bar wanted 342 pixels of a 320-pixel screen and pushed the
 * account avatar 22 pixels off the right edge — the one control that holds sign-out, credits and
 * billing, gone, with nothing on screen to say it was ever there.
 *
 * Nothing is removed to make room. The History button here opens the list of every set the
 * creator has made, which is NOT the render History in the bottom bar, so hiding it would cost
 * them their own work. The bar is simply tightened: a few pixels off each button's padding and
 * the gaps, which is all it was ever short of. */
@media (max-width: 360px) {
  #topbar { gap: 2px !important; padding: 0 6px !important; }
  #topbar .tb { padding-left: 4px !important; padding-right: 4px !important; min-width: 34px !important; }
  #topbar .tb-group { gap: 2px !important; }
  /* the strip gets the same treatment so it stays short enough for a 568-point screen */
  #vp-tools { gap: 1px !important; padding: 4px !important; }
  #vp-tools .vpb { width: 40px !important; height: 28px !important; min-height: 0 !important; }
  /* No slack in the top row at this width — the profile must never lose to the owl. */
  #iconic-fab { width: 30px !important; height: 30px !important; }
  #iconic-fab svg, #iconic-fab img { width: 20px; height: 20px; }
  #account #gb-chip, #account .gb-chip { width: 32px !important; height: 32px !important; }
}
