/* ──────────────────────────────────────────────────────────────────────────
   Studio flow — one question per screen.

   Replaces the workbench (stage.css) as what a project opens on, and replaces
   the moodboard idea with it: there is no wall of thumbnails to scan here. One
   candidate fills the stage, the others are a strip of markers under it, and
   the click that takes it is the click that moves the project on.

   Everything is drawn with the site's own tokens (lowpoly/base.css): the warm
   ground, the burnt-orange accent, Sora for headings, JetBrains Mono for
   anything counted. Buttons reuse `.btn` / `.btn-primary` rather than growing
   a second button system inside the studio.
   ────────────────────────────────────────────────────────────────────────── */

/* TRANSPARENT, like every full-height studio surface. The page's background is
   the body's --bg plus the single `.grid-overlay` element (lowpoly/base.css);
   an opaque box the size of the frame would simply hide it. Only bars, cards
   and thumbnails paint a ground of their own from here down. */
.lp-flow {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr);
    height: 100%;
    min-height: 0;
    background: transparent;
}

/* ── The trail ─────────────────────────────────────────────────────────────
   Where you are, and what is behind you. Nothing in it is clickable: the flow
   runs forwards only (see `useAssetFlow`), so a step already passed is a
   read-out, not a destination. It has to read as COMPLETE rather than as
   disabled — a greyed-out control says "not yet", which is the opposite of
   what happened here. */

.lp-flow-trail {
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 10px 18px;
    border-bottom: 1px solid var(--line);
    background: var(--bg-elev);
    min-height: 0;
}

.lp-flow-trail-step {
    display: inline-flex;
    align-items: center;
    gap: 8px;
    font-family: var(--font-brand);
    font-size: 0.8125rem;
    font-weight: 500;
    letter-spacing: 0.01em;
    color: var(--fg-faint);
    background: none;
    border: 0;
    padding: 4px 8px;
    border-radius: var(--radius-sm);
    transition: color var(--transition), background var(--transition);
}

/* The trail used to grow buttons for the steps behind the user, with a hover
   skin to match. Neither is rendered any more — the rules are removed rather
   than left dormant, so nothing can look clickable by accident. */

.lp-flow-trail-step i {
    width: 6px;
    height: 6px;
    border-radius: 50%;
    background: var(--line-strong);
    display: block;
    transition: background var(--transition), box-shadow var(--transition);
}

.lp-flow-trail-step.is-done { color: var(--fg-muted); }
.lp-flow-trail-step.is-done i { background: var(--accent); opacity: .55; }
.lp-flow-trail-step.is-now { color: var(--fg); }
.lp-flow-trail-step.is-now i {
    background: var(--accent);
    box-shadow: 0 0 0 4px var(--accent-soft);
}

/* `.lp-flow-back` — the trail's Back button — was styled here. The control is
   gone with the gesture; see the header of this section. */

/* Screen-reader text for the trail. `.sr-only` lives in onboarding.css, which
   this bundle does not guarantee — and the workbench's own `.lp-wb-sr` went
   with stage.css. Declared here so the trail never depends on either. */
.lp-flow-sr {
    position: absolute;
    width: 1px; height: 1px;
    padding: 0; margin: -1px;
    overflow: hidden;
    clip: rect(0 0 0 0);
    clip-path: inset(50%);
    white-space: nowrap;
    border: 0;
}

.lp-flow-trail-sep { color: var(--line-strong); font-size: 0.75rem; padding: 0 2px; }
.lp-flow-trail-spacer { flex: 1; }

.lp-flow-trail-cost {
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    letter-spacing: 0.04em;
    color: var(--fg-faint);
    font-variant-numeric: tabular-nums;
}

/* ── The ask ──────────────────────────────────────────────────────────────
   The one text field in the whole studio, on the one screen that has nothing
   to show yet. */

.lp-flow-ask {
    display: grid;
    place-items: center;
    padding: 32px 20px;
    min-height: 0;
    overflow-y: auto;
    /* NO background. This block used to redraw the site grid at 48px, at full
       opacity, without the top/bottom fade, and scrolling with its own content
       — plus an orange radial glow the landing page does not have. The grid is
       `.grid-overlay` now and nothing else. */
    background: transparent;
}

.lp-flow-ask-inner {
    width: min(640px, 100%);
    display: flex;
    flex-direction: column;
    gap: 18px;
    text-align: center;
}

.lp-flow-ask h2 {
    margin: 0;
    font-family: var(--font-brand);
    font-weight: 700;
    font-size: clamp(1.8rem, 4vw, 2.6rem);
    line-height: 1.05;
    letter-spacing: -0.03em;
    color: var(--fg);
}

.lp-flow-ask p { margin: 6px 0 0; color: var(--fg-muted); font-size: 1rem; }

/* ── THE COMPOSER ITSELF LIVES IN lowpoly/composer.css ────────────────────
   `.lp-flow-composer`, `.lp-flow-composer-tools`, `.lp-flow-tool*`,
   `.lp-flow-actionbar*`, `.lp-flow-cost-chip`, `.lp-genbar-count/-n/-go` and
   `.lp-kind*` were all here and moved out whole, in this order, when the
   homepage hero started mounting the same panel: a marketing page cannot load
   85 kB of candidate grids to get one prompt box.

   `layout_studio.php` loads that sheet immediately BEFORE this one, so the
   cascade is unchanged — every rule still standing here that touches those
   classes is either more specific (`.lp-flow-actionbar` overrides on the count,
   `.lp-gen-actions .lp-flow-cost-chip`, `.lp-choice-again .lp-genbar-n`,
   `.lp-home-ask .lp-flow-composer`) or names an element that sheet does not
   (`.lp-flow-embellish`, `.lp-genbar`). Do not reorder the two.

   What stays here is what belongs to the FLOW and not to the panel: the pill
   the step composer pins over its field, the footer row the count sits in on a
   step, and the ask screen's own field wrapper. */

/* ── A dropped reference image, and the verdict on it ─────────────────────
   The verdict is the point, not the thumbnail: most of what people drop is a
   game screenshot or a cluttered photo, and saying so BEFORE a GPU is booked
   is the whole reason this exists. */
.lp-drop {
    display: flex;
    align-items: center;
    gap: 12px;
    width: min(640px, 100%);
    margin: 0 auto;
    padding: 10px 12px;
    border-radius: var(--radius);
    border: 1px solid var(--line-strong);
    background: var(--bg-elev);
    text-align: left;
}

.lp-drop-thumb {
    width: 56px;
    height: 44px;
    object-fit: cover;
    border-radius: var(--radius-sm);
    border: 1px solid var(--line);
    flex: none;
    display: block;
}

.lp-drop-text { display: flex; flex-direction: column; gap: 2px; min-width: 0; flex: 1; }
.lp-drop-verdict { font-size: 0.875rem; color: var(--fg); }
.lp-drop-reason { font-size: 0.75rem; color: var(--fg-faint); }

.lp-drop.is-usable   { border-color: color-mix(in srgb, var(--success) 45%, transparent); }
.lp-drop.is-usable   .lp-drop-verdict { color: var(--success); }
.lp-drop.is-busy     { border-color: var(--accent-line); }
.lp-drop.is-busy     .lp-drop-verdict { color: var(--accent-text); }
.lp-drop.is-unusable { border-color: color-mix(in srgb, var(--danger) 45%, transparent); }
.lp-drop.is-unusable .lp-drop-verdict { color: var(--danger); }

.lp-drop-clear {
    flex: none;
    width: 26px;
    height: 26px;
    border-radius: 50%;
    border: 1px solid var(--line);
    background: transparent;
    color: var(--fg-muted);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
}

.lp-drop-clear:hover { color: var(--fg); border-color: var(--line-strong); }

/* ── EMBELLISH, IN THE CORNER OF THE FIELD IT REWRITES ────────────────────
   THE STEP COMPOSER ONLY (ChoiceScreen). The landing composer had this too and
   gave it up: with the run controls moved onto the panel's own bar, a pill
   floating over the writing surface was the last thing left outside the one
   place this screen's actions live. See `.lp-flow-actionbar`.

   It used to stand next to "Surprise me" in the strip under the box, which
   read as two interchangeable buttons. They are not: the die WRITES a prompt
   into an empty box, Embellish REWRITES the one already there and is disabled
   until there is one. Pinning it to the field's top-right says that without a
   word of copy — it is attached to the text, not to the row of helpers.
   The field's `padding-top` (below) is what keeps the first line of the prompt
   from running under it. Same arrangement as the workbench panel's own
   Embellish pill (`.lp-wb-genpanel-embellish`), which always sat this way and
   which went with stage.css when the workbench was removed.

   Sized down from a full `.lp-flow-tool`: it overlaps the writing surface, so
   it has to read as an affordance on the field rather than as a control that
   was dropped on top of it. */
