/* Room Designer.
   Mobile first: the phone layout is the base and the desktop layout is the
   media query, because the phone is where this game is actually played.

   Layering (documented because it is load-bearing, exactly as in
   web/koloss/touch.css):
       0    canvas
       20   catalogue sheet, panels, toolbars
       30   modal
       40   toast
       200  audio controls -- ALWAYS on top, so no overlay can swallow a tap
             on the mute button. */

/* The bottom edge is shared by four independently-positioned bars. They are
   stacked into fixed, non-overlapping bands rather than relying on them missing
   each other horizontally -- which they did not, at every phone and tablet size.

        catalogue sheet handle   0  .. 52
        audio strip             60  .. 104
        selection toolbar      112  .. 166   (also the walk pad)
        turn-the-room button   174  .. 220
        toast                  230  ..
*/
:root {
    --rd-band-audio: 60px;
    --rd-band-bar: 112px;
    --rd-band-turn: 174px;
    --rd-band-toast: 232px;

    --rd-bg: #f4efe7;
    --rd-panel: #fffdf9;
    --rd-ink: #33302b;
    --rd-ink-soft: #7a7268;
    --rd-line: #e2dbcf;
    --rd-accent: #2f9e8f;
    --rd-accent-dark: #1e7368;
    --rd-danger: #d9534f;
    --rd-shadow: 0 6px 24px rgba(60, 48, 32, .16);
    --rd-radius: 14px;
    --rd-safe-b: env(safe-area-inset-bottom, 0px);
    --rd-safe-t: env(safe-area-inset-top, 0px);
}

* { box-sizing: border-box; }

html, body {
    margin: 0;
    padding: 0;
    height: 100%;
    /* dvh, not vh: mobile browser chrome slides in and out and vh does not
       follow it, which leaves the catalogue sheet cut off. */
    min-height: 100dvh;
    overflow: hidden;
    background: var(--rd-bg);
    color: var(--rd-ink);
    font-family: "Segoe UI", system-ui, -apple-system, Roboto, sans-serif;
    overscroll-behavior: none;
    -webkit-tap-highlight-color: transparent;
}

#scene {
    position: fixed;
    inset: 0;
    width: 100%;
    height: 100%;
    display: block;
    touch-action: none;          /* the canvas owns pinch/pan itself */
    user-select: none;
}

.rd-hidden { display: none !important; }

/* ── Loading ──────────────────────────────────────────────────────────── */
.rd-overlay {
    position: fixed;
    inset: 0;
    z-index: 300;
    display: grid;
    place-items: center;
    background: var(--rd-bg);
    transition: opacity .4s ease;
}
.rd-overlay.rd-gone { opacity: 0; pointer-events: none; }
.rd-loading-card { text-align: center; padding: 24px; max-width: 320px; }
.rd-logo { font-size: 56px; }
.rd-loading-card h1 { margin: 6px 0 4px; font-size: 1.5rem; letter-spacing: .01em; }
.rd-loading-card p { margin: 0 0 16px; color: var(--rd-ink-soft); font-size: .9rem; }
.rd-bar { height: 6px; border-radius: 3px; background: var(--rd-line); overflow: hidden; }
.rd-bar-fill { height: 100%; width: 8%; background: var(--rd-accent); transition: width .35s ease; }