.lp-flow-embellish {
    position: absolute;
    top: 8px;
    right: 8px;
    z-index: 2;
    padding: 4px 10px;
    font-size: 0.75rem;
}

/* (`.lp-flow-composer textarea, .lp-flow-ask-field textarea` — the field's own
   type, padding and minimum height — and the composer-only `padding-top: 40px`
   band that keeps the pill above off the first line of prompt both moved to
   lowpoly/composer.css with the rest of the panel. The grouped selector went
   whole rather than being split: the two boxes have always been sized from one
   declaration and one owner is the point.) */

.lp-flow-ask-field textarea:focus { outline: none; }
.lp-flow-ask-field textarea::placeholder { color: var(--fg-faint); }

.lp-flow-ask-tries {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 8px;
    flex-wrap: wrap;
    font-size: 0.8125rem;
    color: var(--fg-faint);
}

.lp-flow-try {
    font-family: var(--font-display);
    font-size: 0.8125rem;
    padding: 5px 12px;
    border-radius: 999px;
    border: 1px solid var(--line-strong);
    background: var(--bg-elev);
    color: var(--fg-muted);
    cursor: pointer;
    transition: color var(--transition), border-color var(--transition);
}

.lp-flow-try:hover { color: var(--fg); border-color: var(--accent); }

.lp-flow-ask-actions { display: flex; align-items: center; justify-content: center; gap: 12px; flex-wrap: wrap; }

/* ── The choice screen ─────────────────────────────────────────────────────

   Four generations, filling the window, and nothing else. No side panel, no
   tool rail, no run bar, no 3D viewport to learn — the user sees what was just
   made and clicks one. The only other thing they can do is ask for four more.

   Deliberately NOT a thumbnail grid: each tile is ~45% of the window, which is
   the difference between "a moodboard I have to scan" and "the four results,
   here". Everything below serves that one distinction. */

.lp-choice {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
    min-height: 0;
    height: 100%;
    /* The site's own backdrop, and it is not drawn here: this used to be a
       third copy of the landing grid (48px, opaque, plus an orange glow), so
       the same screen showed a different trame depending on the step. The one
       `.grid-overlay` behind the whole studio carries it now. */
    background: transparent;
}

.lp-choice-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 22px 24px 16px;
    text-align: center;
}

.lp-choice-head h2 {
    margin: 0;
    font-family: var(--font-brand);
    font-weight: 700;
    font-size: clamp(1.4rem, 2.8vw, 1.9rem);
    line-height: 1.1;
    letter-spacing: -0.025em;
    color: var(--fg);
}

/* TWO LINES OF ROOM, WHATEVER IT SAYS. This line is swapped for "Still
   working — they arrive as they are ready." while a run is in flight, and the
   two do not wrap alike: the longest subtitle takes two lines, the busy line
   takes one. Every pixel this paragraph gives up is a pixel the grid below
   gains, and that grid sizes its cells off `100cqh` (see `--lp-cell`) — so a
   subtitle losing a line resizes every tile on screen. Reserving the taller of
   the two states here means the swap costs nothing. */
.lp-choice-head p {
    margin: 0;
    font-size: 0.9375rem;
    line-height: 1.45;
    min-height: 2.9em;
    color: var(--fg-muted);
    max-width: 62ch;
}

/* ── Four proposals ───────────────────────────────────────────────────────
   Not a wall of thumbnails: four cards with room around them, centred, each
   one big enough to judge on sight. */

/* SQUARE CELLS, and that is the whole shape of this box.
   The pipeline hands this screen square pictures (the image worker returns a
   square batch) and square mesh renders (`MeshThumb` rasterises at
   PREVIEW_SIZE x PREVIEW_SIZE). The grid used to take the full 1180px and
   stretch its rows over whatever height the stage had left, which made every
   cell a wide landscape rectangle — a square subject in it was cropped top and
   bottom by `object-fit: cover`, so a vertical object lost its head and its
   base and read as "stretched".

   The cell is a SQUARE THAT FITS IN THE STAGE, which means one number:
   min(width the stage grants, height the stage grants), split over the tracks.
   `container-type: size` is what makes that number expressible — it turns this
   box into a query container, so `100cqw` / `100cqh` in `--lp-cell` ARE the
   stage this grid was given, and `min()` can compare them. The property is
   used on the TILES, never here: an element is never its own query container,
   and cq units in this rule would silently fall back to the viewport (measured:
   they did, and the tiles came out 530px tall in a 372px-tall stage).

   That comparison cannot be done with `aspect-ratio` + `max-width` /
   `max-height`, which is what stood here and what this replaces: a ratio only
   survives while ONE axis is automatic, and both a max- constraint and an
   explicit size make it definite. Measured in Chromium on the real sheets: at
   1280x720 with two candidates, `width: 1180px; aspect-ratio: 2;
   max-height: 100%` gave 580x364 cells — a 1.59 landscape box with the picture
   letterboxed inside it, the exact shape this block exists to avoid. The
   `min()` form gives 350x350 there.

   The tracks are `auto` and the content is centred, so the block sits in the
   middle of the stage instead of being spread across it: `1fr` tracks would
   hold the square tiles 200px apart on a wide window.

   ── AND THE TRACKS COME FROM THE RUN, NOT FROM THIS SHEET ──────────────────
   `--lp-cols` / `--lp-rows` are written INLINE by ChoiceScreen and
   TopologyScreen, from `gridShape()`. The two below are the pre-hydration
   fallback and nothing else.

   THE BUG THAT MOVED THEM OUT OF HERE: the shapes used to be enumerated as
   `[data-count="1|2|3"]` rules with a 2x2 default, and the number fed to
   `data-count` was clamped to four while the TILES were not. A run whose row
   registered more outputs than the picker offers — or an upload pile, which
   has no size at all — therefore laid eight tiles into a box declared as two
   columns by two rows. The extra ones fell into implicit rows the `--lp-cell`
   arithmetic below knew nothing about, so the grid overflowed the stage; the
   `align-content: center` on it then split that overflow between the two ends,
   putting the first row ABOVE the top of the box where no scroll could reach
   it; and with nothing clipping or scrolling here, the tiles painted over the
   heading, which is later in the sheet's ground but earlier in the DOM.
   Three things answer that, and all three are needed:
     · the tracks are the real ones, so there are no implicit rows;
     · the cell has a floor, so many candidates stay judgeable rather than
       shrinking to stamps;
     · this box scrolls, and centres SAFELY — `safe center` falls back to
       `start` the moment the content is taller than the box, which is the one
       alignment that cannot hide a row off the top. */
.lp-choice-grid {
    --lp-gap: 20px;
    --lp-cols: 2;
    --lp-rows: 2;
    /* THE TRACKS ARE READ THROUGH THESE TWO, never straight off `--lp-cols` /
       `--lp-rows`. Those arrive as an inline style, which no rule in this sheet
       can outrank — so the phone variant at the bottom of this block needs a
       name of its own to write into. */
    --lp-c: var(--lp-cols);
    --lp-r: var(--lp-rows);
    /* Below this a candidate is a thumbnail, and this screen exists so that
       nobody has to judge a thumbnail. Only the HEIGHT may break the floor:
       applying it to the width would mean a sideways scrollbar, and nothing
       here is worth scrolling sideways for. */
    --lp-cell-min: 150px;
    /* The `- var(--lp-r) * 2px` is not slop, it is the rounding: `100cqh`
       and the cell it divides into are both fractional, and each row is rounded
       UP when the box measures its scrollable overflow. Measured in Chromium, a
       grid that fits its stage exactly still reported a couple of pixels too
       many — enough to make `safe center` fall back to `start` and to grow a
       scrollbar for content that is entirely visible. Two pixels a row buys
       that back, and costs a tile nothing anyone can see. */
    --lp-cell: min(
        (100cqw - (var(--lp-c) - 1) * var(--lp-gap)) / var(--lp-c),
        max(
            var(--lp-cell-min),
            (100cqh - (var(--lp-r) - 1) * var(--lp-gap) - var(--lp-r) * 2px) / var(--lp-r)
        )
    );
    container-type: size;
    display: grid;
    grid-template-columns: repeat(var(--lp-c), auto);
    grid-template-rows: repeat(var(--lp-r), auto);
    justify-content: center;
    /* Two declarations, on purpose: an engine that does not know the `safe`
       keyword drops the second and keeps the behaviour it has always had. */
    align-content: center;
    align-content: safe center;
    gap: var(--lp-gap);
    /* Room for the tiles' drop shadow and their 4px hover lift, which a scroll
       container would otherwise shave off. `cq` units resolve against the
       CONTENT box, so this padding is already outside the cell arithmetic. */
    padding: 6px 8px 10px;
    box-sizing: border-box;
    width: 100%;
    height: 100%;
    max-width: min(1180px, calc(100% - 56px));
    justify-self: center;
    margin: 0 auto;
    min-height: 0;
    overflow-y: auto;
    overflow-x: hidden;
    scrollbar-width: thin;
    scrollbar-color: var(--line-strong) transparent;
}