/* ── Buttons ──────────────────────────────────────────────────────────── */
.rd-icon-btn, .rd-btn {
    font: inherit;
    border: 1px solid var(--rd-line);
    background: var(--rd-panel);
    color: var(--rd-ink);
    border-radius: 10px;
    cursor: pointer;
    /* 44px is the smallest reliably tappable target on a phone. */
    min-height: 44px;
    min-width: 44px;
    padding: 0 10px;
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: 6px;
    transition: background .12s, border-color .12s, transform .06s;
}
.rd-icon-btn:active, .rd-btn:active { transform: scale(.96); }
.rd-icon-btn:hover:not(:disabled), .rd-btn:hover:not(:disabled) { border-color: var(--rd-accent); }
.rd-icon-btn:disabled, .rd-btn:disabled { opacity: .4; cursor: default; }
.rd-btn { background: var(--rd-accent); border-color: var(--rd-accent); color: #fff; padding: 0 16px; font-weight: 600; }
.rd-btn-ghost { background: var(--rd-panel); color: var(--rd-ink); border-color: var(--rd-line); }
.rd-btn-small { min-height: 34px; min-width: 34px; padding: 0 10px; font-size: .82rem; }
.rd-danger { color: var(--rd-danger); }
.rd-danger:hover { border-color: var(--rd-danger) !important; }
.rd-view-btn { font-weight: 600; min-width: 58px; }

/* ── Top bar ──────────────────────────────────────────────────────────── */
#topbar {
    position: fixed;
    top: 0; left: 0; right: 0;
    /* Above the catalogue rail (20): on desktop the rail is full-height and
       would otherwise swallow the right-hand half of this bar. The rail
       reserves blank padding at its own top for exactly this overlap. */
    z-index: 22;
    display: flex;
    align-items: center;
    gap: 6px;
    padding: calc(6px + var(--rd-safe-t)) 8px 6px;
    background: linear-gradient(#f4efe7ee, #f4efe7cc);
    backdrop-filter: blur(6px);
    border-bottom: 1px solid var(--rd-line);
}
.rd-title-wrap { display: flex; flex-direction: column; min-width: 0; flex: 1 1 auto; }
.rd-name {
    font: 600 1rem/1.2 inherit;
    color: var(--rd-ink);
    background: transparent;
    border: 1px solid transparent;
    border-radius: 8px;
    padding: 3px 6px;
    width: 100%;
    max-width: 260px;
    min-width: 0;
}
.rd-name:hover { border-color: var(--rd-line); }
.rd-name:focus { border-color: var(--rd-accent); outline: none; background: #fff; }
.rd-status {
    font-size: .7rem;
    color: var(--rd-ink-soft);
    padding-left: 7px;
    white-space: nowrap;         /* "Not saved" must not wrap the bar to two lines */
    overflow: hidden;
    text-overflow: ellipsis;
}
.rd-status-saving { color: var(--rd-accent-dark); }
.rd-status-error { color: var(--rd-danger); }
.rd-status-guest, .rd-status-dirty { color: #b7791f; }
.rd-spacer { flex: 0 1 12px; }

/* ── Guest banner ─────────────────────────────────────────────────────── */
.rd-banner {
    position: fixed;
    top: calc(58px + var(--rd-safe-t));
    left: 8px; right: 8px;
    z-index: 20;
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 8px 10px;
    font-size: .82rem;
    background: #fff6dd;
    border: 1px solid #e8d9a8;
    border-radius: var(--rd-radius);
    box-shadow: var(--rd-shadow);
}
.rd-banner span { flex: 1 1 auto; }

/* ── Selection toolbar ────────────────────────────────────────────────── */
.rd-selbar {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    bottom: calc(var(--rd-band-bar) + var(--rd-safe-b));
    z-index: 20;
    display: flex;
    align-items: center;
    gap: 4px;
    padding: 5px 6px;
    background: var(--rd-panel);
    border: 1px solid var(--rd-line);
    border-radius: 999px;
    box-shadow: var(--rd-shadow);
    max-width: calc(100vw - 16px);
}
/* Size control in the selection bar.
   The bar is a pill on a phone and already close to the screen width, so the
   slider is deliberately short and the percentage sits in a fixed-width slot --
   a readout that changes width as you drag makes the whole bar jitter. */
.rd-size {
    display: flex;
    align-items: center;
    gap: 5px;
    padding: 0 6px;
    border-left: 1px solid var(--rd-line);
    border-right: 1px solid var(--rd-line);
}
.rd-size-icon { font-size: .85rem; opacity: .55; }
.rd-size input[type="range"] {
    width: 88px;
    accent-color: var(--rd-accent, #2f9e8f);
    /* The shared web/style.css pins .volume-slider and friends; this one is
       ours, but the same trap applies -- state the layout rather than inherit. */
    margin: 0;
    vertical-align: middle;
}
.rd-size-out {
    font-size: .7rem;
    font-variant-numeric: tabular-nums;
    width: 3.2em;
    text-align: right;
    opacity: .75;
}
@media (max-width: 560px) {
    .rd-size input[type="range"] { width: 58px; }
    .rd-size-out { display: none; }
}

.rd-sel-name {
    font-size: .8rem;
    font-weight: 600;
    padding: 0 8px 0 10px;
    max-width: 34vw;
    white-space: nowrap;
    overflow: hidden;
    text-overflow: ellipsis;
}

/* ── Catalogue: bottom sheet on phones ────────────────────────────────── */
.rd-catalog {
    position: fixed;
    left: 0; right: 0; bottom: 0;
    z-index: 20;
    background: var(--rd-panel);
    border-top: 1px solid var(--rd-line);
    border-radius: var(--rd-radius) var(--rd-radius) 0 0;
    box-shadow: var(--rd-shadow);
    display: flex;
    flex-direction: column;
    max-height: 64dvh;
    transform: translateY(calc(100% - 52px - var(--rd-safe-b)));
    transition: transform .22s ease;
    padding-bottom: var(--rd-safe-b);
}
.rd-catalog.rd-cat-open { transform: translateY(0); }

.rd-cat-handle {
    all: unset;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 8px 0 6px;
    cursor: pointer;
    font-size: .82rem;
    font-weight: 600;
    color: var(--rd-ink-soft);
    flex: 0 0 auto;
    min-height: 44px;
}
.rd-grip { width: 40px; height: 4px; border-radius: 2px; background: var(--rd-line); }

/* Always inside the panel, top-right, above its content. The grab handle is a
   convenience; this is the guaranteed way out. */
.rd-cat-close {
    position: absolute;
    top: 6px;
    right: 8px;
    z-index: 2;
    min-width: 38px;
    min-height: 38px;
    padding: 0;
    font-size: 1.1rem;
    line-height: 1;
    background: var(--rd-panel);
}
.rd-catalog:not(.rd-cat-open) .rd-cat-close { display: none; }

.rd-cat-body { overflow: hidden; display: flex; flex-direction: column; min-height: 0; }

/* ── Only the scroll area may give up height ──────────────────────────────
   Every sheet here is a flex COLUMN (.rd-catalog, .rd-panel, .rd-modal-card),
   and a flex item defaults to flex-shrink:1. So when the sheet is capped --
   64dvh, and less than that on a short window -- the browser takes the missing
   height out of EVERY child in proportion, not out of the one that scrolls.

   The category strip was the visible casualty: `overflow-x:auto` makes its
   automatic minimum size 0, so unlike the search box it had nothing holding it
   open, and it was squeezed from 38 px to 11-27 px at every viewport except a
   tall desktop -- a sliver showing its own scrollbar and no readable chip, with
   no way back to another category.

   The rule is one line per element and it is the whole fix: chrome is
   `flex: 0 0 auto`, the scroller keeps `min-height: 0` and is the only thing
   that shrinks. Anything added to one of these sheets later needs the same. */
.rd-cat-handle,
.rd-cat-search,
.rd-style-toggle,
.rd-cat-tabs,
.rd-panel-head,
.rd-panel-foot {
    flex: 0 0 auto;
}

/* Keep the search field clear of the close button. */
.rd-cat-search { padding: 0 54px 8px 10px; }
.rd-cat-search input {
    width: 100%;
    font: inherit;
    padding: 9px 12px;
    min-height: 42px;
    border: 1px solid var(--rd-line);
    border-radius: 10px;
    background: #fff;
    color: var(--rd-ink);
}
.rd-cat-search input:focus { outline: none; border-color: var(--rd-accent); }

.rd-cat-tabs {
    display: flex;
    gap: 6px;
    overflow-x: auto;
    padding: 0 10px 8px;
    scrollbar-width: thin;
    -webkit-overflow-scrolling: touch;
}
.rd-cat-tab {
    flex: 0 0 auto;
    font: inherit;
    font-size: .78rem;
    display: inline-flex;
    align-items: center;
    gap: 5px;
    padding: 7px 11px;
    min-height: 38px;
    border: 1px solid var(--rd-line);
    border-radius: 999px;
    background: #fff;
    color: var(--rd-ink-soft);
    cursor: pointer;
    white-space: nowrap;
}
.rd-cat-tab.rd-on { background: var(--rd-accent); border-color: var(--rd-accent); color: #fff; }
.rd-cat-icon { font-size: 1rem; }

.rd-cat-grid {
    overflow-y: auto;
    -webkit-overflow-scrolling: touch;
    overscroll-behavior: contain;
    display: grid;
    grid-template-columns: repeat(auto-fill, minmax(84px, 1fr));
    gap: 8px;
    padding: 0 10px 12px;
    /* The audio strip floats above everything (z-index 200) and sits over the
       bottom of the sheet. This is a MARGIN, not padding: padding only lifts
       the cards once the grid is scrolled to the end, whereas a margin shrinks
       the grid's box so no card is ever laid out under the strip at all. */
    /* `band-bar`, not `band-audio`: the reserve has to clear the strip's own
       height as well as its offset from the bottom, and that is exactly what
       the next band up is defined as. */
    margin-bottom: calc(var(--rd-band-bar) + 4px);
    min-height: 0;
}
.rd-cat-card {
    font: inherit;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 6px 4px;
    background: #fff;
    border: 1px solid var(--rd-line);
    border-radius: 12px;
    cursor: pointer;
    transition: border-color .12s, transform .06s;
}
.rd-cat-card:active { transform: scale(.95); }
.rd-cat-card:hover { border-color: var(--rd-accent); }
/* The thumbnails are transparent WebP now, so a white vase on a white card was
   an empty square. A faint tinted plate behind the image gives every object
   something to sit against without turning the grid into a set of boxes. */
.rd-cat-card img {
    width: 100%;
    aspect-ratio: 1;
    object-fit: contain;
    background: linear-gradient(#f4f6f8, #eaeef1);
    border-radius: 8px;
}
.rd-cat-card span {
    font-size: .66rem;
    color: var(--rd-ink-soft);
    text-align: center;
    line-height: 1.15;
    display: -webkit-box;
    -webkit-line-clamp: 2;
    -webkit-box-orient: vertical;
    overflow: hidden;
}
.rd-empty { color: var(--rd-ink-soft); font-size: .85rem; padding: 12px 4px; grid-column: 1 / -1; }

/* ── Panels ───────────────────────────────────────────────────────────── */
.rd-panel {
    position: fixed;
    z-index: 21;
    left: 8px; right: 8px;
    bottom: calc(60px + var(--rd-safe-b));
    max-height: 62dvh;
    display: flex;
    flex-direction: column;
    background: var(--rd-panel);
    border: 1px solid var(--rd-line);
    border-radius: var(--rd-radius);
    box-shadow: var(--rd-shadow);
}
.rd-panel-head {
    display: flex;
    align-items: center;
    justify-content: space-between;
    gap: 8px;
    padding: 8px 8px 8px 14px;
    border-bottom: 1px solid var(--rd-line);
}
.rd-panel-body {
    overflow-y: auto;
    overscroll-behavior: contain;
    padding: 12px 14px;
    min-height: 0;
    /* Same reserve as the catalogue grid: the audio strip floats over the
       bottom of the panel, and anything laid out underneath it cannot be
       tapped. See the band table at the top of this file. */
    margin-bottom: calc(var(--rd-band-bar) + 4px);
}
.rd-panel-foot { padding: 8px 14px 12px; border-top: 1px solid var(--rd-line); }
.rd-panel-body h4 { margin: 16px 0 8px; font-size: .8rem; text-transform: uppercase; letter-spacing: .05em; color: var(--rd-ink-soft); }
.rd-panel-body h4:first-child { margin-top: 0; }
.rd-hint { font-size: .76rem; color: var(--rd-ink-soft); margin: -4px 0 8px; }

.rd-field { display: block; margin-bottom: 12px; font-size: .84rem; }
.rd-field span { display: block; margin-bottom: 4px; color: var(--rd-ink-soft); }
.rd-field input[type=range] { width: 100%; accent-color: var(--rd-accent); height: 30px; }

.rd-slot { margin-bottom: 14px; }
.rd-slot-head { font-size: .78rem; font-weight: 600; color: var(--rd-ink-soft); margin-bottom: 6px; }

.rd-style-toggle {
    display: flex;
    gap: 2px;
    margin: 0 10px 8px;
    padding: 2px;
    background: #0000000d;
    border-radius: 10px;
}
.rd-style-toggle > button {
    all: unset;
    flex: 1;
    text-align: center;
    padding: 7px 4px;
    border-radius: 8px;
    font-size: 12px;
    font-weight: 600;
    color: #5a6570;
    cursor: pointer;
}
.rd-style-toggle > button.rd-on { background: #fff; color: var(--rd-accent); box-shadow: 0 1px 3px #0000001f; }

/* Material chips show the real texture, so they need room for the sample to be
   recognisable as oak or brick -- a 34px swatch is too small to read a grain. */
.rd-materials { display: grid; grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); gap: 8px; }
.rd-material {
    all: unset;
    aspect-ratio: 1;
    border-radius: 10px;
    border: 2px solid #0000001a;
    cursor: pointer;
    box-sizing: border-box;
    background-size: cover;
    background-position: center;
    position: relative;
    overflow: hidden;
    display: flex;
    align-items: flex-end;
}
.rd-material > span {
    width: 100%;
    padding: 3px 5px;
    font-size: 10px;
    line-height: 1.2;
    color: #fff;
    /* The label sits on a photograph, so it carries its own scrim -- plain
       white text vanishes over marble and plain dark text over walnut. */
    background: linear-gradient(to top, #000000b0, #00000000);
    text-align: center;
}
.rd-material.rd-on { border-color: var(--rd-accent); box-shadow: 0 0 0 2px #2f9e8f55; }

/* Moulding profiles. Words, not thumbnails: a 72 px picture of a cornice from
   the front is a white line, and the names are ones people already know. */
.rd-trims { display: flex; flex-wrap: wrap; gap: 8px; }
.rd-trim {
    all: unset;
    padding: 8px 12px;
    min-height: 28px;
    border-radius: 10px;
    border: 2px solid #0000001a;
    background: #fff;
    font-size: .8rem;
    color: var(--rd-ink);
    cursor: pointer;
    box-sizing: border-box;
}
.rd-trim.rd-on { border-color: var(--rd-accent); box-shadow: 0 0 0 2px #2f9e8f55; }

.rd-swatches { display: grid; grid-template-columns: repeat(auto-fill, minmax(34px, 1fr)); gap: 6px; }
.rd-swatch {
    all: unset;
    aspect-ratio: 1;
    border-radius: 8px;
    border: 2px solid #0000001a;
    cursor: pointer;
    box-sizing: border-box;
    min-height: 34px;
}
.rd-swatch.rd-on { border-color: var(--rd-accent); box-shadow: 0 0 0 2px #2f9e8f55; }
.rd-swatch-custom {
    display: grid;
    place-items: center;
    background: conic-gradient(#f66, #fc6, #6f6, #6cf, #96f, #f66);
    overflow: hidden;
}
.rd-swatch-custom input { opacity: 0; width: 100%; height: 100%; cursor: pointer; }

/* ── Build mode ───────────────────────────────────────────────────────── */
.rd-tools {
    display: grid;
    grid-template-columns: repeat(auto-fit, minmax(64px, 1fr));
    gap: 6px;
    margin-bottom: 8px;
}
.rd-tool {
    font: inherit;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 3px;
    padding: 8px 4px;
    min-height: 56px;
    font-size: .7rem;
    background: #fff;
    border: 1px solid var(--rd-line);
    border-radius: 11px;
    color: var(--rd-ink-soft);
    cursor: pointer;
}
.rd-tool:hover { border-color: var(--rd-accent); }
.rd-tool.rd-on {
    background: var(--rd-accent);
    border-color: var(--rd-accent);
    color: #fff;
}
.rd-tool-icon { font-size: 1.15rem; line-height: 1; }

/* The build button in the top bar lights up while the mode is on, because the
   tool panel can be scrolled out of sight on a phone. */
#buildBtn.rd-on {
    background: var(--rd-accent);
    border-color: var(--rd-accent);
    color: #fff;
}

.rd-room-list { display: flex; flex-direction: column; gap: 6px; margin-bottom: 8px; }
.rd-room-chip {
    display: flex;
    align-items: center;
    gap: 8px;
    padding: 5px 8px;
    background: #fff;
    border: 1px solid var(--rd-line);
    border-radius: 11px;
    cursor: pointer;
    min-height: 44px;
}
.rd-room-chip.rd-on { border-color: var(--rd-accent); box-shadow: 0 0 0 2px #2f9e8f33; }

/* Which storey you are on. One row, because a house is three floors at most. */
.rd-levels { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 10px; }
.rd-level {
    font: inherit;
    font-size: .8rem;
    min-height: 38px;
    padding: 6px 12px;
    border: 1px solid var(--rd-line);
    border-radius: 999px;
    background: #fff;
    color: var(--rd-ink-soft);
    cursor: pointer;
    white-space: nowrap;
}
.rd-level.rd-on { background: var(--rd-accent); border-color: var(--rd-accent); color: #fff; }
.rd-level-add { border-style: dashed; }
.rd-level-del { min-width: 38px; }

/* Indoors / outside, on the room chip. Both states are drawn -- a control that
   only appears once it is on cannot be discovered. */
.rd-room-out {
    font: inherit;
    font-size: 1rem;
    line-height: 1;
    min-width: 34px;
    min-height: 34px;
    padding: 0;
    border: 1px solid var(--rd-line);
    border-radius: 9px;
    background: #fff;
    color: var(--rd-ink-soft);
    cursor: pointer;
}
.rd-room-out.rd-on { background: #eaf6ec; border-color: #74b57e; color: #2f7d3c; }
.rd-room-dot {
    width: 18px; height: 18px;
    border-radius: 5px;
    border: 1px solid #0000001a;
    flex: 0 0 auto;
}
.rd-room-name {
    flex: 1 1 auto;
    min-width: 0;
    font: inherit;
    font-size: .86rem;
    padding: 4px 6px;
    background: transparent;
    border: 1px solid transparent;
    border-radius: 7px;
    color: var(--rd-ink);
}
.rd-room-name:hover { border-color: var(--rd-line); }
.rd-room-name:focus { outline: none; border-color: var(--rd-accent); background: #fff; }
.rd-room-count {
    font-size: .72rem;
    color: var(--rd-ink-soft);
    padding: 0 2px;
    flex: 0 0 auto;
}

/* ── Door / window style strips ───────────────────────────────────────── */
.rd-styles { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 4px; }
.rd-style {
    font: inherit;
    font-size: .76rem;
    min-height: 34px;
    padding: 4px 10px;
    background: #fff;
    border: 1px solid var(--rd-line);
    border-radius: 999px;
    color: var(--rd-ink);
    cursor: pointer;
}
.rd-style:hover { border-color: var(--rd-accent); }
.rd-style.rd-on { background: var(--rd-accent); border-color: var(--rd-accent); color: #fff; }

/* Until a wall has been tapped there is nothing for these controls to act on;
   greyed rather than hidden, so it is clear they are waiting for a tap. */
#faceSection.rd-face-idle .rd-materials,
#faceSection.rd-face-idle .rd-swatches { opacity: .45; }

/* While building, the furniture catalogue is not what you want in the way. */
body.rd-building .rd-catalog { display: none; }
body.rd-building .rd-selbar { display: none; }

.rd-presets { display: grid; grid-template-columns: repeat(auto-fill, minmax(88px, 1fr)); gap: 8px; }
.rd-preset {
    font: inherit;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    padding: 10px 4px;
    font-size: .72rem;
    background: #fff;
    border: 1px solid var(--rd-line);
    border-radius: 12px;
    cursor: pointer;
    min-height: 62px;
}
.rd-preset:hover { border-color: var(--rd-accent); }
.rd-preset span:first-child { font-size: 1.3rem; }

/* ── Modal ────────────────────────────────────────────────────────────── */
.rd-modal {
    position: fixed;
    inset: 0;
    z-index: 30;
    display: grid;
    place-items: center;
    background: #33302b66;
    padding: 12px;
    padding-bottom: calc(12px + var(--rd-safe-b));
}
.rd-modal-card {
    width: min(760px, 100%);
    max-height: 86dvh;
    display: flex;
    flex-direction: column;
    background: var(--rd-panel);
    border-radius: var(--rd-radius);
    box-shadow: var(--rd-shadow);
    overflow: hidden;
}
.rd-tabs { display: flex; gap: 4px; }
.rd-tab {
    font: inherit;
    font-size: .86rem;
    font-weight: 600;
    padding: 8px 14px;
    min-height: 40px;
    border: none;
    background: transparent;
    color: var(--rd-ink-soft);
    border-radius: 9px;
    cursor: pointer;
}
.rd-tab-on { background: var(--rd-accent); color: #fff; }
.rd-row { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 14px; }

.rd-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px; }
.rd-card {
    border: 1px solid var(--rd-line);
    border-radius: 12px;
    overflow: hidden;
    background: #fff;
    display: flex;
    flex-direction: column;
}
.rd-card-current { border-color: var(--rd-accent); box-shadow: 0 0 0 2px #2f9e8f33; }
.rd-card-thumb {
    all: unset;
    display: block;
    aspect-ratio: 16 / 10;
    background: #ece5d8 center/cover no-repeat;
    cursor: pointer;
}
.rd-card-meta { padding: 8px 10px 4px; display: flex; flex-direction: column; gap: 2px; min-width: 0; }
.rd-card-meta strong { font-size: .86rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.rd-card-by { font-size: .72rem; color: var(--rd-ink-soft); }
.rd-card-actions { display: flex; justify-content: flex-end; gap: 4px; padding: 4px 6px 6px; }
.rd-card-actions .rd-icon-btn { min-height: 36px; min-width: 36px; font-size: .8rem; border: none; }

.rd-share { display: flex; gap: 6px; margin-bottom: 14px; }
.rd-share input {
    flex: 1 1 auto;
    font: inherit;
    font-size: .8rem;
    padding: 8px 10px;
    border: 1px solid var(--rd-line);
    border-radius: 9px;
    background: #fff;
    color: var(--rd-ink);
    min-width: 0;
}

/* ── Turn-the-room button ─────────────────────────────────────────────── */
.rd-rotate-view {
    position: fixed;
    left: 10px;
    bottom: calc(var(--rd-band-turn) + var(--rd-safe-b));
    z-index: 20;
    width: 46px;
    height: 46px;
    font-size: 1.2rem;
    border-radius: 50%;
    border: 1px solid var(--rd-line);
    background: #fffdf9e6;
    color: var(--rd-ink);
    cursor: pointer;
    box-shadow: var(--rd-shadow);
}
.rd-rotate-view:active { background: var(--rd-accent); color: #fff; }
body.rd-walking .rd-rotate-view { display: none; }
/* With the sheet up there is no room left to turn, and the button would sit on
   top of the catalogue's cards. */
.rd-catalog.rd-cat-open ~ .rd-rotate-view { display: none; }

/* ── Walk pad ─────────────────────────────────────────────────────────── */
.rd-walkpad {
    position: fixed;
    left: 12px;
    bottom: calc(var(--rd-band-bar) + var(--rd-safe-b));
    /* ABOVE the panels (21). The build panel is anchored to the same bottom
       edge and covered the arrows completely, so on a phone walk mode looked
       like it simply did not work. */
    z-index: 26;
    display: flex;
    flex-direction: column;
    align-items: center;
    gap: 4px;
    touch-action: none;
}
.rd-walkpad-row { display: flex; gap: 4px; }
.rd-walkpad button {
    font: inherit;
    width: 52px;
    height: 46px;
    border-radius: 12px;
    border: 1px solid var(--rd-line);
    background: #fffdf9dd;
    color: var(--rd-ink);
    cursor: pointer;
    touch-action: none;
}
.rd-walkpad button:active,
.rd-walkpad button.rd-on { background: var(--rd-accent); color: #fff; }
.rd-walkpad button { font-size: 1.05rem; }

/* ── Walk mode owns its own bottom bands ──────────────────────────────────
   The movement pad and the selection toolbar were BOTH on `band-bar` (112).
   The toolbar is centred and, on a phone, nearly the full width of the screen,
   so it landed straight on the arrows -- and its rotate buttons are the first
   thing your thumb reaches for. They get separate bands while walking:

        catalogue handle    0  ..  52
        movement pad       60  .. 158   (left)   / audio chip 60 (right)
        selection toolbar 178 .. 232   (centred)
        toast             248  ..
*/
body.rd-walking .rd-walkpad { bottom: calc(var(--rd-band-audio) + var(--rd-safe-b)); }
body.rd-walking .rd-selbar { bottom: calc(178px + var(--rd-safe-b)); }
body.rd-walking .rd-toast { bottom: calc(248px + var(--rd-safe-b)); }

/* With the build panel open the pad is above the panel (z 26) so the arrows
   stay tappable, but sitting ON its controls is not good enough. Move it up
   into the canvas strip the panel leaves free, and cap the panel so the two
   have somewhere each to be. */
body.rd-walking.rd-building .rd-walkpad {
    bottom: auto;
    top: calc(112px + var(--rd-safe-t));
}
body.rd-walking .rd-panel { max-height: 60dvh; }

/* Walking used to hide the catalogue and the selection bar, which is what made
   the 3D view a viewer rather than a place you can furnish. Both stay now; what
   gets out of the way is whichever of them is standing on the movement pad. */
.rd-catalog.rd-cat-open ~ .rd-walkpad { display: none; }

/* A shared room is someone else's -- hide everything that would edit it. */
body.rd-readonly .rd-catalog,
body.rd-readonly .rd-selbar,
body.rd-readonly #roomBtn,
body.rd-readonly #undoBtn,
body.rd-readonly #redoBtn,
body.rd-readonly .rd-status { display: none; }

/* ── Toast ────────────────────────────────────────────────────────────── */
.rd-toast {
    position: fixed;
    left: 50%;
    transform: translateX(-50%);
    bottom: calc(var(--rd-band-toast) + var(--rd-safe-b));
    z-index: 40;
    padding: 10px 16px;
    font-size: .84rem;
    background: #33302bee;
    color: #fff;
    border-radius: 999px;
    box-shadow: var(--rd-shadow);
    max-width: calc(100vw - 24px);
    text-align: center;
}

/* ── Audio controls: the site-wide strip ──────────────────────────────── */
.audio-controls {
    position: fixed;
    right: 8px;
    bottom: calc(var(--rd-band-audio) + var(--rd-safe-b));
    z-index: 200;                /* above every overlay -- see the note at top */
    display: flex;
    align-items: center;
    gap: 5px;
    padding: 5px 8px;
    font-size: .74rem;
    background: #fffdf9e6;
    border: 1px solid var(--rd-line);
    border-radius: 999px;
    box-shadow: var(--rd-shadow);
}
.audio-controls button {
    font: inherit;
    background: none;
    border: 1px solid var(--rd-line);
    color: var(--rd-ink);
    border-radius: 8px;
    padding: 5px 8px;
    min-height: 32px;
    cursor: pointer;
    white-space: nowrap;
}
.audio-controls button:hover { border-color: var(--rd-accent); }

/* Collapsed to one chip until it is asked for. The strip is z-index 200 and
   pinned over the bottom-right corner, so left open it permanently covers
   whatever button is underneath it on a phone. */
.rd-audio-toggle { font-size: 1rem; padding: 5px 9px !important; }
.audio-controls:not(.rd-audio-open) > :not(.rd-audio-toggle) { display: none; }
.audio-controls.rd-audio-open .rd-audio-toggle { color: var(--rd-accent); }
/* The shared web/style.css pins `.volume-slider` to `position: fixed; top: 12px;
   z-index: 1000` -- a leftover from the original Snake layout that teleports
   both sliders out of this strip and onto the top bar. Undo it here rather than
   editing the shared sheet, which eleven other games rely on. */
.volume-slider {
    position: static;
    top: auto;
    z-index: auto;
    width: 56px;
    /* The track is drawn thin, but the hit area must still be a real target --
       a 16px-tall slider is not draggable with a thumb. */
    height: 34px;
    margin: 0;
    accent-color: var(--rd-accent);
    cursor: pointer;
}

/* ── If the page gets zoomed anyway ───────────────────────────────────────
   js/viewport.js blocks browser zoom, but a browser with forced zoom on (or an
   accessibility setting) can still do it. Everything here is `position: fixed`,
   which pins it to the LAYOUT viewport -- the part of the page a zoomed screen
   has stopped showing, which is exactly how the side panels "disappeared".

   These two properties slide the chrome back into the visible region and scale
   it to fit across it. `translate:` and `scale:` are separate properties from
   `transform:`, so they compose with the transforms the sheets already use to
   slide in and out rather than fighting them. The origin is set per element to
   whichever corner it is anchored to, or scaling would push it off the other
   way. */
body.rd-vv-zoomed #topbar,
body.rd-vv-zoomed .rd-banner,
body.rd-vv-zoomed .rd-selbar,
body.rd-vv-zoomed .rd-catalog,
body.rd-vv-zoomed .rd-panel,
body.rd-vv-zoomed .rd-modal,
body.rd-vv-zoomed .rd-rotate-view,
body.rd-vv-zoomed .rd-walkpad,
body.rd-vv-zoomed .rd-toast,
body.rd-vv-zoomed .audio-controls {
    translate: var(--rd-vv-dx, 0px) var(--rd-vv-dy, 0px);
    scale: var(--rd-vv-s, 1);
}
body.rd-vv-zoomed #topbar,
body.rd-vv-zoomed .rd-banner { transform-origin: left top; }
body.rd-vv-zoomed .rd-catalog,
body.rd-vv-zoomed .rd-panel,
body.rd-vv-zoomed .rd-walkpad,
body.rd-vv-zoomed .rd-rotate-view { transform-origin: left bottom; }
body.rd-vv-zoomed .audio-controls { transform-origin: right bottom; }
body.rd-vv-zoomed .rd-selbar,
body.rd-vv-zoomed .rd-toast { transform-origin: center bottom; }
body.rd-vv-zoomed .rd-modal { transform-origin: left top; }

/* The way back from an accidental page zoom. Hidden unless the page really is
   zoomed, pinned to the TOP of the visible viewport (not the layout viewport,
   which is the part that has scrolled away), and above everything -- it is the
   one control that must be reachable when nothing else is. */
.rd-zoom-reset {
    position: fixed;
    top: 0;
    left: 50%;
    z-index: 400;
    display: none;
    font: inherit;
    font-size: .82rem;
    font-weight: 600;
    padding: 8px 14px;
    min-height: 40px;
    color: #fff;
    background: var(--rd-accent-dark);
    border: none;
    border-radius: 0 0 12px 12px;
    box-shadow: var(--rd-shadow);
    cursor: pointer;
}
body.rd-vv-zoomed .rd-zoom-reset {
    display: block;
    translate: calc(var(--rd-vv-dx, 0px) - 50%) var(--rd-vv-dy, 0px);
    scale: var(--rd-vv-s, 1);
    transform-origin: center top;
}

/* ── Phones ───────────────────────────────────────────────────────────── */
@media (max-width: 640px) {
    .audio-controls { font-size: .68rem; gap: 3px; padding: 4px 6px; right: 6px; }
    .volume-slider { width: 40px; }
    .rd-cat-grid { grid-template-columns: repeat(auto-fill, minmax(72px, 1fr)); }

    /* Seven controls plus a title do not fit across a 390px phone at desktop
       sizing. Shrink the buttons to the 40px touch floor, drop the gaps, and
       let the name shrink before anything gets pushed off the edge. */
    #topbar { gap: 3px; padding-left: 5px; padding-right: 5px; }
    #topbar .rd-icon-btn { min-width: 40px; min-height: 40px; padding: 0 4px; }
    .rd-view-btn { min-width: 46px; font-size: .78rem; }
    .rd-name { font-size: .84rem; padding: 2px 4px; }
    .rd-title-wrap { flex: 1 1 0; min-width: 44px; }
    .rd-spacer { display: none; }

    .rd-banner { font-size: .76rem; padding: 7px 8px; gap: 6px; top: calc(52px + var(--rd-safe-t)); }
    .rd-banner .rd-btn-small { min-height: 32px; padding: 0 8px; }
}

/* Narrow phones: the room name is the first thing that can go. */
@media (max-width: 400px) {
    .rd-name { font-size: .78rem; }
    #topbar .rd-icon-btn { min-width: 38px; padding: 0 2px; }
}

/* ── Short windows: give the height back to the GRID ──────────────────────
   Now that the chrome no longer collapses, it is a fixed cost: handle 52 +
   search 50 + style toggle 40 + category strip 46 = ~188 px, on top of the
   ~116 px the grid reserves so no card is laid out under the audio strip.
   Against a 64dvh sheet on a 600 px window that leaves ~80 px of grid -- one
   row of cards. So the sheet gets taller as the window gets shorter, and the
   chrome gets tighter, which is what actually buys rows.

   Measured with scratch probe_tabs.py; the target is two card rows on a phone
   and never fewer than one. Nothing here may shrink the strip again. */
@media (max-height: 760px) {
    .rd-catalog { max-height: 78dvh; }
    .rd-panel { max-height: 76dvh; }
}

@media (max-height: 560px) {
    .rd-catalog { max-height: 88dvh; }
    .rd-panel { max-height: 86dvh; }

    /* Trim the chrome, not the strip: every px here becomes grid. */
    .rd-cat-handle { min-height: 42px; }
    .rd-cat-search { padding-bottom: 5px; }
    .rd-cat-search input { min-height: 36px; padding: 6px 10px; }
    .rd-style-toggle { margin-bottom: 5px; }
    .rd-style-toggle > button { min-height: 32px; }
    .rd-cat-tabs { padding-bottom: 5px; }
    .rd-cat-tab { min-height: 32px; padding: 4px 9px; }
    .rd-cat-grid { grid-template-columns: repeat(auto-fill, minmax(68px, 1fr)); }
}

/* ── Desktop: catalogue becomes a right-hand rail ─────────────────────── */
@media (min-width: 900px) {
    .rd-catalog {
        left: auto;
        right: 0;
        top: 0;
        bottom: 0;
        width: 340px;
        max-height: none;
        border-radius: 0;
        border-top: none;
        border-left: 1px solid var(--rd-line);
        padding-top: calc(56px + var(--rd-safe-t));
        /* Fully off-screen when collapsed. The grab handle lives outside the
           panel (translated -100%), so leaving a sliver of the panel behind
           just exposes half a search box at the screen edge. */
        transform: translateX(100%);
    }
    .rd-catalog.rd-cat-open { transform: translateX(0); }
    .rd-cat-handle {
        position: absolute;
        left: 0; top: 50%;
        transform: translate(-100%, -50%);
        writing-mode: vertical-rl;
        background: var(--rd-panel);
        border: 1px solid var(--rd-line);
        border-right: none;
        border-radius: 10px 0 0 10px;
        padding: 14px 6px;
    }
    .rd-catalog.rd-cat-open .rd-cat-handle { transform: translate(-100%, -50%); }
    /* The rail runs the full height, so its top-right corner is behind the top
       bar (z-index 22). Drop the close button below it. */
    .rd-cat-close { top: calc(62px + var(--rd-safe-t)); }
    .rd-grip { width: 4px; height: 32px; }
    .rd-cat-body { flex: 1 1 auto; }
    .rd-cat-grid {
        grid-template-columns: repeat(auto-fill, minmax(88px, 1fr));
        /* The rail sits to the right of the audio strip here, not above it. */
        margin-bottom: 0;
    }

    .rd-panel { left: 12px; right: auto; width: 340px; bottom: 12px; max-height: calc(100dvh - 140px); }
    /* On desktop the panel sits left of the audio strip, so no reserve needed. */
    .rd-panel-body { margin-bottom: 0; }
    /* Same stacked bands as on mobile. The audio strip used to shift sideways
       depending on whether the rail was open, which made whether it collided
       with the selection toolbar depend on window width -- it did, at 900px. */
    .rd-audio-toggle { display: none; }
    .audio-controls > :not(.rd-audio-toggle) { display: revert; }
    .rd-selbar { bottom: 74px; left: 50%; }
    .audio-controls { bottom: 14px; right: 360px; }
    .rd-catalog:not(.rd-cat-open) ~ .audio-controls { right: 54px; }
    .rd-walkpad { bottom: 74px; }
    /* Wide enough that the centred toolbar and the left-hand pad never meet,
       so the walk-mode stacking above is not needed here. */
    body.rd-walking .rd-walkpad { bottom: 74px; }
    body.rd-walking .rd-selbar { bottom: 74px; }
    body.rd-walking .rd-toast { bottom: 190px; }
    .rd-rotate-view { bottom: 136px; }
    .rd-toast { bottom: 190px; }
}