.lp-choice-grid::-webkit-scrollbar { width: 8px; }
.lp-choice-grid::-webkit-scrollbar-track { background: transparent; }
.lp-choice-grid::-webkit-scrollbar-thumb { background: var(--line-strong); border-radius: 999px; }

/* On a phone the wide shape is unusable: a run of eight at four columns is an
   89px cell, measured at 480px wide. Two columns and a scroll instead — the
   pair the screens ship alongside the wide one (`gridVars` in model.ts),
   because `rows = ceil(tiles / cols)` is not something this sheet can work out
   for itself. The fallbacks keep the pre-hydration render honest. */
@media (max-width: 620px) {
    .lp-choice-grid {
        --lp-c: var(--lp-cols-narrow, var(--lp-cols));
        --lp-r: var(--lp-rows-narrow, var(--lp-rows));
        --lp-gap: 12px;
    }
}

/* One candidate gets the whole stage — the only shape that still needs a rule
   of its own, because it is the only one that changes the grid's WIDTH rather
   than its tracks. */
.lp-choice-grid[data-count="1"] {
    max-width: min(820px, calc(100% - 56px));
}

.lp-choice-tile {
    /* The square, and the only place it is stated. `--lp-cell` is declared on
       the grid but SUBSTITUTED HERE, which is what lets its `cq` units resolve
       against the grid's own content box. Every tile therefore gets the same
       number, whatever the run holds. */
    width: var(--lp-cell);
    height: var(--lp-cell);
    position: relative;
    padding: 0;
    border: 1px solid var(--line);
    border-radius: 20px;
    background: var(--bg-elev);
    overflow: hidden;
    cursor: pointer;
    min-height: 0;
    display: block;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.28);
    animation: lp-choice-in 260ms cubic-bezier(0.2, 0, 0.2, 1) both;
    transition: border-color var(--transition), transform var(--transition), box-shadow var(--transition);
}

@keyframes lp-choice-in {
    from { opacity: 0; transform: translateY(14px) scale(0.985); }
    to   { opacity: 1; transform: none; }
}

.lp-choice-tile:hover,
.lp-choice-tile:focus-visible {
    border-color: var(--accent);
    transform: translateY(-4px);
    box-shadow: 0 18px 44px rgba(0, 0, 0, 0.45), 0 0 0 1px var(--accent);
}

/* `contain`, never `cover`: a candidate the user has to judge cannot be
   cropped. The cells above are already square, so on the pipeline's own output
   this changes nothing visible; it is what keeps an uploaded reference of any
   other ratio — and the cells that fall back to landscape on a narrow window —
   whole and undistorted instead of trimmed. The tile's own `--bg-elev` is the
   neutral backdrop the letterboxing lands on. */
.lp-choice-tile img,
.lp-choice-tile .lp-studio-mesh-thumb {
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

/* The mesh renderer draws its own placeholder box; make it fill the tile. */
.lp-choice-tile .lp-studio-mesh-thumb { display: grid; place-items: center; }

/* What clicking does, said only on hover: an always-on label over every tile
   is four labels shouting the same word. */
/* ── The hover verb ───────────────────────────────────────────────────────
   IT USED TO SAY "CHOOSE THIS ONE", AND IT WAS THE WRONG WAY ROUND. Clicking
   the picture — what a person does to mean "that one" — opened a modal, and
   choosing was this button, which only exists on hover and therefore does not
   exist at all on a touch screen. The two swapped: the tile chooses, and this
   enlarges. See ChoiceScreen. */
.lp-choice-zoom {
    position: absolute;
    inset: auto 0 0 0;
    display: flex;
    align-items: flex-end;
    justify-content: center;
    gap: 8px;
    padding: 40px 12px 18px;
    background: linear-gradient(to top, rgba(0, 0, 0, 0.72), transparent);
    opacity: 0;
    transform: translateY(6px);
    transition: opacity var(--transition), transform var(--transition);
    pointer-events: none;
}

.lp-choice-tile:hover .lp-choice-zoom,
.lp-choice-tile:focus-visible .lp-choice-zoom { opacity: 1; transform: none; }

/* Glass, not accent — and that is the point of the restyle, not a taste.
   `--accent` is what says CHOSEN on this screen (see `.is-taken` below), and it
   cannot also be what says "look closer": the tile the user has picked would
   wear the same colour as the pill hovering over the tile they have not. Dark
   translucent over the gradient reads on any candidate, image or mesh, and
   leaves the accent to mean one thing. */
.lp-choice-zoom-btn {
    background: rgba(0, 0, 0, 0.55);
    color: #fff;
    font-weight: 600;
    font-size: 0.875rem;
    font-family: inherit;
    border: 1px solid rgba(255, 255, 255, 0.28);
    border-radius: 999px;
    padding: 8px 20px;
    cursor: pointer;
    pointer-events: auto;
    backdrop-filter: blur(4px);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
    transition: background var(--transition), transform var(--transition);
}

.lp-choice-zoom-btn:hover:not(:disabled) {
    background: rgba(0, 0, 0, 0.72);
    border-color: rgba(255, 255, 255, 0.45);
    transform: translateY(-1px);
}

.lp-choice-zoom-btn:disabled { cursor: default; opacity: 0.45; }

/* Taking the candidate's own file. Round rather than a pill, and glyph-only:
   the verb next to it carries the words, and two labelled pills on a 280px
   tile is a toolbar. Same glass as the Enlarge pill so the pair reads as one
   control group — and, like it, deliberately not `--accent`, which on this
   screen means "chosen". */
.lp-choice-dl {
    display: inline-grid;
    place-items: center;
    width: 36px;
    height: 36px;
    flex: 0 0 auto;
    color: #fff;
    background: rgba(0, 0, 0, 0.55);
    border: 1px solid rgba(255, 255, 255, 0.28);
    border-radius: 50%;
    cursor: pointer;
    pointer-events: auto;
    backdrop-filter: blur(4px);
    box-shadow: 0 6px 20px rgba(0, 0, 0, 0.45);
    transition: background var(--transition), transform var(--transition);
}

.lp-choice-dl:hover {
    background: rgba(0, 0, 0, 0.72);
    border-color: rgba(255, 255, 255, 0.45);
    transform: translateY(-1px);
}

.lp-choice-dl svg { width: 18px; height: 18px; display: block; }

/* Meshes are rendered square and must be seen WHOLE — a cropped model is a
   model the user cannot judge.

   `position: absolute` rather than `height: 100%`, and that is the whole fix:
   the thumb wrapper is a grid whose single row is `auto`, so the row took the
   768px image's height and the image's own `height: 100%` then resolved
   against that row — 550px inside a 279px card, overflowing it. Taking the
   image out of flow makes the card the only thing that decides the size. */
.lp-choice-tile .lp-studio-mesh-thumb,
.lp-panel-media .lp-studio-mesh-thumb,
.lp-flow-done-media .lp-studio-mesh-thumb,
.lp-flow-thumb .lp-studio-mesh-thumb,
.lp-flow-home-thumb .lp-studio-mesh-thumb {
    display: block;
    position: relative;
}

.lp-choice-tile .lp-studio-mesh-thumb img,
.lp-panel-media .lp-studio-mesh-thumb img,
.lp-flow-done-media .lp-studio-mesh-thumb img,
.lp-flow-thumb .lp-studio-mesh-thumb img,
.lp-flow-home-thumb .lp-studio-mesh-thumb img {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    object-fit: contain;
}

/* ── The enlarged panel ───────────────────────────────────────────────────
   Clicking a bubble opens it here. Looking is free; the only thing that
   commits is the same button as on the tile. */

.lp-panel-scrim {
    position: fixed;
    inset: 0;
    z-index: 60;
    background: rgba(0, 0, 0, 0.72);
    backdrop-filter: blur(4px);
    display: grid;
    place-items: center;
    padding: 28px;
    animation: lp-panel-fade 160ms ease both;
    /* Query container for the panel: `100cqh` below is the height the scrim
       allows, which is the only way the panel's WIDTH can be derived from the
       picture's height. Free of side effects — this box is `inset: 0` on a
       fixed element, so its size never came from its contents. */
    container-type: size;
}

@keyframes lp-panel-fade { from { opacity: 0; } to { opacity: 1; } }

/* `height`, not just `max-height` — see `.lp-panel-media` below for why the
   row underneath cannot do its job without it. */
.lp-panel {
    /* Head + foot, measured: the two rows the picture does NOT get. */
    --lp-panel-chrome: 139px;
    /* The band kept clear beside the picture on each side, so the prev/next
       arrows have somewhere to sit that is not on top of it. 42px button +
       14px inset + air. */
    --lp-panel-gutter: 72px;
    position: relative;
    width: min(1080px, 100%);
    height: min(880px, 100%);
    max-height: 100%;
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
    background: var(--bg-elev);
    border: 1px solid var(--line-strong);
    border-radius: 20px;
    overflow: hidden;
    box-shadow: 0 30px 80px rgba(0, 0, 0, 0.6);
    animation: lp-panel-rise 200ms cubic-bezier(0.2, 0, 0.2, 1) both;
}

/* A live 3D stage needs a box of its own.
   The panel's middle row is `minmax(0, 1fr)`, and `1fr` only means anything
   inside a container of DEFINITE height — an <img> gives the panel one, a
   <canvas> does not (WebGL has no intrinsic size), so the mesh row collapsed
   to a sliver and the model was framed inside a 1080 x ~150 letterbox. Giving
   the panel a height hands the row the space it was asking for; `100%` keeps
   it inside the scrim on short screens, so the footer is never cut off and
   nothing here needs a media query. */
.lp-panel.is-mesh { height: min(820px, 100%); }

@keyframes lp-panel-rise {
    from { opacity: 0; transform: translateY(12px) scale(0.99); }
    to   { opacity: 1; transform: none; }
}

.lp-panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 12px;
    padding: 12px 16px;
    border-bottom: 1px solid var(--line);
}

.lp-panel-count {
    font-family: var(--font-mono);
    font-size: 0.75rem;
    letter-spacing: 0.08em;
    color: var(--fg-faint);
    font-variant-numeric: tabular-nums;
}

.lp-panel-close {
    background: transparent;
    border: 1px solid var(--line);
    border-radius: 999px;
    width: 30px;
    height: 30px;
    color: var(--fg-muted);
    font-size: 1.125rem;
    line-height: 1;
    cursor: pointer;
    transition: color var(--transition), border-color var(--transition);
}

.lp-panel-close:hover { color: var(--fg); border-color: var(--line-strong); }

/* The picture IS the panel.
   The middle row is `minmax(0, 1fr)`, and `1fr` only resolves inside a box of
   definite height — exactly the trap `.is-mesh` already documents, and the
   image case walked into it too. With the panel sized by its content, the row
   took the file's own pixel height (a 1024px square), the panel overshot
   `max-height: 100%`, and `overflow: hidden` cut the top and the bottom off
   the very thing the user opened the panel to look at. `.lp-panel` now has a
   height, so the row is definite.
   On top of that the media box takes the picture's OWN ratio rather than
   stretching to the panel's width: `--lp-panel-ar` is set inline by
   CandidatePanel from the artifact's `metadata.width/height`, or from the
   file's natural size once it has loaded. `1` is both the fallback and the
   ratio the pipeline actually produces. */
.lp-panel-media {
    position: relative;
    min-height: 0;
    display: grid;
    place-items: center;
    background: var(--bg);
    overflow: hidden;
}

/* THE PANEL IS AS WIDE AS THE PICTURE IT FRAMES, plus the two arrow gutters.
   Sized from the height, because the height is what is scarce: the media row
   gets `height - chrome`, the picture is that times its own ratio, and the
   panel is that plus the gutters. A fixed 1080px frame around a square 741px
   picture left 170px of dead panel on each side and put the arrows on top of
   the image; this hugs it at every ratio, on any window. `min()` keeps the old
   1080px ceiling and the scrim's own width as the two other bounds. The bare
   `2px` is the panel's own left+right border: the box is border-box, so
   without it the media's `100% - gutters` lands 2px short of the picture and
   the ratio is clipped by that much. */
.lp-panel:not(.is-mesh) {
    /* What the scrim allows, per axis, capped at the sizes this panel has
       always used. */
    --lp-panel-w-room: min(1080px, 100cqw);
    --lp-panel-h-room: min(880px, 100cqh);
    /* The picture's WIDTH, and the one number the whole frame is built from:
       whichever of the two rooms runs out first. The floor keeps a window
       shorter than the chrome itself (a 300px-tall browser) from computing a
       negative size and collapsing the panel to a sliver — `max-width` /
       `max-height` below are what catch that case instead. */
    --lp-panel-pic: max(180px, min(
        var(--lp-panel-w-room) - 2 * var(--lp-panel-gutter) - 2px,
        (var(--lp-panel-h-room) - var(--lp-panel-chrome)) * var(--lp-panel-ar, 1)
    ));
    width: calc(var(--lp-panel-pic) + 2 * var(--lp-panel-gutter) + 2px);
    height: calc(var(--lp-panel-pic) / var(--lp-panel-ar, 1) + var(--lp-panel-chrome));
    max-width: 100%;
}

/* On a phone the gutters ARE the picture: 144px reserved out of ~340 leaves
   the candidate smaller than it was on the tile it was opened from. The arrows
   go back over the image there — overlapping a corner of it costs less than
   showing it at half size. */
@media (max-width: 720px) {
    .lp-panel { --lp-panel-gutter: 0px; }
}

.lp-panel:not(.is-mesh) .lp-panel-media {
    aspect-ratio: var(--lp-panel-ar, 1);
    height: 100%;
    width: auto;
    /* Never under the arrows: the gutters are subtracted here too, so a
       picture wider than the frame letterboxes inside the box rather than
       sliding beneath the controls. */
    max-width: calc(100% - 2 * var(--lp-panel-gutter));
    justify-self: center;
    /* Static on purpose: the arrows then anchor to the PANEL and land in the
       gutters, instead of to a media box that now stops at the picture's edge
       — which is where they were drawn, on top of the very thing being
       judged. The mesh case keeps `position: relative` (rule above): its stage
       fills the panel, so its arrows are already clear of the model. */
    position: static;
}

.lp-panel-media img,
.lp-panel-media .lp-studio-mesh-thumb,
.lp-panel-media .lp-studio-mesh-thumb img {
    max-width: 100%;
    max-height: 100%;
    width: 100%;
    height: 100%;
    object-fit: contain;
    display: block;
}

.lp-panel-nav {
    position: absolute;
    top: 50%;
    transform: translateY(-50%);
    width: 42px;
    height: 42px;
    border-radius: 50%;
    border: 1px solid var(--line-strong);
    background: color-mix(in srgb, var(--bg-elev) 88%, transparent);
    color: var(--fg-muted);
    display: grid;
    place-items: center;
    cursor: pointer;
    z-index: 2;
    transition: color var(--transition), border-color var(--transition);
}

.lp-panel-nav:hover { color: var(--accent); border-color: var(--accent); }
.lp-panel-nav.is-prev { left: 14px; }
.lp-panel-nav.is-next { right: 14px; }
.lp-panel-nav svg { width: 18px; height: 18px; }

.lp-panel-foot {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 14px;
    padding: 14px 16px 18px;
    border-top: 1px solid var(--line);
}

/* Taking the file, next to taking the candidate. Quiet on purpose: one button
   in this footer commits the choice, and it stays the only loud thing here. */
.lp-panel-dl {
    display: inline-flex;
    align-items: center;
    gap: 7px;
    font-size: 0.875rem;
    color: var(--fg-muted);
    text-decoration: none;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    padding: 9px 14px;
    transition: color var(--transition), border-color var(--transition);
}

.lp-panel-dl:hover { color: var(--fg); border-color: var(--line-strong); }
.lp-panel-dl svg { width: 16px; height: 16px; display: block; }

.lp-panel-hint {
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    color: var(--fg-faint);
}

/* The live viewer fills the panel's stage. */
.lp-panel-media > div { width: 100%; height: 100%; }

/* ── THE ONE THAT IS CHOSEN ───────────────────────────────────────────────
   This was a medal: a quiet hairline on the candidate a finished project
   happened to be built on, seen once in a while when a step was revisited. It
   is now the ANSWER TO EVERY CLICK on this screen — the tile is the choice —
   so it has to be legible across the room, on a 279 px thumbnail, next to three
   others, and it has to survive a hover gradient sweeping over its bottom half.

   Two rings rather than a thicker one: the accent ring sits on the border, and
   a second ring of the page's own background separates it from the tile beside
   it, so the mark reads as a lifted card instead of a coloured edge that could
   pass for a focus outline. No new colour — `--accent` and `--bg` — and the
   badge says which of the four it is in words, for anyone who does not read the
   ring as a state. */
.lp-choice-tile.is-taken {
    border-color: var(--accent);
    box-shadow: 0 0 0 3px var(--accent), 0 0 0 5px var(--bg);
}

.lp-choice-tile.is-taken::before {
    content: "chosen";
    position: absolute;
    /* TOP-RIGHT. The retopology tile already prints its face count top-LEFT
       (`.lp-topo-chip`), and two pills in one corner is two pills on top of one
       another — the one screen where a selection is now made constantly. */
    right: 12px;
    top: 12px;
    z-index: 1;
    font-family: var(--font-mono);
    font-size: 0.625rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    background: var(--accent);
    color: var(--accent-fg);
    border-radius: 999px;
    padding: 3px 9px;
}

/* A tile whose result has not landed yet. Same box, so the four never move
   under the cursor as they fill in.

   SOLID EDGE, NOT DASHED. A dashed outline is what this panel uses for a
   placeholder — a slot with nothing coming. A tile with a live ring on it is
   the opposite of that, and the two read the same from a metre away. The
   surface is `--bg-elev` rather than the page's own `--bg` for the same reason:
   a box that is working is a box that is there. That holds for a QUEUED tile
   too — its job is booked and paid for; only its ring says it has not started
   yet. */
.lp-choice-tile.is-pending {
    cursor: default;
    border-style: solid;
    display: grid;
    place-items: center;
    position: relative;
    overflow: hidden;
    background: var(--bg-elev);
}

.lp-choice-tile.is-pending:hover { transform: none; border-color: var(--line); box-shadow: none; }

/* ── The wait, as a ring ──────────────────────────────────────────────────
   The tile holds ONE mark, `.lp-ring` (PendingRing.tsx, styled in
   lowpoly/base.css because the /generate stream and the workspace's candidate
   strip draw the same thing). Everything that used to live here — the
   `.lp-choice-pending` flex box, `.lp-choice-pending-num`, `-unit` and the
   bottom-edge `-bar` — went with the digits it existed to lay out.

   WHY THE DIGITS WENT: the tile had become a 2.5rem figure filling the box,
   and four of those side by side is a wall of arithmetic. The full history of
   this tile (spinner → number in a ring → number alone → ring) is in
   PendingTile.tsx; the short version is that a placeholder is glanced at, not
   read, and a ring is the shape for that.

   Only the FIRST placeholder of a run gets a filled ring: the outputs are
   produced one at a time, so the rest are queued and `.is-queued` says so
   without a fill. Nothing is declared here for that state — the difference is
   in the ring, which is the point of having one implementation of it. */

/* Sized for a tile: bigger than the strip's, small enough that the box still
   reads as an empty slot rather than as a widget. */
.lp-choice-tile.is-pending .lp-ring {
    --lp-ring-size: clamp(34px, 7vh, 52px);
    --lp-ring-width: 4px;
}

/* ── The topology step ────────────────────────────────────────────────────
   Four tiles, one switch above them, and ONE number per tile.

   The tile used to carry a caption row under the render — a name and a face
   count — because a preset had a NAME and a size and neither was visible in the
   picture. The mode switch took the name away: once the stance is chosen once,
   above the grid, the four tiles differ by how much geometry each one has, and
   that is a number. So the caption row is gone and the number is a chip ON the
   render.

   That is NOT a return to the badge this screen threw out. That badge was four
   lines — name, share of the source, faces, quad share and a sentence of prose
   — set in --fg-muted over a white mesh, covering the part of the model the
   user was being asked to look at. One short mono figure on an opaque backdrop,
   in a corner, is legible on any mesh and gives the render back the strip the
   caption row was taking.

   The square cell itself is untouched — `--lp-cell` still decides the tile's
   outer size, exactly as on every other grid in the flow, so the four still line
   up with a run of candidates seen one step earlier. */

/* ONE MORE ROW THAN A CHOICE SCREEN.
   `.lp-choice` declares three tracks — head, stage, foot — and this screen has
   four children. Without this the switch would take the stage's `1fr` and the
   grid would be squeezed into the foot's `auto`. Stated here rather than by
   widening `.lp-choice`, so `ChoiceScreen` keeps exactly the box it had. */
.lp-topo-choice {
    grid-template-rows: auto auto minmax(0, 1fr) auto;
}

/* The mode switch. Three pills, centred over the grid, in the same idiom as
   the panel's wire toggle — a pressed state rather than a select, because
   there are three of them, they are all worth reading at once, and the current
   one has to be visible without opening anything. */
.lp-topo-modes {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 0 24px 14px;
}

.lp-topo-mode-btn {
    font-family: inherit;
    font-size: 0.8125rem;
    font-weight: 600;
    letter-spacing: 0.01em;
    color: var(--fg-muted);
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 7px 18px;
    cursor: pointer;
    transition: color var(--transition), border-color var(--transition), background var(--transition);
}

.lp-topo-mode-btn:hover { color: var(--fg); border-color: var(--line-strong); }

/* The current stance, and it has to be unmistakable: everything under it was
   computed by it, and the four pictures cannot say which one they came from. */
.lp-topo-mode-btn.is-on {
    color: var(--accent-fg);
    background: var(--accent);
    border-color: var(--accent);
    cursor: default;
}

.lp-topo-tile {
    display: flex;
    flex-direction: column;
}

/* The render's own box, and the positioning context for the hover verb: with
   `.lp-choice-zoom` absolute against the TILE it would have spread its dark
   gradient over the caption as well, which is the overlay-on-text problem
   again, one element lower. `min-height: 0` because a flex item's automatic
   minimum size is its content, and the thumb would otherwise refuse to give
   the caption its line. */
.lp-topo-tile-media {
    position: relative;
    display: block;
    flex: 1 1 auto;
    min-height: 0;
    overflow: hidden;
}

/* (The thumb itself needs no rule: `.lp-choice-tile .lp-studio-mesh-thumb`
   already sizes it to 100 % of its parent, and its parent is now this box.) */

/* THE ONE BUBBLE. The faces delivered, and nothing else.

   TOP-left, and that corner is chosen: `.lp-choice-zoom`'s hover gradient runs
   from the BOTTOM edge upwards and is darkest exactly where a bottom-corner
   chip would sit, so the two would fight for the same pixels every time the
   pointer entered the tile. Up here the chip is over the render's own quietest
   region (a mesh thumb is rendered centred, with headroom) and the verb sweeps
   in under it untouched.

   Opaque, not translucent. These renders are near-white and the badge that got
   thrown out was --fg-muted straight over one; a solid dark pill with light
   text reads on any mesh, at any zoom, without knowing what is behind it.

   `pointer-events: none` so it never eats the click that enlarges the tile —
   the chip covers a corner of a box whose whole surface is a button. */
.lp-topo-chip {
    position: absolute;
    top: 10px;
    left: 10px;
    z-index: 1;
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    font-weight: 600;
    /* The four counts are read against each other, so the digits must not
       breathe from one tile to the next. */
    font-variant-numeric: tabular-nums;
    line-height: 1;
    color: #fff;
    background: rgba(0, 0, 0, 0.62);
    border-radius: 999px;
    padding: 5px 10px;
    pointer-events: none;
}

.lp-topo-failed {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100%;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    color: var(--fg-faint);
}

/* ── A variant, opened in the real viewer ─────────────────────────────────
   Everything structural is `.lp-panel*` — the same modal `CandidatePanel`
   uses, `is-mesh` included, so the 3D stage gets the definite height a
   <canvas> cannot ask for on its own. Only what this panel adds is here. */

/* Which preset is being turned. The count sits on the left and the close on
   the right (the head is a 3-item flex row), so this takes the middle and is
   allowed to shrink first — the two controls must never be pushed off. */
.lp-topo-panel-title {
    display: flex;
    align-items: baseline;
    gap: 10px;
    min-width: 0;
    overflow: hidden;
}

.lp-topo-panel-title b { font-size: 0.875rem; color: var(--fg); font-weight: 600; }

.lp-topo-panel-title span {
    font-size: 0.75rem;
    color: var(--fg-faint);
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* Tabular, because the four presets are read against each other. */
.lp-topo-panel-stats {
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    color: var(--fg-muted);
    font-variant-numeric: tabular-nums;
}

/* The wire toggle. A pressed-state button rather than a checkbox: it has one
   job and the user flips it back and forth while turning the model. */
.lp-topo-wire-btn {
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    letter-spacing: 0.04em;
    color: var(--fg-muted);
    background: transparent;
    border: 1px solid var(--line);
    border-radius: 999px;
    padding: 5px 12px;
    cursor: pointer;
    transition: color var(--transition), border-color var(--transition), background var(--transition);
}

.lp-topo-wire-btn:hover { color: var(--fg); border-color: var(--line-strong); }

.lp-topo-wire-btn.is-on {
    color: var(--accent-fg);
    background: var(--accent);
    border-color: var(--accent);
}

/* On a phone the footer would put five items on one line and cut the only one
   that commits anything. The verb keeps the last row to itself. */
@media (max-width: 720px) {
    .lp-panel-foot { flex-wrap: wrap; }
    .lp-topo-panel-title span { display: none; }
}

/* ── One row at the bottom: what was asked, and the only other verb ───────── */

.lp-choice-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 10px 16px;
    padding: 18px 24px 22px;
}

.lp-choice-prompt {
    font-family: var(--font-mono);
    font-size: 0.75rem;
    color: var(--fg-faint);
    max-width: 44ch;
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

.lp-choice-cost { font-family: var(--font-mono); font-size: 0.6875rem; color: var(--fg-faint); }

/* ── How many, and which generation ───────────────────────────────────────
   The two controls that sit next to the regenerate button. Both are small on
   purpose: the question on screen is "which one", not "how should I configure
   this". */

/* `.lp-gen-actions` — three buttons, three quantities, the number IS the button
   — is not used by the studio any more; see GenerateBar for what replaced it and
   why. The rules stay because `components/generate/Gate.tsx` still carries the
   class on the marketing island, which has its own sheet (`lowpoly/generate.css`)
   and is not loaded here. */
.lp-gen-actions {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 8px;
}

.lp-gen-actions .btn { gap: 8px; }
.lp-gen-actions .lp-flow-cost-chip { background: rgba(255, 255, 255, 0.06); }
.lp-gen-actions .btn-primary .lp-flow-cost-chip { background: rgba(0, 0, 0, 0.22); }

/* ── HOW MANY, THEN GO ────────────────────────────────────────────────────
   A segmented count and one primary button carrying the total price. What it
   replaced was `Generate 1 image / Generate 2 images / Generate 4 images` — the
   verb three times, the noun twice, and no price at all — or, on the later
   steps, `1 more / 2 more / 4 more` in the footer of a grid that was empty
   because the user had just chosen a candidate and arrived. See GenerateBar. */
.lp-genbar {
    display: flex;
    align-items: center;
    justify-content: center;
    flex-wrap: wrap;
    gap: 10px;
}

/* (The count's own look — `.lp-genbar-count`, `.lp-genbar-n` and its selected
   state, `.lp-genbar-go` and the chip inside it — is in lowpoly/composer.css:
   the same two controls sit on the composer's action bar, which the homepage
   mounts. Only `.lp-genbar` above, the footer row that centres them on a step,
   belongs to the flow.) */

/* ── WHAT AN ANSWERED STEP OFFERS ─────────────────────────────────────────
   Choosing a candidate no longer moves the project on by itself, so the footer
   of a step that HAS a choice on it carries the two things left to do: the next
   step, as the bar above (the count, the price, one primary button), and this
   step again, underneath, as a line of text.

   Two rows, not two buttons side by side: the second one costs the same credits
   as the first and would read as its equal on one line. Stacked and quiet, it
   reads as what it is — the way back into a decision already made. */
.lp-choice-next {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 10px;
}

/* The count only appears on this row when the PRIMARY button has none — a step
   followed by the retopology screen, or by the finished asset, generates
   nothing on the way there, so the run size belongs to "make more of these".
   See `RunCount` in GenerateBar. */
.lp-choice-again {
    display: flex;
    align-items: center;
    justify-content: center;
    gap: 10px;
}

/* Same idiom as `.lp-flow-link`, one step quieter: `--fg-faint` rather than the
   accent, because it sits directly under a filled accent button and two accents
   an inch apart is two primary actions. */
.lp-choice-again-btn {
    background: none;
    border: 0;
    padding: 4px 2px;
    font-size: 0.8125rem;
    color: var(--fg-faint);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 3px;
    transition: color var(--transition);
}

.lp-choice-again-btn:hover:not(:disabled) { color: var(--fg); }
.lp-choice-again-btn:disabled { cursor: default; opacity: 0.5; text-decoration: none; }

/* The counter next to that line is a footnote-sized control, not the full
   44px-tall segmented bar it is when it drives the primary button. */
.lp-choice-again .lp-genbar-n {
    min-width: 32px;
    padding: 6px 4px;
    font-size: 0.8125rem;
}

/* The empty state of a step the user has just arrived at by choosing something:
   the question, then the bar. Two rows rather than the usual three — there is
   no grid of candidates and no footer, and leaving the 1fr middle row in place
   would push the bar to the bottom edge of the frame. */
.lp-choice.is-empty { grid-template-rows: auto minmax(0, 1fr); }
.lp-choice-start {
    display: flex;
    align-items: center;
    justify-content: center;
    padding: clamp(16px, 6vh, 56px) 16px;
}

.lp-run-nav {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    padding: 2px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--bg);
}

.lp-run-nav-btn {
    width: 26px;
    height: 26px;
    border: 0;
    border-radius: 50%;
    background: transparent;
    color: var(--fg-muted);
    font-size: 1rem;
    line-height: 1;
    cursor: pointer;
    transition: color var(--transition), background var(--transition);
}

.lp-run-nav-btn:hover:not(:disabled) { color: var(--accent); background: var(--bg-elev-2); }
.lp-run-nav-btn:disabled { opacity: 0.3; cursor: default; }

.lp-run-nav-label {
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    color: var(--fg-muted);
    padding: 0 4px;
    font-variant-numeric: tabular-nums;
    white-space: nowrap;
}

.lp-flow-composer-spacer { flex: 1; }

/* ── Finished asset ───────────────────────────────────────────────────────
   The result, big, and the two things a person does with it: take the file, or
   put it in the gallery. Editing is not here and is not meant to be — the site
   has somewhere else for that. */

.lp-flow-done {
    display: grid;
    grid-template-rows: auto minmax(0, 1fr) auto;
    min-height: 0;
    background:
        radial-gradient(120% 70% at 50% 0%, var(--accent-soft), transparent 60%),
        var(--bg);
}

.lp-flow-done-head {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 2px;
    padding: 16px 22px 10px;
    text-align: center;
}

.lp-flow-done-head h2 {
    margin: 0;
    font-family: var(--font-brand);
    font-size: clamp(1.2rem, 2.4vw, 1.5rem);
    letter-spacing: -0.02em;
    color: var(--fg);
}

.lp-flow-done-head p { margin: 0; font-size: 0.875rem; color: var(--fg-muted); }

.lp-flow-done-stage {
    display: grid;
    place-items: center;
    min-height: 0;
    padding: 0 22px;
}

.lp-flow-done-media {
    position: relative;
    width: 100%;
    height: 100%;
    max-width: 900px;
    border-radius: var(--radius-lg);
    border: 1px solid var(--line);
    background: var(--bg-elev);
    box-shadow: var(--shadow);
    overflow: hidden;
    display: grid;
    place-items: center;
}

.lp-flow-done-media img,
.lp-flow-done-media .lp-studio-mesh-thumb { width: 100%; height: 100%; object-fit: contain; display: grid; place-items: center; }

/* The live stage and the snapshot it fades in over.

   Both fill the media box out of flow, so the box's height — which the row
   above already settles — is never a function of what is inside it: a WebGL
   canvas has no intrinsic size, and letting it take part in the layout is
   exactly how a stage collapses to a sliver. Stacking order is DOM order, the
   canvas on top; the poster only stops being visible once the scene reports
   itself loaded, because a mounted-but-empty canvas is transparent and would
   otherwise show the snapshot straight through it. */
.lp-flow-done-poster,
.lp-flow-done-canvas {
    position: absolute;
    inset: 0;
}

.lp-flow-done-poster {
    pointer-events: none;
    transition: opacity var(--transition);
}

.lp-flow-done-poster.is-behind { opacity: 0; }

/* ── The display modes ────────────────────────────────────────────────────
   Solid / Wire / UV / Normals, plus the texture switch, floated over the top
   left of the stage. Glass rather than accent, for the same reason the
   candidate tiles' Enlarge pill is: on this screen `--accent` means "this is
   the one you chose", and a control that borrowed it would read as a state of
   the asset instead of a state of the view.

   `z-index` puts the row above the canvas, which fills the box out of flow;
   without it the WebGL surface paints over the buttons. */
.lp-flow-view {
    position: absolute;
    top: 12px;
    left: 12px;
    z-index: 4;
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    gap: 4px;
    padding: 4px;
    border-radius: 999px;
    background: rgba(0, 0, 0, 0.42);
    border: 1px solid rgba(255, 255, 255, 0.16);
    backdrop-filter: blur(6px);
}

.lp-flow-view-btn {
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    color: rgba(255, 255, 255, 0.72);
    background: none;
    border: 0;
    border-radius: 999px;
    padding: 5px 11px;
    cursor: pointer;
    transition: background var(--transition), color var(--transition);
}

.lp-flow-view-btn:hover:not(:disabled) { color: #fff; background: rgba(255, 255, 255, 0.12); }
.lp-flow-view-btn.on { color: #fff; background: rgba(255, 255, 255, 0.22); }
.lp-flow-view-btn:disabled { opacity: 0.4; cursor: default; }

/* The texture switch is not one of the four readings — it composes with two of
   them. Set apart by a hairline so the row reads as "pick one of these, and
   this other thing on top". */
.lp-flow-view-btn.is-toggle {
    margin-left: 2px;
    border-left: 1px solid rgba(255, 255, 255, 0.18);
    border-radius: 0 999px 999px 0;
    padding-left: 12px;
}

@media (max-width: 640px) {
    .lp-flow-view { top: 8px; left: 8px; gap: 2px; padding: 3px; }
    .lp-flow-view-btn { padding: 4px 8px; font-size: 0.625rem; }
}

.lp-flow-done-foot {
    display: flex;
    flex-wrap: wrap;
    align-items: center;
    justify-content: center;
    gap: 10px 14px;
    padding: 16px 22px 20px;
}

/* The generations behind the asset, folded away. Open only when asked: the
   finished screen is not a place to browse. */
.lp-flow-history {
    border-top: 1px solid var(--line);
    background: var(--bg-elev);
    padding: 16px 22px 20px;
    display: flex;
    flex-direction: column;
    gap: 16px;
    max-height: 40vh;
    overflow-y: auto;
    /* Centred and capped: this strip inherited a 340px side panel's grid and,
       moved to the full width of the screen, drew four 380px thumbnails per
       step — a contact sheet where a reminder was wanted. */
    width: min(1080px, 100%);
    margin: 0 auto;
}

.lp-flow-history .lp-flow-thumbs {
    grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
}

.lp-flow-panel h3 {
    margin: 0;
    font-family: var(--font-brand);
    font-size: 1.125rem;
    letter-spacing: -0.015em;
    color: var(--fg);
}

.lp-flow-panel .lp-flow-sub { margin: 2px 0 0; font-size: 0.875rem; color: var(--fg-muted); }

.lp-flow-label {
    font-family: var(--font-mono);
    font-size: 0.625rem;
    letter-spacing: 0.14em;
    text-transform: uppercase;
    color: var(--fg-faint);
}

.lp-flow-field { display: flex; flex-direction: column; gap: 8px; }

.lp-flow-recap { display: flex; flex-direction: column; gap: 14px; }
.lp-flow-recap-step { display: flex; flex-direction: column; gap: 7px; }

.lp-flow-recap-head { display: flex; align-items: baseline; gap: 8px; font-size: 0.875rem; color: var(--fg); }
.lp-flow-recap-head .lp-flow-picked { color: var(--fg-muted); font-size: 0.8125rem; }
.lp-flow-recap-head .lp-flow-spacer { flex: 1; }

/* Stands where the "change" link used to, so the row keeps its shape — and
   states a fact instead of offering a way back. */
.lp-flow-recap-count {
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    letter-spacing: 0.04em;
    color: var(--fg-faint);
    font-variant-numeric: tabular-nums;
}

.lp-flow-link {
    background: none;
    border: 0;
    padding: 0;
    font-size: 0.8125rem;
    color: var(--accent-text);
    cursor: pointer;
    text-decoration: underline;
    text-underline-offset: 3px;
    transition: color var(--transition);
}

.lp-flow-link:hover { color: var(--accent); }

.lp-flow-thumbs { display: grid; grid-template-columns: repeat(4, 1fr); gap: 6px; }

.lp-flow-thumb {
    position: relative;
    aspect-ratio: 1;
    border: 1px solid var(--line);
    border-radius: var(--radius-sm);
    overflow: hidden;
    background: var(--bg);
    padding: 0;
    cursor: pointer;
    transition: border-color var(--transition), transform var(--transition);
}

.lp-flow-thumb:hover:not(.is-static) { border-color: var(--accent); transform: translateY(-1px); }
.lp-flow-thumb[aria-pressed="true"],
.lp-flow-thumb[data-taken="true"] { border-color: var(--accent); box-shadow: 0 0 0 1px var(--accent); }

/* The finished screen's recap: a record of what was made, not a way back into
   it. Same tile, without the affordance. */
.lp-flow-thumb.is-static { cursor: default; }
.lp-flow-thumb img,
.lp-flow-thumb .lp-studio-mesh-thumb { width: 100%; height: 100%; object-fit: cover; display: block; }

.lp-flow-stats { display: grid; grid-template-columns: 1fr auto; gap: 4px 12px; font-size: 0.875rem; }
.lp-flow-stats dt { color: var(--fg-muted); }
.lp-flow-stats dd {
    margin: 0;
    font-family: var(--font-mono);
    font-size: 0.75rem;
    text-align: right;
    font-variant-numeric: tabular-nums;
    color: var(--fg);
}

.lp-flow-actions { display: flex; flex-direction: column; gap: 8px; }

.lp-flow-actions .btn { justify-content: space-between; width: 100%; }
.lp-flow-action-note { font-family: var(--font-mono); font-size: 0.6875rem; opacity: 0.75; }

.lp-flow-export { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
.lp-flow-formats { display: flex; flex-wrap: wrap; gap: 4px; }

.lp-flow-format {
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    letter-spacing: 0.04em;
    padding: 6px 11px;
    border-radius: 999px;
    border: 1px solid var(--line);
    background: var(--bg-elev);
    color: var(--fg-muted);
    cursor: pointer;
    transition: border-color var(--transition), color var(--transition), background var(--transition);
}

.lp-flow-format:hover:not(:disabled) { color: var(--fg); border-color: var(--line-strong); }
.lp-flow-format[aria-checked="true"] { color: var(--accent-text); border-color: var(--accent-line); background: var(--accent-soft); }
.lp-flow-format:disabled { opacity: .5; cursor: default; }

.lp-flow-callout {
    background: var(--accent-soft);
    border: 1px solid var(--accent-line);
    border-radius: var(--radius-sm);
    padding: 10px 12px;
    font-size: 0.8125rem;
    color: var(--fg-muted);
}

/* ── The landing screen ───────────────────────────────────────────────────
   One line of text, and everything already made underneath it. */

/* The scroll container. The block sizes above are tuned so the fold lands
   between two rows of thumbnails rather than through one — but a fold is a
   moving target (a cookie bar, an impersonation bar, a zoom level, a font that
   loads late all shift it), so the last 44px of the port also dissolve into the
   page. Whatever the fold catches then reads as "this continues" instead of
   "this is broken", which was the actual complaint. Sticky, so it stays on the
   bottom edge while scrolling; negative margin, so it costs no layout height;
   and it only ever covers the section's 40px of bottom padding once the user
   has scrolled to the end. */
.lp-home::after {
    content: "";
    display: block;
    position: sticky;
    bottom: 0;
    height: 44px;
    margin-top: -44px;
    pointer-events: none;
    background: linear-gradient(
        to top,
        color-mix(in srgb, var(--bg-elev) 40%, var(--bg)) 0%,
        color-mix(in srgb, var(--bg-elev) 40%, var(--bg)) 18%,
        transparent 100%
    );
}

.lp-home {
    height: 100%;
    min-height: 0;
    overflow-y: auto;
    /* See `.lp-flow-ask` above: the grid is `.grid-overlay`, never a local
       background-image, and the orange radial is gone with it. */
    background: transparent;
}

/* NO min-height, and that is the whole point of this block.
   It used to be `min(62vh, 560px)` with the content centred inside it, so on a
   1080p screen the ask stood in a 560px box it only half filled: ~80px of dead
   air under the Generate buttons, and "Your generations" pushed to y≈672 —
   which put its first row of thumbnails astride the bottom of the window. The
   block is now exactly as tall as what it holds (≈380px), so the library below
   starts at a predictable line and its first rows land whole above the fold.
   Every number below was measured, not guessed: see the row-fit note on
   `.lp-home-past`. */
.lp-home-ask {
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 12px;
    padding: 24px 24px 20px;
    text-align: center;
}

.lp-home-ask h1 {
    margin: 0;
    font-family: var(--font-brand);
    font-weight: 700;
    /* One line, at every width this screen is used at. The old clamp topped out
       at 3rem with an 18ch cap, which wrapped "What do you want to make?" onto
       two lines and cost 50px of height for no added meaning. */
    font-size: clamp(1.5rem, 2.4vw, 2rem);
    line-height: 1.1;
    letter-spacing: -0.03em;
    color: var(--fg);
    max-width: 30ch;
}

.lp-home-ask p {
    margin: 0;
    color: var(--fg-muted);
    font-size: 0.875rem;
    max-width: 56ch;
}

.lp-home-ask .lp-flow-composer { width: min(640px, 100%); text-align: left; }

/* ── THE LANDING'S FIELD IS SIZED IN home.css, NOT HERE ───────────────────
   Two rules stood here — `.lp-home-ask .lp-flow-composer textarea` and
   `.lp-home-ask .lp-flow-composer-tools` — giving the landing a tighter box
   than the in-flow ask screen. They never applied: `layout_studio.php` loads
   this sheet and THEN `studio/home.css`, which set the same two selectors at
   the same specificity, so the later sheet won every declaration and the values
   here were decoration that read like configuration.

   That is worse than it sounds — the two sets had drifted apart, so a reader
   retuning the landing's field would have edited these numbers and watched
   nothing move. Removed rather than synchronised: one owner per selector.

   Where each one lives now:
     · the FIELD  — `.lp-home-ask .lp-flow-composer textarea` in studio/home.css,
       including the padding band the Embellish pill sits in;
     · the STRIP  — `.lp-flow-composer-tools` above in THIS file. home.css's
       override of it was byte-identical and has gone too, so the landing and
       the in-flow screen share one strip on purpose. */

/* (The object/character cards — `.lp-kind`, `.lp-kind-btn`, `.lp-kind-text`,
   `.lp-kind-name`, `.lp-kind-hint` and the selected state — are in
   lowpoly/composer.css. They are part of the composer, not of the landing:
   the homepage hero asks the same question above the same box. Only
   `.lp-home-ask .lp-kind` in studio/home.css tunes them for this screen.) */

/* ── Everything already made ───────────────────────────────────────────────
   Where this section starts is a measurement, not a taste: the ask block above
   is 385px tall, the studio bar and the site header take 112px, and this
   header costs 20 + 20 + 12 = 52px, so the first row of thumbnails begins at
   y≈550 on every screen. With a card pitch of ~175px that puts two whole rows
   above the fold in a real 1080p browser window (and three in a 1080px
   viewport), two whole rows at 1440x900, and one at 1280x720 — with whatever
   follows starting below the fold or peeking by a few pixels, never sliced
   through the middle. Change the padding here or the ask block's height and
   those four numbers move together: re-measure before shipping. */
.lp-home-past {
    border-top: 1px solid var(--line);
    padding: 20px 28px 40px;
    display: flex;
    flex-direction: column;
    gap: 12px;
    background: color-mix(in srgb, var(--bg-elev) 40%, transparent);
}

.lp-home-past h2 {
    margin: 0;
    font-family: var(--font-brand);
    font-weight: 600;
    font-size: 1.0625rem;
    letter-spacing: -0.015em;
    color: var(--fg);
}

.lp-home-note { margin: 0; font-size: 0.875rem; color: var(--fg-faint); }

.lp-home-grid {
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
    gap: 12px;
}

.lp-home-card {
    display: flex;
    flex-direction: column;
    gap: 6px;
    padding: 0 0 8px;
    background: var(--bg-elev);
    border: 1px solid var(--line);
    border-radius: 14px;
    overflow: hidden;
    cursor: pointer;
    text-align: left;
    transition: border-color var(--transition), transform var(--transition);
}

.lp-home-card:hover { border-color: var(--accent); transform: translateY(-2px); }

/* 3:2 rather than 4:3. The thumbnails are `object-fit: contain`, so the wider
   box crops nothing — it only removes letterbox: 24px of card height at 196px
   wide, which is what buys the second full row of the grid above the fold. */
.lp-home-card-media {
    position: relative;
    display: block;
    aspect-ratio: 3 / 2;
    background: var(--bg);
    border-bottom: 1px solid var(--line);
    overflow: hidden;
}

.lp-home-card-media img { width: 100%; height: 100%; object-fit: contain; display: block; }

.lp-home-card-empty {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100%;
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    color: var(--fg-faint);
}

.lp-home-card-flag {
    position: absolute;
    left: 8px;
    top: 8px;
    font-family: var(--font-mono);
    font-size: 0.5625rem;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    padding: 3px 8px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-fg);
}

.lp-home-card-name {
    padding: 0 10px;
    font-size: 0.8125rem;
    color: var(--fg);
    overflow: hidden;
    text-overflow: ellipsis;
    white-space: nowrap;
}

/* Phones. The grid's 180px floor gave a single 334px-wide card per row on a
   390px screen — one asset per screenful, which is not a library. Two columns
   read fine at this size: the thumbnail is still 176px wide. */
@media (max-width: 620px) {
    .lp-home-ask { padding: 18px 16px 16px; gap: 10px; }
    .lp-home-past { padding: 16px 14px 32px; }
    .lp-home-grid {
        grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
        gap: 10px;
    }
}

/* ── Home cards ───────────────────────────────────────────────────────────
   The project grid on /studio gets a face and a state: a list of names said
   nothing about which asset was which, and nothing at all about the one left
   half-finished. */

.lp-studio-home-card.lp-flow-home-card {
    display: flex;
    flex-direction: column;
    gap: 8px;
    padding: 0 0 12px;
    overflow: hidden;
}

.lp-flow-home-thumb {
    position: relative;
    display: block;
    aspect-ratio: 4 / 3;
    background: var(--bg);
    border-bottom: 1px solid var(--line);
    overflow: hidden;
}

.lp-flow-home-thumb > img,
.lp-flow-home-thumb > .lp-studio-mesh-thumb { width: 100%; height: 100%; object-fit: cover; display: block; }

.lp-flow-home-thumb > .lp-studio-home-library-kind {
    display: grid;
    place-items: center;
    width: 100%;
    height: 100%;
    font-family: var(--font-mono);
    font-size: 0.6875rem;
    color: var(--fg-faint);
}

.lp-flow-home-badge {
    position: absolute;
    left: 10px;
    top: 10px;
    font-family: var(--font-mono);
    font-size: 0.625rem;
    letter-spacing: 0.06em;
    text-transform: uppercase;
    padding: 3px 8px;
    border-radius: 999px;
    background: var(--accent);
    color: var(--accent-fg);
}

.lp-flow-home-badge.is-done {
    background: color-mix(in srgb, var(--success) 16%, var(--bg-elev));
    color: var(--success);
    border: 1px solid color-mix(in srgb, var(--success) 40%, transparent);
}

.lp-flow-home-card .lp-studio-home-card-name,
.lp-flow-home-card .lp-studio-home-card-meta-row { padding: 0 12px; }

@media (max-width: 900px) {
    .lp-flow-asset { grid-template-columns: minmax(0, 1fr); grid-template-rows: minmax(220px, 1fr) auto; }
    .lp-flow-panel { border-left: 0; border-top: 1px solid var(--line); }
    .lp-choice-stage { padding: 4px 12px 8px; }
    .lp-choice-arrow { width: 38px; height: 38px; }
    .lp-choice-arrow.is-prev { left: 2px; }
    .lp-choice-arrow.is-next { right: 2px; }
}

@media (prefers-reduced-motion: reduce) {
    .lp-choice-media { animation: none; }
    /* The tiles' entry. Listed here because the placeholder tiles are now the
       thing a user watches for minutes at a time, and they are `.lp-choice-tile`
       like every other: the one moving part left on them must not be a 260 ms
       slide replayed every time a candidate lands. The rings on those tiles are
       not listed here and do not need to be: the filled one is a gauge that
       only moves when its value does, and the queued one's turn is opted INTO
       under `prefers-reduced-motion: no-preference` in lowpoly/base.css —
       which is the mistake the old `.lp-choice-spin` made the other way round
       (never listed here, so it turned for these users forever). */
    .lp-choice-tile { animation: none; }
    .lp-choice-pulse i { animation: none; width: 100%; opacity: .6; }
    .lp-choice-chip.is-pending i { animation: none; }
    .lp-choice-chip:hover,
    .lp-choice-tile:hover,
    .lp-flow-thumb:hover { transform: none; }
}
