  /* Tokens = the legendhaven.com Onepage kit (read off the live site 2026-09-07): charcoal #202125,
     Instrument Sans body, Revalia display, white pill buttons, glass chips with a 1px inset ring.
     Layout geometry (sidebar 320, dock 64, bubble sizes) is still the spatial.chat measure.
     v115: accent was #E84548 (red); Munk called for the brand gold instead — see --gold below. */
  :root{
  /* v53. The scales this stylesheet was already using, written down.
     Names say what a value is FOR, never what colour the venue is: when the
     palette question is answered these do not have to change. Every number
     here is a value that was already in the file — nothing was converged and
     nothing moved, which is why the render fingerprint is unchanged. */
  /* Space: the 2px ladder under 482 margins, paddings and gaps. */
  --s-1: 2px;
  --s-2: 4px;
  --s-3: 6px;
  --s-4: 8px;
  --s-5: 10px;
  --s-6: 12px;
  --s-7: 14px;
  --s-8: 16px;
  --s-9: 18px;
  --s-10: 20px;
  /* Radius. --r and --pill already existed and still do. */
  --r-xs: 2px;
  --r-sm: 4px;
  --r-md: 6px;
  --r-lg: 8px;
  --r-xl: 10px;
  --r-2xl: 12px;
  --r-3xl: 14px;
  --r-4xl: 18px;
  /* How long. Three of these are the same idea at .14, .15 and .16 — see the
     note in PRODUCTION-PLAN §4: converging them is a decision, not a refactor. */
  --t-tap: .12s;
  --t-base: .16s;
  --t-settle: .18s;
  --t-ease: .2s;
  --t-panel: .22s;
  --t-travel: .3s;
  --t-scene: .45s;
  /* How it moves. Six other curves in this file are near-duplicates of these
     three and were deliberately left alone for the same reason. */
  --ease-press: cubic-bezier(.2,.8,.2,1);
  --ease-move: cubic-bezier(.2,.7,.3,1);
  --ease-arrive: cubic-bezier(.16,.84,.3,1);
  /* v54. Not duplicates of the three above: one runs forever and has no
     arrival, the other is the v33 slam and is a decision. */
  --ease-loop: cubic-bezier(.5,0,.5,1);
  --ease-slam: cubic-bezier(.4,.1,.2,1);
  /* v146. A slight overshoot — the left floor rail's + growing open "pops"
     rather than just sliding, distinct from --ease-arrive's own gentler
     settle (no overshoot at all). Nothing else in the file wants a bounce,
     which is why this earns its own token rather than reusing one of the
     six above. */
  --ease-pop: cubic-bezier(.34,1.56,.64,1);
  /* Alpha that is one idea used many times: hairlines over dark, and scrims. */
  --on-dark-08: rgba(255,255,255,.08);
  --on-dark-10: rgba(255,255,255,.1);
  --on-dark-14: rgba(255,255,255,.14);
  --on-dark-18: rgba(255,255,255,.18);
  /* v167. --on-dark-* is white-over-dark by name and by value — the drawer's
     own rule/rail-divider and its switch/slider tracks used it unconditionally,
     so in light mode they were white on white paper: a rule you could not
     see, a slider track that vanished, a switch with no visible off-state.
     These two flip with the theme (redefined in the light block below);
     everything else in the --on-dark family stays literal white-over-dark on
     purpose (rings and scrims meant for the floor itself, not this fix's
     job). */
  --rule-line: var(--on-dark-10);
  --ctrl-track: var(--on-dark-14);
  --shade-55: rgba(0,0,0,.55);
  --shade-60: rgba(0,0,0,.6);
  --shade-70: rgba(0,0,0,.7);
  /* white/black/ink primitives (theme-invariant) */
  --white: #FFFFFF; --black: #000000;
  /* on-dark: white at N% over dark chrome -- extends the original --on-dark-08/10/14/18 ladder */
  --on-dark-07: rgba(255,255,255,.07); --on-dark-09: rgba(255,255,255,.09); --on-dark-12: rgba(255,255,255,.12); --on-dark-13: rgba(255,255,255,.13); --on-dark-16: rgba(255,255,255,.16); --on-dark-22: rgba(255,255,255,.22); --on-dark-24: rgba(255,255,255,.24); --on-dark-35: rgba(255,255,255,.35); --on-dark-50: rgba(255,255,255,.50); --on-dark-55: rgba(255,255,255,.55); --on-dark-60: rgba(255,255,255,.60); --on-dark-85: rgba(255,255,255,.85); --on-dark-90: rgba(255,255,255,.90); --on-dark-96: rgba(255,255,255,.96);
  /* shade: black at N% -- extends the original --shade-55/60/70 ladder */
  --shade-14: rgba(0,0,0,.14); --shade-22: rgba(0,0,0,.22); --shade-28: rgba(0,0,0,.28); --shade-30: rgba(0,0,0,.30); --shade-35: rgba(0,0,0,.35); --shade-40: rgba(0,0,0,.40); --shade-45: rgba(0,0,0,.45); --shade-50: rgba(0,0,0,.50); --shade-53: rgba(0,0,0,.53); --shade-62: rgba(0,0,0,.62); --shade-72: rgba(0,0,0,.72); --shade-75: rgba(0,0,0,.75); --shade-80: rgba(0,0,0,.80); --shade-85: rgba(0,0,0,.85);
  /* ink alpha (the charcoal #202125, translucent) */
  --ink-a20: rgba(32,33,37,.20); --ink-a35: rgba(32,33,37,.35); --ink-a50: rgba(32,33,37,.50); --ink-a55: rgba(32,33,37,.55); --ink-a82: rgba(32,33,37,.82);
  /* void: near-black scrim/backdrop base #08090B, translucent */
  --void: #08090B; --void-a55: rgba(8,9,11,.55); --void-a66: rgba(8,9,11,.66); --void-a72: rgba(8,9,11,.72); --void-a80: rgba(8,9,11,.8);
  /* deep: near-black surface base #101114 (matches the reference branch's --deep), and its alpha steps */
  --deep: #101114; --deep-a72: rgba(16,17,20,.72); --deep-a82: rgba(16,17,20,.82); --deep-a84: rgba(16,17,20,.84);
  /* surface, translucent -- reuses the existing --surface:#17181B rather than a parallel value */
  --surface-a15: rgba(23,24,27,.15); --surface-a28: rgba(23,24,27,.28); --surface-a62: rgba(23,24,27,.62); --surface-a70: rgba(23,24,27,.70); --surface-a72: rgba(23,24,27,.72);
  /* named, single-role colours */
  --stage-bg: #2A2B30; --select-option-bg: #26272C; --dusk: #1C1A24; --note-ink: #22201A; --note-ink-strong: #151310; --mist: #F4F4F5; --ok-text: #6FCF97; --live: #3FBE6B; --live-soft: rgba(63,190,107,.25); --live-active: #3FB27F; --valid: #46B87A; --info-text: #A7A7FF; --info-a10: rgba(120,170,255,.10); --info-a60: rgba(120,170,255,.6); --alert-text: #FFAAAA; --mote-core: #FFF7D0; --mote-mid: #F1C65A; --mote-fade: rgba(241,198,90,0); --glow-mote-color: rgba(255,214,110,.9); --glow-impact-1: rgba(255,222,130,.85); --glow-impact-2: rgba(255,200,90,.5); --cook-core: #F6D37A; --glow-cook-1: rgba(246,211,122,.85); --glow-cook-2: rgba(246,160,60,.55); --chip-pill-bg: rgba(27,27,32,.9);
    /* v126. Munk: "solid white for the light, solid dark" — one surface colour
       per theme, not a stack of lighter greys for a sheet sitting on a panel.
       --surface is that one colour; every panel/sheet/modal/flyout/popover/
       card/pill fill below points at it instead of its own shade. */
    --surface:#17181B;
    --ink:#202125; --panel:var(--surface); --glass:var(--surface); --glass-solid:var(--surface); --ring:inset 0 0 0 1px rgba(255,255,255,.18);
    --fg:#FFFFFF; --fg-1:var(--fg); --fg-2:rgba(255,255,255,.82); --fg-3:rgba(255,255,255,.56); --fg-4:rgba(255,255,255,.56);
    --group:var(--surface); --hover:rgba(255,255,255,.08);
    --host-gold:#9B885B; --knight-green:#2D6A4F; --author-plum:#6D4C9F;   /* v57.2: --mod-blue retired with the rank */
    /* v115. Munk: "eliminate the use of red in this UI; use the brand gold."
       Sampled off public/brand/legendhaven-logo.png. --on-gold is near-black
       because white text on --gold measures ~2:1 — nowhere near readable —
       so anything solid in gold wears dark text, not white. --err is a new,
       separate hue: the one place red's JOB (a failed save has to look like
       failure) still needs a colour, and gold can't do double duty as both
       "this is the brand" and "this broke". */
    /* v136. Munk: "i want to control the venue colors ... it defaults to
       white in dark mode, and dark in light mode." --gold/--gold-deep/
       --gold-soft/--on-gold stay exactly as v115 left them — the Gold
       swatch's own colours, defined once, here. Everything accent-shaped
       below is generic now: --accent/--on-accent/--accent-deep/--accent-soft/
       --accent-rgb/--accent-icon/--accent-text resolve through a
       data-accent attribute selector on :root (and one scoped to light theme
       for the two text variants, below the light block), set from cfg.space.accent
       at boot and on change (applyAccent, venue.js). What's declared right
       here is the DEFAULT swatch, "Theme": not its own hue, it just points at
       --solid/--on-solid/--hi, which already flip between white-on-dark and
       dark-on-light — so Theme needs no light override of --accent/
       --on-accent/--accent-icon/--accent-text, only of --accent-rgb/
       --accent-deep/--accent-soft (added to the light block below). */
    --gold:#E8B04F; --gold-deep:#B8742A; --gold-soft:rgba(232,176,79,.2); --on-gold:#1B1407;
    --err:#E0894A;
    --accent:var(--solid); --on-accent:var(--on-solid); --accent-icon:var(--hi); --accent-text:var(--hi);
    --accent-rgb:255,255,255; --accent-deep:#E6E6E6; --accent-soft:rgba(255,255,255,.14);
    --coral:var(--accent); --coral-soft:var(--accent-soft); --coral-icon:var(--accent-icon); --coral-dot:var(--accent); --leave:var(--accent-deep);
    --violet:var(--accent); --plum:#4A4B52; --plum-2:#5A5B63; --pin:var(--accent);
    --paper:var(--surface); --paper-2:var(--surface); --paper-3:var(--surface); --ink-2:#F2F2F2; --ink-3:#FFFFFF; --ink-4:rgba(255,255,255,.62); --link:#FFFFFF;
    --ui:"Instrument Sans",system-ui,-apple-system,"Segoe UI",Roboto,sans-serif; --display:var(--ui); --brand:"Crimson Pro","Big Caslon",Georgia,serif;
    --r:8px; --pill:100px;
    --side:320px;
    /* v35. The whites that meant something: strong text, the solid button and
       its text, hairlines, a field's fill, the ground, the focus ring. */
    --bg:var(--ink); --hi:#FFFFFF; --solid:#FFFFFF; --on-solid:var(--ink);
    --line:rgba(255,255,255,.1); --line-2:rgba(255,255,255,.14); --field:var(--surface);
    --focus:#FFFFFF; --focus-ring:rgba(255,255,255,.7); --halo:rgba(255,255,255,.28); --scroll:rgba(255,255,255,.22);
    --glass-pill:var(--surface); --coral-text:var(--accent-text); --coral-solid:var(--accent);
    color-scheme:dark;
  /* ---- v151. Ruthless design tokens: the discipline the ea7e2ced/d057a95d/
     4e9507b5 pass put on this file (then on a base that has since drifted 32
     versions stale) applied fresh here, to everything the venue grew since —
     the drawer, the rail, the colour picker, 1:1 chat, the privacy square.
     Nothing above this comment moved.
     The --s, --r, --fs, --fw, --lh, --ls, --t and --z families are named by
     rank (continuing --s-1..10 the same way v53 already did), by their literal
     value (--fs-24, --t-360), or — for z-index — as a named layer
     (--z-dock, --z-flyout): every one of those is a genuinely distinct
     measurement powering one rule, not a duplicate of a neighbour, so
     forcing them into fewer, role-named steps would either invent a shared
     meaning that isn't there or move something by a pixel/a frame that
     nobody asked to move. Checked for near-duplicates worth collapsing
     (spacing, duration, z-index) and found none within 1-2px/a perceptible
     alpha that weren't already sharing a token — see README v151.
     Colours and the box-shadow/filter tokens that carry them ARE named by
     role, not value: --white/--black/--ink are the plain primitives;
     --on-dark-N/--shade-N/--ink-aN/--void-aN/--deep-aN/--surface-aN are
     alpha ramps on those and on the existing --surface, each a real percent
     used somewhere; everything else below (--ok-text, --live, --dusk,
     --note-ink, --glow-mote, …) is named for the one thing it colours. A
     small number of near-identical near-black/near-white values were
     snapped onto the ramp they belong to — every merge and its delta is in
     README v151. See README v151 for the full literal count and the
     allowlist of what's still raw. */
  /* space (extended past the original 2px ladder, by rank, continuing --s-1..10) */
  --s-11: 44px; --s-12: 40px; --s-13: 26px; --s-14: 24px; --s-15: 28px; --s-16: 22px; --s-17: 5px; --s-18: 9px; 
  --s-19: 60px; --s-20: 11px; --s-21: 3px; --s-22: 96px; --s-23: 15px; --s-24: 17px; --s-25: 7px; --s-26: 13px; 
  --s-27: 80px; --s-28: 64px; --s-29: 30px; --s-30: 32px; --s-31: 52px; --s-32: 72px; --s-33: 34px; --s-34: 9.5px; 
  --s-35: 54px; --s-36: 68px; --s-37: 128px; --s-38: 70px; --s-39: 74px; --s-40: 84px; --s-41: 124px; --s-42: 78px; 
  --s-43: 66px;
  /* radius (extended; xs..5xl match the existing semantic ladder, the rest are named by their pixel value) */
  --r-999: 999px; --r-5xl: 24px; --r-16: 16px; --r-23: 23px; --r-7: 7px; --r-99: 99px; --r-9: 9px;
  /* type scale: size, weight, line-height, tracking (named by value; this version had none before) */
  --fw-medium: 500; --fs-16: 16px; --lh-1-4: 1.4; --fw-semibold: 600; --fs-15: 15px; --fw-regular: 400; 
  --fs-13: 13px; --fs-11: 11px; --ls-012em: .12em; --fs-26: 26px; --lh-1-25: 1.25; --ls-06px: .6px; --lh-1-5: 1.5; 
  --lh-1-3: 1.3; --fs-24: 24px; --lh-1-2: 1.2; --fs-14: 14px; --lh-1: 1; --fs-12: 12px; --ls-004em: .04em; 
  --lh-1-45: 1.45; --fs-18: 18px; --ls-02px: .2px; --ls-009em: .09em; --fs-21: 21px; --fs-12-5: 12.5px; 
  --lh-1-1: 1.1; --fw-bold: 700; --fs-10: 10px; --fs-19: 19px; --fs-34: 34px; --fs-23: 23px; --fs-40: 40px; 
  --lh-1-35: 1.35; --lh-1-55: 1.55; --ls-01em: .1em; --ls-008em: .08em; --lh-22px: 22px; --fs-22: 22px; 
  --ls-03px: .3px; --fs-28: 28px; --fs-17: 17px; --lh-1-6: 1.6; --ls-01px: .1px; --fs-30: 30px; --lh-1-15: 1.15; 
  --ls-001em: .01em; --ls-014em: .14em; --ls-006em: .06em; --fs-20: 20px;
  --fs-hero: clamp(28px,7vw,44px); --mono: ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;
  /* motion: duration (named by ms) and easing keywords (aliased so every value is a var()) */
  --ease: ease; --t-1150: 1.15s; --ease-out: ease-out; --t-60: .06s; --linear: linear; --t-1600: 1.6s; 
  --t-700: .7s; --t-500: .5s; --t-440: .44s; --t-100: .10s; --t-140: .14s; --t-settle: .18s; --t-panel: .22s; 
  --t-420: .42s; --t-400: .4s; --t-40: .04s; --t-90: .09s; --t-1900: 1.9s; --t-1050: 1.05s; 
  --ease-in-out: ease-in-out; --t-1400: 1.4s; --t-360: .36s; --t-250: .25s; --t-1500: 1.5s; --t-2400: 2.4s; 
  --t-1200: 1.2s; --t-800: .8s; --t-1800: 1.8s; --t-340: .34s; --t-240: .24s; --t-40000: 40s; --t-26000: 26s; 
  --t-3400: 3.4s; --t-1130: 1.13s; --t-2270: 2.27s; --t-280: .28s; --t-ease: .2s; --ease-in: ease-in; 
  --t-380: .38s; --t-35: 35ms; --t-base: .16s; --t-260: .26s; --t-30: 30ms; --t-120: 120ms; --t-150: 150ms; 
  --t-180: 180ms; --t-tap: .12s;
  /* z-index (the stack as it already stood, named by its literal value) */
  --z-gate: 50; --z-gallery: 80; --z-lockdown: 49; --z-crop: 90; --z-floor-ui: 6; --z-5: 5; --z-4: 4; --z-3: 3; --z-2: 2; --z-dock: 8; 
  --z-bar: 9; --z-hud: 7; --z-orient: 28; --z-toast: 12; --z-under: -1; --z-1: 1; --z-modal: 40; --z-admin: 60; --z-drawer: 70; 
  --z-popover: 11; --z-sidebar: 25; --z-sidebar-open: 30; --z-sidebar-tab: 26; --z-countdown: 20; --z-alert: 900; --z-flyout: 75; --z-tip: 76;
  /* blur() filters, reused across glass chips and dim overlays */
  --blur-8: blur(8px); --blur-12: blur(12px); --blur-20-sat: blur(20px) saturate(1.15); --blur-6: blur(6px); 
  --blur-10: blur(10px); --blur-14: blur(14px);
  /* the accent swatch's alpha tints, for rgba(var(--accent-rgb),N) reads that used to write the alpha raw */
  --accent-a75: rgba(var(--accent-rgb),.75); --accent-a85: rgba(var(--accent-rgb),.85); 
  --accent-a50: rgba(var(--accent-rgb),.5); --accent-a55: rgba(var(--accent-rgb),.55); 
  --accent-a0: rgba(var(--accent-rgb),0); --accent-a8: rgba(var(--accent-rgb),.08); 
  --accent-a65: rgba(var(--accent-rgb),.65); --accent-a72: rgba(var(--accent-rgb),.72); 
  --accent-a78: rgba(var(--accent-rgb),.78); --accent-a20: rgba(var(--accent-rgb),.20); 
  --accent-a6: rgba(var(--accent-rgb),.06); --accent-a28: rgba(var(--accent-rgb),.28);
  /* box-shadow/filter/text-shadow/mask-image combinations, named by role: an
     "elev" family for card/panel/popover drop shadows, "ring" for outline/
     selection/focus rings, "glow"/"drop"/"text-shadow" for one-off effect
     glows, "scrim"/"mask" for cutouts and fades, "hairline" for 1px
     dividers. See README v151 for the full list and why a numbered scale
     didn't fit: none of these 65 values were literal duplicates of each
     other except elev-modal/elev-modal-shadow (composed, not copied). */
  --elev-gate: var(--ring),0 24px 60px -20px var(--shade-60); 
  --ring-avatar: 0 0 0 3px var(--white),0 0 0 4px var(--shade-40); 
  --ring-avatar-hover: 0 0 0 3px var(--hi),0 0 0 7px var(--halo); --elev-gallery: var(--ring),0 30px 70px var(--shade-60); 
  --ring-banner-btn: inset 0 0 0 1px var(--on-dark-50); --scrim-crop: 0 0 0 9999px var(--void-a55); 
  --ring-selected: 0 0 0 3px var(--paper),0 0 0 5px var(--hi); --glow-mote: drop-shadow(0 0 4px var(--glow-mote-color)); 
  --glow-impact: 0 0 0 2px var(--glow-impact-1),0 0 30px var(--glow-impact-2); 
  --elev-glasspill: inset 0 0 0 1px var(--on-dark-13),0 2px 6px var(--shade-30),0 20px 46px -16px var(--shade-72); 
  --ring-subtle: 0 0 0 1px var(--on-dark-22); --elev-hint: var(--ring),0 12px 30px -10px var(--shade-60); 
  --ring-focus-scrim: 0 0 0 3px var(--focus-ring),0 0 0 9999px var(--void-a66); --scrim-orient: 0 0 0 9999px var(--void-a66); 
  --elev-orient: var(--ring),0 26px 60px -20px var(--shade-80); --ring-chip: inset 0 0 0 1px var(--on-dark-35); 
  --elev-toast: 0 0 0 1px var(--ink-a20),0 2px 6px var(--shade-30),0 18px 40px -12px var(--shade-75); 
  --ring-toast-btn: inset 0 0 0 1px var(--ink-a35); --drop-react: drop-shadow(0 2px 6px var(--shade-55)); 
  --ring-range: inset 0 0 0 2px var(--on-dark-50),inset 0 0 0 3px var(--shade-22),0 0 0 1px var(--shade-22); 
  --ring-disc: 0 0 0 6px var(--on-dark-90); --ring-speaking: 0 0 0 4px var(--white),0 0 0 8px var(--accent-a75); 
  --ring-pulse-mid: 0 0 0 4px var(--white),0 0 0 6px var(--accent-a85),0 0 0 6px var(--accent-a50); 
  --ring-pulse-end: 0 0 0 4px var(--white),0 0 0 8px var(--accent-a55),0 0 0 22px var(--accent-a0); 
  --shadow-badge: 0 0 0 2px var(--shade-35),0 2px 6px var(--shade-40); 
  --shadow-shield: 0 0 0 2px var(--ink),0 2px 6px var(--shade-45); --text-shadow-name: 0 1px 3px var(--shade-80); 
  --elev-say: 0 4px 16px var(--shade-35); --elev-tag: 0 2px 8px var(--shade-45); 
  --elev-pin: var(--ring),0 16px 40px -16px var(--shade-60); --elev-note: var(--ring),0 18px 44px -18px var(--shade-70); 
  --elev-pop: var(--ring),0 18px 44px -14px var(--shade-62); --ring-live: 0 0 0 2px var(--live-soft); 
  --elev-menu: var(--ring),0 16px 40px -12px var(--shade-60); --elev-modal: var(--ring),var(--elev-modal-shadow);
  --text-shadow-strong: 0 2px 14px var(--shade-60); 
  --glow-cook: drop-shadow(0 0 10px var(--glow-cook-1)) drop-shadow(0 0 26px var(--glow-cook-2)); 
  --elev-drawer-left: 1px 0 0 var(--on-dark-14),32px 0 80px -24px var(--shade-75); 
  --elev-modal-shadow: 0 24px 60px -20px var(--shade-70); 
  --elev-drawer-right: -1px 0 0 var(--on-dark-14),-32px 0 80px -24px var(--shade-75); 
  /* v167. Was --on-dark-09 (invisible in light mode, see --rule-line above) —
     --rule-line already covers this exact "thin hairline that has to read on
     either theme" job, so the rail|pane divider reuses it rather than
     growing its own third light-mode override. */
  --hairline-rail-v: inset -1px 0 0 var(--rule-line); --hairline-rail-h: inset 0 -1px 0 var(--rule-line);
  --elev-slider-thumb: 0 1px 4px var(--shade-55); --elev-spawn-dot: 0 2px 8px var(--shade-50); --ring-bgdel: 0 0 0 1px var(--on-dark-24); 
  --ring-selected-accent: 0 0 0 3px var(--paper),0 0 0 6px var(--accent); --drop-logo: drop-shadow(0 6px 18px var(--shade-45)); 
  --ring-ambience-hover: 0 0 0 2px var(--on-dark-60),0 22px 48px -14px var(--shade-85); 
  --ring-valid: inset 0 0 0 1.5px var(--valid); --ring-ambience: 0 0 0 2px var(--accent-a50); 
  --ring-ambience-drag: 0 0 0 2px var(--accent-a78); --elev-amb-btn: 0 6px 18px -6px var(--shade-70); 
  --ring-ambience-ring: 0 0 0 1px var(--on-dark-60); --elev-pin-link: var(--ring),0 14px 34px -14px var(--shade-60); 
  --ring-address-invalid: inset 0 0 0 1px var(--shade-14); --ring-reach: inset 0 0 0 3px var(--on-dark-55); 
  --ring-spawnmark: 0 0 0 3px var(--white),0 0 0 6px var(--shade-35); --text-shadow-spawnmark: 0 1px 3px var(--shade-85); 
  --shadow-side-mobile: -10px 0 40px var(--shade-53); 
  --mask-dock-fade: linear-gradient(90deg,var(--black) calc(100% - 26px),transparent); 
  --elev-closing-modal: 0 12px 40px var(--shade-45); --elev-author-dot: 0 1px 4px var(--shade-50); 
  --ring-access-live: inset 0 0 0 1px var(--live-active); --elev-flyout: var(--ring),0 20px 50px -16px var(--shade-70); 
  --elev-tip: var(--ring),0 10px 26px -10px var(--shade-70);
  }
  /* v35. Light: warm paper under white panels, charcoal text, the same gold.
     Only the chrome changes — the floor is a painting and keeps its own light. */
  :root[data-theme=light]{
    /* v126. Same one-surface move as dark, above: white, everywhere a panel
       used to shade itself relative to what it sat on. */
    --surface:#FFFFFF;
    --ink:#202125; --bg:#ECEAE4; --panel:var(--surface); --glass:var(--surface); --glass-solid:var(--surface); --ring:inset 0 0 0 1px rgba(32,33,37,.14);
    /* Secondary text at .72, not .6: measured 2026-09-10, .6 on a frosted pill
       was 3.7:1 and on paper 4.2:1; .72 clears 4.5 on both. */
    --fg:#202125; --fg-1:var(--fg); --fg-2:rgba(32,33,37,.84); --fg-3:rgba(32,33,37,.72); --fg-4:rgba(32,33,37,.72);
    --group:var(--surface); --hover:rgba(32,33,37,.07);
    --paper:var(--surface); --paper-2:var(--surface); --paper-3:var(--surface); --ink-2:#202125; --ink-3:#202125; --ink-4:rgba(32,33,37,.72); --link:#202125;
    --hi:#202125; --solid:#202125; --on-solid:#FFFFFF;
    --line:rgba(32,33,37,.1); --line-2:rgba(32,33,37,.16); --field:var(--surface);
    --focus:#202125; --focus-ring:rgba(32,33,37,.7); --halo:rgba(32,33,37,.22); --scroll:rgba(32,33,37,.25);
    --glass-pill:var(--surface);
    /* v167. Dark-over-paper instead of the dark theme's white-over-dark —
       see the --rule-line/--ctrl-track comment in :root, above. */
    --rule-line: rgba(32,33,37,.16); --ctrl-track: rgba(32,33,37,.18);
    /* v136. Theme accent, light half: --accent/--on-accent/--accent-icon/
       --accent-text already flip for free (they point at --solid/--on-solid/
       --hi, redefined above); only the rgb triplet, the hover-deep and the
       wash need their own light values — near-black rather than white. */
    --accent-rgb:32,33,37; --accent-deep:#050505; --accent-soft:rgba(32,33,37,.14);
    --err:#9A4A12;
    color-scheme:light;
  }
  /* v136 (was v115's --coral-text/--coral-icon here). --gold as TEXT on white
     paper is ~2:1 — worse than the red it replaced; teal/blue/violet/rose have
     the same problem, each its own way. Every non-Theme swatch gets a second,
     darker text/icon colour for light paper, tuned to clear 4.5:1 — the Gold
     pair keeps its old numbers (measured 4.99:1) unchanged. */
  :root[data-theme=light][data-accent="gold"]{ --accent-icon:#9A6412; --accent-text:#9A6412; }
  :root[data-theme=light][data-accent="teal"]{ --accent-icon:#1F7A6E; --accent-text:#1F7A6E; }
  :root[data-theme=light][data-accent="blue"]{ --accent-icon:#2A5FB0; --accent-text:#2A5FB0; }
  :root[data-theme=light][data-accent="violet"]{ --accent-icon:#6650AE; --accent-text:#6650AE; }
  :root[data-theme=light][data-accent="rose"]{ --accent-icon:#B14A72; --accent-text:#B14A72; }
  /* v136. The five non-default swatches: solid, hover-deep, translucent wash,
     the rgb triplet (rgba(var(--accent-rgb),alpha) lets a caller pick its own
     alpha rather than baking one in), and the dark-theme accent-icon/
     accent-text — the solid colour itself, legible enough against #17181B
     (the light-theme override above is why paper needs a darker one instead).
     "Theme" needs no block here: it is the :root default, above. */
  :root[data-accent="gold"]{
    --accent:var(--gold); --on-accent:var(--on-gold); --accent-deep:var(--gold-deep); --accent-soft:var(--gold-soft); --accent-rgb:232,176,79;
    --accent-icon:var(--gold); --accent-text:var(--gold); --coral-solid:var(--gold);
  }
  :root[data-accent="teal"]{
    --teal:#3FAE9E;
    --accent:var(--teal); --on-accent:var(--ink); --accent-deep:#2D7D72; --accent-soft:rgba(63,174,158,.2); --accent-rgb:63,174,158;
    --accent-icon:var(--teal); --accent-text:var(--teal); --coral-solid:var(--teal);
  }
  :root[data-accent="blue"]{
    --blue:#4C8DFF;
    --accent:var(--blue); --on-accent:var(--ink); --accent-deep:#3766B8; --accent-soft:rgba(76,141,255,.2); --accent-rgb:76,141,255;
    --accent-icon:var(--blue); --accent-text:var(--blue); --coral-solid:var(--blue);
  }
  :root[data-accent="violet"]{
    --accent-violet:#9585CC;
    --accent:var(--accent-violet); --on-accent:var(--ink); --accent-deep:#6B6093; --accent-soft:rgba(149,133,204,.2); --accent-rgb:149,133,204;
    --accent-icon:var(--accent-violet); --accent-text:var(--accent-violet); --coral-solid:var(--accent-violet);
  }
  :root[data-accent="rose"]{
    --rose:#E0709A;
    --accent:var(--rose); --on-accent:var(--ink); --accent-deep:#A1516F; --accent-soft:rgba(224,112,154,.2); --accent-rgb:224,112,154;
    --accent-icon:var(--rose); --accent-text:var(--rose); --coral-solid:var(--rose);
  }
  /* v126. Was a translucent wash over the art; the gate card is a card like
     any other now, solid --paper (== --surface) same as the base rule below. */
  :root[data-theme=light] #gate .card{background:var(--paper)}
  :root[data-theme=light] #stage{background:var(--stage-bg)}
  /* The gate's bars sit on the dark art either way; only its card goes to paper. */
  :root[data-theme=light] #gate{--fg:#FFFFFF;--fg-2:rgba(255,255,255,.82);--fg-3:rgba(255,255,255,.72);--fg-4:rgba(255,255,255,.72);--hi:#FFFFFF;--ink-3:#FFFFFF;--ink-4:rgba(255,255,255,.72);
    --glass:rgba(255,255,255,.08);--glass-solid:#2A2B30;--ring:inset 0 0 0 1px rgba(255,255,255,.18);--hover:rgba(255,255,255,.08);--group:rgba(255,255,255,.05);
    --line:rgba(255,255,255,.1);--line-2:rgba(255,255,255,.14);--field:rgba(255,255,255,.06);--solid:#FFFFFF;--on-solid:#202125;--focus:#FFFFFF;
    /* the bars inherit body's resolved colour, so the token alone is not enough */
    color:var(--fg)}
  :root[data-theme=light] #gate .card{--fg:#202125;--fg-2:rgba(32,33,37,.84);--fg-3:rgba(32,33,37,.72);--fg-4:rgba(32,33,37,.72);--hi:#202125;--ink-3:#202125;--ink-4:rgba(32,33,37,.72);
    --glass:rgba(255,255,255,.66);--glass-solid:#FFFFFF;--ring:inset 0 0 0 1px rgba(32,33,37,.14);--hover:rgba(32,33,37,.07);--group:rgba(32,33,37,.04);
    --line:rgba(32,33,37,.1);--line-2:rgba(32,33,37,.16);--field:rgba(32,33,37,.05);--solid:#202125;--on-solid:#FFFFFF;--focus:#202125;color:var(--fg)}
  /* v115. The off-mic pill was coral text on a coral wash; dark text on a
     translucent gold wash measured too close to on-gold-on-dark-on-dark to
     read, so it goes solid gold instead, same as the other "this needs your
     attention" surfaces below. */
  :root[data-theme=light] #dock #d-mic.off{background:var(--accent);color:var(--on-accent);box-shadow:none}
  :root[data-theme=light] #dock #d-mic.off:hover{background:var(--accent-deep);color:var(--on-accent)}
  ::selection{background:var(--accent);color:var(--on-accent)}
  :focus-visible{outline:2px solid var(--focus);outline-offset:2px}
  /* Firefox reads these two; everything below is for the WebKit/Blink pair. */
  #side,.pane,#tool-modal .card,#admin .card,#roommenu{scrollbar-width:thin;scrollbar-color:var(--scroll) transparent}
  ::-webkit-scrollbar{width:10px;height:10px} ::-webkit-scrollbar-thumb{background:var(--on-dark-18);border-radius:var(--r-xl);border:3px solid transparent;background-clip:padding-box} ::-webkit-scrollbar-track{background:transparent}
  /* A display rule on a class outranks the hidden attribute, which is how the
     password field stayed on screen with its lock switched off. */
  [hidden]{display:none!important}
  *{box-sizing:border-box} button{font:inherit;color:inherit;background:none;border:0;cursor:pointer;padding:0}
  html,body{height:100%;margin:0;background:var(--bg);color:var(--fg);font:var(--fw-medium) var(--fs-16)/var(--lh-1-4) var(--ui);overflow:hidden;user-select:none}
  .hidden{display:none!important}
  svg.i{width:20px;height:20px;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round;display:block;flex:none}
  svg.i.s{width:16px;height:16px} svg.i.l{width:24px;height:24px}

  /* ================= gate (pre-join) ================= */
  #gate{position:fixed;inset:0;z-index:var(--z-gate);background:linear-gradient(180deg,var(--ink-a55),var(--ink-a82)),var(--ink);background-size:cover;background-position:center;display:grid;grid-template-rows:64px 1fr 48px}
  /* v33. The gate leaves by fading; the floor is already under it. */
  #gate{transition:opacity var(--t-scene) var(--ease)}
  #gate.fading{opacity:0;pointer-events:none}
  #gate .gtop{display:flex;align-items:center;gap:var(--s-4);padding:var(--s-4)}
  #gate .gtop .spacer{flex:1}
  /* One column is taller than two: the card must be able to scroll on a short
     viewport, or Join space sits below the fold with no way to reach it. */
  /* v47/v166. v47's own fix centred the card in #gmid with auto margins and
     let #gmid itself scroll — a centred box whose content overflows a
     scrolling ancestor still overflows equally on both edges, and the half
     above the top edge is the half a browser will not scroll back to; on a
     tall card (the health panel, mic test and all) that took the heading off
     the top with no way back to it, on exactly the "scrolling crops the top"
     report this comment used to say was already fixed. #gmid centres the
     card and never scrolls itself now; the card is capped to the room #gmid
     actually has (max-height:100%, itself resolved off #gmid's own definite
     height — the middle grid row of #gate's fixed, full-viewport box) and
     scrolls INSIDE that cap, so it opens with its own header on screen and
     never has an unreachable half in either direction. */
  #gate .gmid{position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:var(--s-10);overflow:hidden;min-height:0}
  #gate .gmid > *{flex:none}
  /* One column, one face. The picker grid is retired: the big avatar IS the
     control — it starts on a random face and each press moves to another. */
  /* v126. Was a translucent dark wash (blurred) over the art; solid --paper
     now, same surface every other card uses — the blur did nothing once the
     fill is opaque, so it goes too.
     v166. A little wider (400 -> 440) — Munk's live pass — and its own
     scroller now (see the #gmid comment above): max-height:100% of #gmid's
     definite height, overflow-y:auto, so the card's own header is what a
     scroll starts on rather than whatever the middle of a taller pane
     happened to centre on. */
  /* v166 item 11 (Munk, "minify it, ideally NO scrolling"): padding and the
     gate-left/gate-right gap both came down from --s-11/--s-12/--s-13
     (44/40/26px) — the single biggest source of the card's own overflow at
     1280x720/390x844 was its own frame, not the content inside it. */
  #gate .card{width:min(440px,94vw);max-height:100%;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable;background:var(--paper);box-shadow:var(--elev-gate);border-radius:var(--r);padding:var(--s-6) var(--s-10);display:grid;grid-template-columns:1fr;gap:var(--s-6);color:var(--ink-3);min-width:0}
  #gate .gate-left,#gate .gate-right{min-width:0}
  /* Locked = the password has not been accepted, and the faces go with it.
     The gate's top bar carries one thing at every stage: "Return to website", a way out.
     Menu and the avatar chip were decoration imitating the app's own chrome —
     they read as controls that do nothing until a room is open, so they are the
     app's, not the gate's. */
  #gate.locked .gate-left{display:none}
  #gate .gate-left{display:grid;gap:var(--s-8);justify-items:center}
  /* The face is the picker: pressing it opens the gallery. The little grid
     under it is a third way to do the same thing and shows nowhere now. */
  /* The whole welcome modal is the one surface; the mode note is chatter. */
  #gate #mode-note{display:none}
  /* avatar library: the 39 Story Quest characters, by URL off the Onepage CDN.
     --src is the image; the identity travelling between peers is still the index. */
  .av{--src:none;background-image:var(--src);background-size:cover;background-position:center top;background-repeat:no-repeat;background-color:var(--plum);border-radius:50%;display:inline-block}
  .gate-left{display:grid;gap:var(--s-7);align-content:start}
  .avhead{display:grid;gap:var(--s-6);justify-items:center;text-align:center}
  /* v166 item 11: tighter at the door only (same #gate-scoping reasoning as
     #gate .avbig, above — this stops applying once #me-panel moves into the
     drawer's own Profile pane). */
  #gate .avhead{gap:var(--s-4)}
  .avbig{width:112px;height:112px;flex:none;box-shadow:var(--ring-avatar);cursor:pointer;transition:transform var(--t-base) var(--ease-press),box-shadow var(--t-base)}
  /* v166 (Munk, "minify it, ideally NO scrolling"): a smaller face preview
     at the door only — once #me-panel moves into the drawer's own Profile
     pane (#tool-content, no #gate ancestor any more) this stops applying
     and the full 112px comes back, where there is room for it. */
  #gate .avbig{width:76px;height:76px}
  .avbig:hover{transform:scale(1.04);box-shadow:var(--ring-avatar-hover)}
  .avbig:active{transform:scale(.98)}
  .avttl{font:var(--fw-semibold) var(--fs-15) var(--ui);color:var(--hi)} .avsub{font:var(--fw-regular) var(--fs-13)/var(--lh-1-4) var(--ui);color:var(--ink-4);margin-top:var(--s-1)}
  /* v42. The small avatar grid is gone: the face IS the picker and it opens
     the gallery. The grid was drawn at the door (hidden there) and again in
     the profile panel - two more ways to do the one thing. */
  .disc .av{position:absolute;inset:0;width:100%;height:100%}
  .avatar .av{width:100%;height:100%;border-radius:50%}
  /* v166 item 11: padding-top only ever applies at the door — the drawer's
     own #me-panel.as-edit .gate-right (below) already zeroes it there —
     so shrinking it here is door-only in effect either way. */
  .gate-right{display:grid;gap:var(--s-6);align-content:start;padding-top:var(--s-4)}
  /* v42. One panel, two homes. At the door it is the join step; pressed from
     your own face it is Your profile, moved node-for-node into the modal so
     the fields, the face and the microphone test are the same elements and
     never a second copy with the same ids. */
  #me-panel{display:grid;gap:var(--s-13);min-width:0}
  #me-panel.as-edit{gap:var(--s-10)}
  #me-panel.as-edit .gate-right{padding-top:0}
  #me-panel.as-edit #gate-title,#me-panel.as-edit #auth-step,#me-panel.as-edit #enter,
  #me-panel.as-edit #who,#me-panel.as-edit #access-msg,#me-panel.as-edit #mode-note,
  #me-panel.as-edit #gate-trust,#me-panel.as-edit #gate-health{display:none}
  #me-panel.as-edit .gate-left{justify-items:center}
  /* At the door the picture is a thing to choose; inside, it is your face. */
  .as-edit-only{display:none}
  #me-panel.as-edit .as-edit-only{display:block}
  #me-panel.as-edit .avhead > div > .avttl:not(.as-edit-only){display:none}
  .devrows{display:none;gap:var(--s-6)}
  /* Two ideas, two blocks: who you are, and what you sound like. At the door
     they read as one flow (no headings, no rules); in the profile each block
     is named and separated. */
  .pblock{display:grid;gap:var(--s-8);min-width:0}
  /* v166 (Munk, "minify it"): tighter at the door specifically — the
     drawer's own #me-panel.as-edit .pblock rule (below) is more specific
     and wins there regardless, so Profile/Sound in the drawer keep their
     own, more generous gap. */
  #gate .pblock{gap:var(--s-5)}
  .pblock-h{margin:0;font:var(--fw-semibold) var(--fs-11) var(--ui);letter-spacing:var(--ls-012em);text-transform:uppercase;color:var(--fg-3)}
  #me-panel.as-edit .pblock{gap:var(--s-6);padding-top:var(--s-9);border-top:1px solid var(--line-2)}
  #me-panel.as-edit .avhead{padding-bottom:var(--s-1)}
  /* #tool-content has no top padding, so the face's white ring sat flush
     against the scroll edge and read as a cropped head. */
  #me-panel.as-edit{padding-top:var(--s-4)}
  .amb-row{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-5)}
  .amb-row b{font-weight:var(--fw-semibold);font-variant-numeric:tabular-nums;color:var(--hi)}
  .f.amb{display:grid;gap:var(--s-4)}
  .devrows input[type=range]{accent-color:var(--accent)}
  /* v134. #gate-mic-test itself carries .as-edit now, not just an ancestor
     #me-panel — Sound moves it out to its own drawer pane, no longer a
     descendant of #me-panel, so the reveal has to key off the node itself. */
  #me-panel.as-edit .devrows,#gate-mic-test.as-edit .devrows{display:grid}
  /* v166 (Munk, "minify it"): centred, and tighter letter-spacing — .6px
     was an outlier against every other --brand heading in this file
     (#orient h3, .auth-confirm h2, …), which already read at --ls-02px. */
  .gate-right h1{font:var(--fw-regular) var(--fs-26)/var(--lh-1-25) var(--brand);letter-spacing:var(--ls-02px);margin:0 0 var(--s-2);color:var(--hi);text-wrap:balance;text-align:center}
  .gate-right p{margin:0;font:var(--fw-regular) var(--fs-16)/var(--lh-1-5) var(--display);color:var(--ink-4)}
  .gate-right p a{color:var(--link);text-decoration:underline;text-underline-offset:3px}
  label.f{display:grid;gap:var(--s-3);font:var(--fw-medium) var(--fs-13) var(--display);color:var(--ink-4)}
  /* v166 (Munk, "minify it"): "About you" no longer carries its own
     "optional" span — the placeholder ("A line about you") signals that
     now — so a single-line field can grow with what is typed instead of
     starting at the generic textarea's own 2-line min-height. Capped short
     of runaway growth; #chatin/#dmin's own chatGrow/dmGrow (venue.js) are
     the same shape, wired to #bio as bioGrow. */
  #bio{resize:none;overflow-y:auto;min-height:0;max-height:104px}
  /* v124. input[type=url] (the Venue flyout's Con calendar link) fell
     through this list and rendered browser-default — any input this rule
     doesn't name does, since only text/email/password/textarea/select were
     ever listed. Added here rather than a one-off rule so every url field,
     present or future, is styled for free.
     v135. type=search (Hosting's "Search people") fell through the same way
     — docs/ui-backlog.md's "One button style, one field style" caught it as
     a raw browser box. appearance:none below also drops the native
     cancel/magnifier chrome Chromium/Safari otherwise add to a search box. */
  input[type=text],input[type=email],input[type=password],input[type=url],input[type=search],textarea,select{font:var(--fw-regular) var(--fs-16) var(--display);color:var(--hi);background:var(--field);border:0;box-shadow:var(--ring);margin:0;height:auto;line-height:var(--lh-1-3);padding:var(--s-6) var(--s-7);outline:0;border-radius:var(--r);width:100%;appearance:none;caret-color:var(--accent);transition:box-shadow var(--t-base)}
  input::placeholder,textarea::placeholder{color:var(--fg-3)}
  textarea{resize:vertical;min-height:64px;line-height:var(--lh-1-4)}
  input:focus,textarea:focus,select:focus{box-shadow:inset 0 0 0 1px var(--focus-ring);outline:0}
  .btn{font:var(--fw-semibold) var(--fs-15) var(--display);border-radius:var(--pill);height:48px;padding:0 var(--s-14);background:var(--solid);color:var(--on-solid);display:inline-flex;align-items:center;gap:var(--s-4);justify-content:center;transition:transform var(--t-base) var(--ease-press),box-shadow var(--t-base)}
  .btn:hover{box-shadow:0 0 0 3px var(--halo)} .btn:active{transform:scale(.98)}
  .btn:disabled{opacity:.55;cursor:default;box-shadow:none}
  .btn.plain{background:transparent;color:var(--hi);box-shadow:var(--ring)}
  .btn.violet{background:var(--accent);color:var(--on-accent)}
  /* ---- the face gallery: press the avatar, pick from all of them ---- */
  #av-gal{position:fixed;inset:0;z-index:var(--z-gallery);display:none;place-items:center;padding:var(--s-14);background:var(--void-a72);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}
  #av-gal.open{display:grid}
  :root[data-theme=light] #av-gal .gal{--hi:#202125;--ink-3:#202125;--ink-4:rgba(32,33,37,.72);--fg:#202125;--fg-2:rgba(32,33,37,.84);
    --fg-3:rgba(32,33,37,.72);--ring:inset 0 0 0 1px rgba(32,33,37,.14);--paper:#FFFFFF}
  #av-gal .gal{width:min(680px,94vw);background:var(--paper);border-radius:var(--r-3xl);box-shadow:var(--elev-gallery);padding:var(--s-13) var(--s-15) var(--s-16);display:grid;gap:var(--s-8);min-width:0}
  #av-gal h3{font:var(--fw-regular) var(--fs-24)/var(--lh-1-2) var(--brand);margin:0;color:var(--hi);text-align:center}
  /* Fixed tiles, deliberately. Sizing a circle with aspect-ratio off a 1fr
     column made the row height depend on a width the grid had not resolved yet:
     rows came out 37px under 110px tiles and every face overlapped the one
     below. A pixel column and a pixel row have no such circularity. */
  #av-gal .gal-tabs{display:flex;gap:var(--s-4);justify-content:center}
  #av-gal .gal-tabs button{font:var(--fw-medium) var(--fs-14)/var(--lh-1) var(--ui);padding:var(--s-4) var(--s-8);border-radius:var(--r-999);color:var(--fg-2);background:transparent;box-shadow:var(--ring);cursor:pointer}
  #av-gal .gal-tabs button[aria-selected=true]{color:var(--paper);background:var(--hi);box-shadow:none}
  #av-gal .gal-grid{display:grid;grid-template-columns:repeat(5,96px);grid-auto-rows:min-content;gap:var(--s-8);justify-content:center;max-height:min(52vh,430px);overflow-y:auto;padding:var(--s-1) var(--s-3)}
  /* v61b. The framing dialog. It sits above the gallery that opened it, and the
     ring is drawn over the canvas rather than clipping it, so the person can see
     what they are cutting off as well as what they are keeping. */
  /* v60. Venue › Reports/Flagged/Faces/Words/Codes (was the admin desk, one
     wide sheet of its own — the desk and its own .desk-tabs are gone since
     v133). A report is a row you read, not a card you admire: the reasons
     are chips, the words are quoted, and the buttons sit where the eye
     already is at the end of the line. */
  /* v126. --paper-3 used to be a shade lighter than the row's own surface;
     collapsed to the same --surface now, so the divider draws with --line-2
     instead of a fill nobody would see. */
  .desk-row{display:grid;gap:var(--s-4);padding:var(--s-8) 0;border-top:1px solid var(--line-2)}
  .desk-row:first-of-type{border-top:0}
  .desk-who{font:var(--fw-semibold) var(--fs-15) var(--ui);color:var(--hi)}
  .desk-when{font:var(--fw-regular) var(--fs-12) var(--ui);color:var(--ink-4)}
  .desk-chips{display:flex;flex-wrap:wrap;gap:var(--s-3)}
  /* v126. Same collapse: a chip on the desk row reads by its ring, not a
     fill that now matches the row it sits on. */
  .desk-chips span{font:var(--fw-medium) var(--fs-11)/var(--lh-1) var(--ui);letter-spacing:var(--ls-004em);text-transform:uppercase;padding:var(--s-17) var(--s-18);border-radius:var(--r-999);background:transparent;box-shadow:var(--ring);color:var(--ink-2)}
  .desk-note{margin:0;font:var(--fw-regular) var(--fs-14)/var(--lh-1-5) var(--ui);color:var(--ink-2)}
  .desk-claim{margin:0;font:var(--fw-regular) var(--fs-12)/var(--lh-1-5) var(--ui);color:var(--ink-4)}
  .desk-acts{display:flex;flex-wrap:wrap;gap:var(--s-4)}
  /* v66b. The Access panel: one row a link, expanding in place — same idea
     as .desk-row, but a row is a button (so the whole head is the toggle,
     not a chevron nobody notices) and the divider only draws under a closed
     row so an expanded one reads as one continuous block with its own form. */
  /* v126. --paper-3 collapsed into --surface (below); a divider drawn in the
     surface colour is invisible, so it draws in --line-2 instead. */
  #tool-modal:not(.sheet) #tool-content .rule{height:1px;background:var(--line-2);margin:var(--s-6) 0}
  .access-row{border-top:1px solid var(--line-2);padding:var(--s-6) 0}
  .access-row:first-of-type{border-top:0}
  .access-row-head{display:grid;gap:var(--s-2);width:100%;text-align:left;cursor:pointer}
  /* v126. A "lighter layer inside the surface" that is no longer lighter —
     the fill stays (still correct, just == the surface) and a ring says
     where the block starts. */
  .access-edit{display:grid;gap:var(--s-6);margin-top:var(--s-6);padding:var(--s-6);background:var(--paper-3);box-shadow:var(--ring);border-radius:var(--r-lg)}
  .scope-pick{border:0;margin:0;padding:0;display:grid;gap:var(--s-3)}
  .scope-pick legend{font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--ink-4);padding:0 0 var(--s-3)}
  .chk{display:flex;align-items:center;gap:var(--s-3);font:var(--fw-regular) var(--fs-13) var(--ui);color:var(--ink-2);padding:var(--s-2) 0}
  .chk input{margin:0}
  /* v115. Lockdown/emergency reads as urgent because of the layout (fixed,
     centred, loud), not because it borrows red — solid gold, dark text. */
  .access-emergency-banner{margin:0;padding:var(--s-5) var(--s-6);border-radius:var(--r-lg);background:var(--coral-solid);color:var(--on-accent);font:var(--fw-semibold) var(--fs-13) var(--ui)}
  /* The floor-wide instance (index.html #emergency-banner): fixed to the top
     edge, above the stage, square corners — a banner, not a card. */
  #emergency-banner{position:fixed;top:0;left:0;right:0;z-index:var(--z-gate);text-align:center;border-radius:0;display:flex;gap:var(--s-6);align-items:center;justify-content:center}
  #emergency-banner .btn{color:var(--white);box-shadow:var(--ring-banner-btn)}
  /* v113. Same fixed-banner shape as #emergency-banner, one below it so both
     can show at once without overlapping (an outage and a lockdown are not
     mutually exclusive). Visible to every visitor, not staff-only — see the
     comment on venueLockdown (venue.js) for why. */
  #lockdown-banner{position:fixed;top:0;left:0;right:0;z-index:var(--z-lockdown);text-align:center;border-radius:0;display:flex;gap:var(--s-6);align-items:center;justify-content:center}
  #emergency-banner:not(.hidden) + #lockdown-banner{top:var(--s-12)}
  #lockdown-banner .btn{color:var(--white);box-shadow:var(--ring-banner-btn)}
  .access-reveal{display:grid;gap:var(--s-4)}
  /* v62. The code, shown once. Big enough to read off a laptop and type into
     a phone across a room, and monospaced so the characters are told apart -
     the same reason the alphabet has no O or l in it. */
  /* v126. Ring added: --paper-3 is the card's own surface now, so the code
     block needs its own boundary to still read as a block. */
  .code-given{font:var(--fw-bold) var(--fs-hero)/var(--lh-1-2) var(--mono);letter-spacing:var(--ls-012em);text-align:center;padding:var(--s-8) var(--s-6);margin:var(--s-8) 0;border-radius:var(--r-2xl);background:var(--paper-3);box-shadow:var(--ring);color:var(--hi);user-select:all}
  .desk-blocked{opacity:.72}
  .desk-faces{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:var(--s-8)}
  /* v167. The tile is the button now (faceRowHTML) — Remove/Reset live in a
     flyout it opens, not a button of their own under every face at once. */
  .desk-face{display:grid;gap:var(--s-3);justify-items:center;text-align:center;border:0;background:none;padding:var(--s-3);border-radius:var(--r-lg);cursor:pointer;font:inherit;color:inherit;transition:background var(--t-base) var(--ease)}
  .desk-face:hover,.desk-face:focus-visible{background:var(--hover)}
  .desk-face:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  /* v126. Avatar placeholder: --paper-3 no longer stands apart from the card
     behind it, so a ring marks the circle even with no photo loaded. */
  .desk-face .av{width:72px;height:72px;border-radius:50%;background:var(--paper-3) center/cover no-repeat;background-image:var(--src);box-shadow:var(--ring)}
  .desk-face small{font:var(--fw-regular) var(--fs-11)/var(--lh-1-3) var(--ui);color:var(--ink-4);word-break:break-all}
  .desk-face-preview{display:grid;justify-content:center;padding:var(--s-4) 0 var(--s-6)}
  .desk-face-preview .av{width:96px;height:96px;border-radius:50%;background:var(--paper-3) center/cover no-repeat;background-image:var(--src);box-shadow:var(--ring)}
  #av-crop{position:fixed;inset:0;z-index:var(--z-crop);display:none;place-items:center;padding:var(--s-14);background:var(--void-a80);-webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px)}
  #av-crop.open{display:grid}
  #av-crop .crop{width:min(420px,94vw);background:var(--paper);border-radius:var(--r-3xl);box-shadow:var(--elev-gallery);padding:var(--s-13) var(--s-15) var(--s-16);display:grid;gap:var(--s-8);justify-items:center}
  #av-crop h3{font:var(--fw-regular) var(--fs-24)/var(--lh-1-2) var(--brand);margin:0;color:var(--hi);text-align:center}
  /* v126. Ring added for the same reason as .desk-face .av above. */
  #av-crop .crop-stage{position:relative;width:320px;height:320px;max-width:100%;border-radius:var(--r);overflow:hidden;background:var(--paper-3);box-shadow:var(--ring);touch-action:none;cursor:grab}
  #av-crop .crop-stage:active{cursor:grabbing}
  #av-crop canvas{display:block;width:100%;height:100%}
  /* Everything outside the circle, dimmed in one box-shadow rather than four
     divs. pointer-events:none or it eats the drag. */
  #av-crop .crop-ring{position:absolute;inset:0;border-radius:50%;pointer-events:none;
    box-shadow:var(--scrim-crop);outline:2px solid var(--on-dark-85);outline-offset:-2px}
  #av-crop .crop-zoom{display:grid;gap:var(--s-3);width:100%;font:var(--fw-medium) var(--fs-13) var(--display);color:var(--ink-4)}
  #av-crop .crop-zoom input{width:100%}
  #av-crop .fine{margin:0;text-align:center;font:var(--fw-regular) var(--fs-12)/var(--lh-1-5) var(--ui);color:var(--ink-4)}
  #av-crop .crop-actions{display:flex;gap:var(--s-4);width:100%}
  #av-crop .crop-actions .btn{flex:1}
  #av-gal .gal-head{grid-column:1/-1;margin:var(--s-3) 0 0;font:var(--fw-medium) var(--fs-12)/var(--lh-1) var(--ui);letter-spacing:var(--ls-012em);text-transform:uppercase;color:var(--fg-2)}
  #av-gal .gal-head:first-child{margin-top:0}
  /* v167. A tile press had no feedback at all — only the .av filter below
     eased. Small-button press per the venue's own convention (.avbig, .btn). */
  #av-gal .gal-grid button{display:block;width:96px;height:96px;border-radius:50%;overflow:hidden;position:relative;box-shadow:var(--ring);transition:transform var(--t-base) var(--ease-press)}
  #av-gal .gal-grid button:active{transform:scale(.97)}
  #av-gal .gal-grid .av{display:block;width:100%;height:100%;border-radius:50%;filter:saturate(.8) brightness(.85);transition:filter var(--t-base)}
  #av-gal .gal-grid button:hover .av{filter:none}
  #av-gal .gal-grid button[aria-pressed=true]{box-shadow:var(--ring-selected)}
  #av-gal .gal-grid button[aria-pressed=true] .av{filter:none}
  @media(max-width:560px){#av-gal .gal-grid{grid-template-columns:repeat(3,84px);grid-auto-rows:min-content;gap:var(--s-6)}#av-gal .gal-grid button{width:84px;height:84px}}
  /* Once the test is running the control stops being an offer and becomes an
     instruction: no pill, no fill, just the line telling you to talk. */
  #g-mic.listening{background:none;color:var(--ink-4);box-shadow:none;height:auto;padding:0;font:var(--fw-medium) var(--fs-14) var(--display);justify-content:flex-start}
  #g-mic.listening:hover{box-shadow:none} #g-mic.listening:active{transform:none}
  /* ---- microphone test: the face pulses the way it does in a room ---- */
  .avbig.testing{animation:lh-pulse var(--t-1150) var(--ease-out) infinite}
  #gate-mic-test{display:grid;gap:var(--s-5)}
  #mic-join-wrap{display:flex;align-items:center;gap:var(--s-4);cursor:pointer}
  #gate-mic-meter{display:none;align-items:center;gap:var(--s-5)}
  #gate-mic-meter.on{display:flex}
  #mic-bar{flex:1;height:8px;border-radius:var(--r-sm);background:var(--line-2);overflow:hidden;min-width:0}
  /* scaleX, not width: this repaints every animation frame while the mic test runs. */
  #mic-bar span{display:block;height:100%;width:100%;border-radius:var(--r-sm);background:var(--accent);transform:scaleX(0);transform-origin:left center;transition:transform var(--t-60) var(--linear)}
  #mic-state{display:flex;align-items:center;gap:var(--s-3);font:var(--fw-semibold) var(--fs-12) var(--ui);color:var(--ink-4);white-space:nowrap}
  #mic-state.ok{color:var(--ok-text)} #mic-state.bad{color:var(--accent)}
  #mic-state .i{width:16px;height:16px}
  .devs{display:flex;gap:var(--s-6);margin-top:var(--s-6)}
  .devs .btn{flex:1;height:44px;font-size:var(--fs-14)}
  .devs .btn.on{background:var(--white);color:var(--ink)} .devs .btn.off{background:var(--on-dark-08);color:var(--fg-2);box-shadow:var(--ring)}
  .devs .btn.off:hover{background:var(--on-dark-14)}
  .fine{font:var(--fw-regular) var(--fs-14)/var(--lh-1-45) var(--display);color:var(--ink-4);border-top:1px solid var(--on-dark-12);padding-top:var(--s-7);text-align:center}
  .fine b{color:var(--white);font-weight:var(--fw-semibold)} .fine code{font-size:var(--fs-12);background:var(--on-dark-10);padding:1px var(--s-17);border-radius:var(--r-sm)}
  /* v166 (Munk, "minify it"): one plain line now (the heading and the Help
     link both left — the heading to the drop below, Help to #gate-help),
     folded into the Sound pblock right above the control it is about
     rather than its own separator block between the headline and the form. */
  #gate-trust{margin:0 0 var(--s-1);font:var(--fw-regular) var(--fs-12-5)/var(--lh-1-45) var(--display);color:var(--ink-4)}
  #gate-health{display:grid;gap:var(--s-4);padding:var(--s-7);border:1px solid var(--line);border-radius:var(--r);background:var(--surface-a70);font:var(--fw-regular) var(--fs-13)/var(--lh-1-45) var(--display);color:var(--ink-4)}
  .gate-health-head{display:flex;align-items:center;justify-content:space-between;gap:var(--s-5)}
  .gate-health-head strong{color:var(--hi);font-weight:var(--fw-semibold)}
  .text-action{appearance:none;border:0;padding:0;background:transparent;color:var(--link);font:var(--fw-semibold) var(--fs-13) var(--ui);text-decoration:underline;text-underline-offset:3px;cursor:pointer}
  .text-action:disabled{color:var(--fg-4);cursor:wait;text-decoration:none}
  #gate-health-summary{font-size:var(--fs-13)}
  #gate-health-details summary{width:max-content;max-width:100%;color:var(--link);font:var(--fw-semibold) var(--fs-13) var(--ui);cursor:pointer}
  #gate-health-list{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--s-3) var(--s-6);margin:0;padding:0;list-style:none}
  #gate-health-details[open] #gate-health-list{margin-top:var(--s-4)}
  #gate-health-list li{display:grid;grid-template-columns:auto 1fr;align-items:start;gap:var(--s-3);min-width:0}
  #gate-health-list .health-dot{width:9px;height:9px;margin-top:5px;border-radius:50%;background:var(--fg-4);box-shadow:0 0 0 2px var(--surface)}
  #gate-health-list .health-dot.available{background:var(--live)}
  #gate-health-list .health-dot.unavailable{background:var(--coral)}
  #gate-health-list b{display:block;color:var(--hi);font-weight:var(--fw-semibold)}
  #gate-health-list small{display:block;color:var(--ink-4);font:inherit}
  #gate .gbot{display:flex;align-items:center;padding:0 var(--s-14);background:var(--surface-a70);font:var(--fw-regular) var(--fs-18) var(--brand);letter-spacing:var(--ls-06px)}
  #gate .gbot .spacer{flex:1}
  #gate .gbot small{font:var(--fw-semibold) var(--fs-12) var(--ui);color:var(--fg-3);margin-left:var(--s-10)}
  /* v166. Replaces the "Help" text link that used to sit inside #gate-trust.
     Bottom-left of #gmid (the middle grid row, position:relative above) —
     not the viewport, which put it right over .gbot's own "LegendHaven" text
     at the very bottom of a short window — so it is clear of the header and
     footer rows at any height, on screen for #auth-step and #profile-step
     alike since it is #gmid's own child, not either step's. The same
     round-glass shape #mobile-side already wears elsewhere in the app. */
  #gate-help{position:absolute;left:var(--s-8);bottom:var(--s-8);width:44px;height:44px;display:grid;place-items:center;border-radius:50%;color:var(--hi);background:var(--glass);box-shadow:var(--ring);-webkit-backdrop-filter:var(--blur-12);backdrop-filter:var(--blur-12);font:var(--fw-bold) var(--fs-18) var(--ui);text-decoration:none}
  #gate-help:hover{background:var(--hover)}
  #gate-help:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
  @media(max-width:900px){#gate-help{left:var(--s-6);bottom:var(--s-6);width:40px;height:40px}}

  /* ================= app frame ================= */
  #app{position:fixed;inset:0;display:grid;grid-template-columns:1fr var(--side);grid-template-rows:1fr}
  /* The floor's chrome sizes itself against the floor, not the window — the
     sidebar takes a few hundred pixels the viewport knows nothing about. */
  #stage{position:relative;overflow:hidden;background:var(--ink);cursor:grab;container:stage / inline-size}
  #stage.panning{cursor:grabbing}
  /* The wrapper carries the arrival, so applyView keeps sole ownership of
     #world's transform and the two never fight over the same property. */
  /* An 8 MB background is a real wait on a hotel connection, so the room says
     it is coming and how far along it is instead of showing an empty floor. */
  #loading{position:absolute;inset:0;z-index:var(--z-floor-ui);display:grid;place-items:center;background:var(--deep-a82);
    -webkit-backdrop-filter:var(--blur-8);backdrop-filter:var(--blur-8);-webkit-backdrop-filter:var(--blur-8);transition:opacity var(--t-travel) var(--ease)}
  #loading.hidden{display:none}
  #loading.out{opacity:0;pointer-events:none}
  #loading .box{display:grid;gap:var(--s-8);justify-items:center;width:min(300px,72vw);text-align:center}
  #loading .rn{font:var(--fw-regular) var(--fs-24)/var(--lh-1-2) var(--brand);letter-spacing:var(--ls-02px);color:var(--white)}
  #loading .bar{width:100%;height:4px;border-radius:var(--r-xs);background:var(--on-dark-14);overflow:hidden;position:relative}
  #loading .bar span{position:absolute;inset:0;border-radius:var(--r-xs);background:var(--accent);transform-origin:left center;
    transform:scaleX(var(--p,0));transition:transform var(--t-settle) var(--linear)}
  /* Without a Content-Length there is no percentage to show, so the bar paces
     itself rather than inventing one. */
  #loading.indeterminate .bar span{width:38%;inset:0 auto 0 0;transform:none;animation:lh-slide var(--t-1150) var(--ease-loop) infinite}
  @keyframes lh-slide{0%{left:-40%}100%{left:100%}}
  #loading .pct{font:var(--fw-semibold) var(--fs-12) var(--ui);letter-spacing:var(--ls-009em);text-transform:uppercase;color:var(--fg-3);font-variant-numeric:tabular-nums}
  @media (prefers-reduced-motion: reduce){#loading.indeterminate .bar span{animation:none;width:100%;left:0;opacity:.45}}

  /* The floor layer, not #stage: a pinch has to reach the gesture handler
     rather than the browser's own page zoom, but the panels floating over
     the stage still have to scroll under a finger. */
  #worldwrap{position:absolute;inset:0;transform-origin:50% 50%;touch-action:none}
  #worldwrap.arriving{animation:lh-arrive var(--t-scene) var(--ease-arrive)}
  @keyframes lh-arrive{from{transform:scale(1.045);opacity:.25;filter:blur(3px)}to{transform:none;opacity:1;filter:none}}
  /* v33. Your first landing: the face drops in from above at 2x and lands
     hard, then the motes. The transform is on the bubble itself, which
     carries none of its own; the speech line and the ring are children. */
  .bubble.me.slam{animation:lh-slam var(--t-1600) var(--ease-slam) both;z-index:var(--z-5)}
  @keyframes lh-slam{0%{transform:translate(var(--sx,0px),var(--sy,0px)) scale(var(--sk,3.8));opacity:0}
    16%{transform:translate(var(--sx,0px),var(--sy,0px)) scale(calc(var(--sk,3.8) * .9));opacity:1}
    52%{transform:translate(var(--sx,0px),var(--sy,0px)) scale(calc(var(--sk,3.8) * .9));opacity:1}
    100%{transform:none;opacity:1}}
  /* The hearing circle is inside the bubble; scaled with it, it would cover the room. */
  .bubble.me.slam .range{opacity:0!important}
  .mote{position:absolute;z-index:var(--z-4);pointer-events:none;width:9px;height:9px;margin:calc(var(--s-2) * -1) 0 0 calc(var(--s-2) * -1);border-radius:50%;
    background:radial-gradient(circle,var(--mote-core) 0%,var(--mote-mid) 45%,var(--mote-fade) 72%);filter:var(--glow-mote);
    animation:lh-mote var(--t,.9s) var(--ease-move) forwards}
  @keyframes lh-mote{0%{transform:translate(0,0) scale(.6);opacity:0}12%{opacity:1;transform:translate(calc(var(--dx)*.25),calc(var(--dy)*.25)) scale(1.15)}100%{transform:translate(var(--dx),var(--dy)) scale(.15);opacity:0}}
  .impact{position:absolute;z-index:var(--z-3);pointer-events:none;width:120px;height:120px;margin:calc(var(--s-19) * -1) 0 0 calc(var(--s-19) * -1);border-radius:50%;
    box-shadow:var(--glow-impact);animation:lh-impact var(--t-700) var(--ease-out) forwards}
  @keyframes lh-impact{0%{transform:scale(.2);opacity:1}100%{transform:scale(2.2);opacity:0}}
  @media (prefers-reduced-motion: reduce){.bubble.me.slam{animation:none}.mote{animation-duration:var(--t-500)}.impact{animation:none;display:none}}
  @media (prefers-reduced-motion: reduce){#worldwrap.arriving{animation:lh-fade var(--t-ease) var(--linear)}@keyframes lh-fade{from{opacity:.4}to{opacity:1}}}

  /* v49. Arriving: the venue assembles instead of appearing whole. The material
     is the floor's own — a small scale and a blur clearing, lh-arrive's curve —
     so the chrome and the floor read as one event rather than four.
     Every one of these is a BEGINNING added to something already visible, and
     the fill is `backwards`, never `both`: an animation that does not run
     leaves the finished venue on screen, not an empty one. That matters here,
     because the venue is embedded and a hidden tab is not to be trusted with
     anything that gates what can be seen. Nothing waits on it either — every
     control is where it lands from the first frame and takes a click there. */
  #app.booting #topleft,
  #app.booting #floorbar,
  #app.booting #viewdock,
  #app.booting #adminrail:not(.hidden){animation:lh-settle var(--t-440) var(--ease-arrive) backwards}
  #app.booting #topleft{animation-delay:var(--t-100)}
  #app.booting #adminrail:not(.hidden){animation-delay:var(--t-140)}
  #app.booting #viewdock{animation-delay:var(--t-settle)}
  #app.booting #floorbar{animation-delay:var(--t-panel)}
  @keyframes lh-settle{from{opacity:0;transform:translateY(6px) scale(.985);filter:blur(4px)}to{opacity:1;transform:none;filter:none}}
  /* The rail comes in from the edge it lives on, then writes its rooms down the
     list. A stagger is honest here and nowhere else in this venue: the rail IS
     a list, and the order it lands in is the order the rooms are in — which is
     the whole point of doing it. --i is a row's place from the top of the rail,
     counted across groups so the cascade does not restart at every heading.
     The delay is capped, so a venue with forty rooms still finishes arriving. */
  #app.booting #side{animation:lh-rail var(--t-420) var(--ease-arrive) backwards}
  @keyframes lh-rail{from{opacity:0;transform:translateX(14px)}to{opacity:1;transform:none}}
  #app.booting #side .brandbar,
  #app.booting #side .head,
  #app.booting #side .foot{animation:lh-settle var(--t-400) var(--ease-arrive) var(--t-tap) backwards}
  /* The delay is written onto the row by railTurn(), which reads it off the
     clock the venue opened on rather than off this repaint. */
  .grp.enter,.rm.enter{animation:lh-row var(--t-420) var(--ease-arrive) backwards}
  @keyframes lh-row{from{opacity:0;transform:translateY(7px);filter:blur(3px)}to{opacity:1;transform:none;filter:none}}
  /* Reduced motion keeps the ORDER and drops the travel. Where everything is
     was the reason for the sequence, so muting it to nothing would remove the
     meaning rather than the movement — it runs on opacity, quicker and tighter. */
  @media (prefers-reduced-motion: reduce){
    #app.booting #topleft,#app.booting #floorbar,#app.booting #viewdock,
    #app.booting #adminrail:not(.hidden),#app.booting #side,
    #app.booting #side .brandbar,#app.booting #side .head,
    #app.booting #side .foot{animation:lh-appear var(--t-ease) var(--linear) backwards;animation-delay:var(--t-40)}
    .grp.enter,.rm.enter{animation:lh-appear var(--t-panel) var(--linear) backwards}
    @keyframes lh-appear{from{opacity:0}to{opacity:1}}
  }
  #world{position:absolute;left:0;top:0;transform-origin:0 0;will-change:transform}
  /* --inv-z undoes the floor's zoom for the few things that are controls
     rather than scenery. A pencil is the same pencil at 25% and at 100%;
     scaling it with the room made it a speck across a big floor. */
  /* Weather in front of the camera, above the floor and below the chrome
     (#loading owns z-index 6). Sized to the pane, so a flake is a flake. */
  #atmos-screen{position:absolute;inset:0;z-index:var(--z-2);pointer-events:none}
  #world img.bg{display:block;pointer-events:none;-webkit-user-drag:none}
  /* v68 / Phase 3. No image backups — a background the Sweep has since
     garbage-collected renders as a neutral fill rather than a broken-image
     icon (venue.js clears the class on the next successful load). */
  #world img.bg.missing{background:var(--paper)}
  #world .bg-dim{position:absolute;left:0;top:0;pointer-events:none;background:var(--deep);opacity:0}
  /* The atmosphere's own layer, so its position in the stack is a fact of the
     floor rather than something the engine has to be told twice. */
  #world .atmos-layer{position:absolute;left:0;top:0;width:100%;height:100%;pointer-events:none}

  /* chips (top bar) */
  .chip{display:flex;align-items:center;gap:var(--s-5);height:48px;padding:0 var(--s-8);background:var(--glass);box-shadow:var(--ring);-webkit-backdrop-filter:var(--blur-12);backdrop-filter:var(--blur-12);-webkit-backdrop-filter:var(--blur-12);border-radius:var(--r);color:var(--fg-2);font:var(--fw-semibold) var(--fs-14) var(--ui);text-transform:none;letter-spacing:0;white-space:nowrap}
  .chip.r14{border-radius:var(--r)}
  .chip.icon{width:48px;padding:0;justify-content:center}
  .chip .brand{font:var(--fw-medium) var(--fs-16) var(--ui);text-transform:none;letter-spacing:0;color:var(--fg-2)}
  .chip .pill{font:var(--fw-semibold) var(--fs-16) var(--display);text-transform:none;letter-spacing:0;background:var(--paper-2);color:var(--chip-pill-bg);height:28px;padding:0 var(--s-4);border-radius:var(--r-5xl);display:grid;place-items:center}
  .chip .vsep{width:1px;height:20px;background:var(--line-2)}
  .chip .count{display:flex;align-items:center;gap:var(--s-3);text-transform:none;font:var(--fw-regular) var(--fs-16) var(--ui);color:var(--fg-3)}
  .chip .count svg{width:13px;height:13px;color:var(--fg-3)}
  .avatar{width:24px;height:24px;border-radius:50%;background:var(--plum);color:var(--white);display:grid;place-items:center;font:var(--fw-semibold) var(--fs-13) var(--ui);overflow:hidden;text-transform:none}
  .avatar img{width:100%;height:100%;object-fit:cover}
  #mobile-side{display:none;position:absolute;right:var(--s-6);top:var(--s-6);z-index:var(--z-dock);width:46px;height:46px;place-items:center;border-radius:var(--r);color:var(--hi);background:var(--glass);box-shadow:var(--ring);-webkit-backdrop-filter:var(--blur-12);backdrop-filter:var(--blur-12)}
  .chip:hover{background:var(--glass-solid)}

  /* The admin doors stand together on the left, vertical and out of the way of
     the room. They were scattered across a top-right menu that mostly repeated
     the dock and the view cluster. */
  #adminrail{position:absolute;left:var(--s-10);top:50%;transform:translateY(-50%);z-index:var(--z-dock);display:grid;gap:var(--s-2);padding:var(--s-3);border-radius:var(--pill)}
  #adminrail.hidden{display:none}
  #adminrail button{width:42px;height:42px;border-radius:50%;display:grid;place-items:center;color:var(--fg-3);position:relative;
    transition:background var(--t-base),color var(--t-base)}
  #adminrail button:hover{background:var(--hover);color:var(--hi)}
  #adminrail button.on,#adminrail button.addopen{background:var(--accent);color:var(--on-accent)}
  #adminrail button.hidden{display:none}
  #adminrail .i{width:19px;height:19px}
  /* v110. The label used to be this button's own ::after; it is now the one
     shared #tip system (wireTooltips, venue.js), which reads data-label the
     same way. */

  /* v138. #add-options replaces #addfly: the rail grows open in place rather
     than popping a separate panel beside it. `order:-1` puts it above
     #m-add visually no matter where it sits in the DOM (kept after #m-add
     there so Tab still reaches it right after the + ). The 0fr->1fr
     grid-template-rows trick is the same one .ppl .list uses below for an
     animated height; .inner is the real column so the outer box has
     something to collapse against, and it also carries the mobile-only
     overflow cap (media block further down) so a long list on a short
     screen cannot run past the top of the viewport. */
  /* v146. Munk: the spacing between the rail's buttons read as uneven. The
     cause: #adminrail's own `gap` (below) is inserted around EVERY grid row,
     #add-options included — and #add-options never goes away, it only
     collapses its own inner height to 0fr at rest, so the rail carried one
     full extra `gap` above #m-add that none of its other buttons had. v146's
     own fix put a negative margin-top on #add-options itself to cancel it —
     which does pull #add-options's OWN (already empty) box up into the
     padding, but a grid's row-gap is a property of the TRACKS, not of any
     one item's margin, so it never actually removed the gap #m-add's own
     track still opened below it. The audit (docs/ui-backlog.md) is what
     caught it: the pill's own top padding measured 10px, the bottom 6px —
     v146's fix looked right and simply didn't reach the actual gap.
     v167's real fix is the opposite direction: pull #m-add up instead,
     while there is nothing open above it to make room for (the :has()
     below), which does move the thing that is actually where the eye reads
     the gap — #add-options's own explicit height:0 (next rule) is a
     separate, smaller fix: its own resolved height only rounded to exactly
     0 with height set directly; grid-template-rows:0fr alone left a few px
     of residual on this rail even with overflow:hidden/min-height:0 set. */
  #add-options{order:-1;display:grid;grid-template-rows:0fr;opacity:0;margin-top:calc(-1 * var(--s-2));overflow:hidden;min-height:0;height:0;
    transition:grid-template-rows var(--t-panel) var(--ease-move),opacity var(--t-base) var(--ease),margin-top var(--t-panel) var(--ease-move)}
  /* v167. The actual fix for the uneven padding: while #add-options is
     closed, pull #m-add up by exactly one row-gap so the space above it
     matches #adminrail's own padding, the same as every other button's own
     spacing (and #r-bg's own bottom padding, unaffected either way). Once
     #add-options opens, this stops applying — #m-add drops back to its
     ordinary gap below the now-real list, which is the room it needs. */
  #adminrail:not(:has(#add-options.open)) #m-add{margin-top:calc(-1 * var(--s-2))}
  /* v167. The same closed list was still 52px WIDE, which set the rail's
     column: every button sat 6px from the left edge and 16px from the
     right. Closed, it takes no width; open, the column centres it. */
  #add-options:not(.open){width:0}
  #adminrail{justify-items:center}
  /* v146. The "pop": a slight overshoot on top of the height reveal above,
     scale+translate rather than height, so it reads as the options
     springing out of the + rather than just a panel unfolding. */
  #add-options .inner{display:grid;gap:var(--s-2);overflow:hidden;min-height:0;max-height:min(50vh,calc(100vh - 160px));overflow-y:auto;
    transform:scale(.92) translateY(6px);transition:transform var(--t-panel) var(--ease-pop)}
  #add-options.open{grid-template-rows:1fr;opacity:1;margin-top:0;height:auto}
  #add-options.open .inner{transform:none}
  @media (prefers-reduced-motion: reduce){#add-options,#add-options .inner{transition:none;transform:none}}

  /* ---------- floor chrome ----------------------------------------------
     Two clusters, two jobs. The dock is what YOU do (be heard, be seen, leave);
     the view cluster is what the MAP does (find me, zoom, help). They were one
     undifferentiated rail of eleven 20px icons in the sidebar, where the
     microphone — the only control that matters in a voice room — weighed the
     same as the zoom percentage. */
  .glasspill{background:var(--glass-pill);-webkit-backdrop-filter:var(--blur-20-sat);backdrop-filter:var(--blur-20-sat);-webkit-backdrop-filter:var(--blur-20-sat);
    box-shadow:var(--elev-glasspill)}
  /* the room you are standing in, named on the floor it belongs to */
  /* Top left is who you are and what you can set. The room name sits beside it,
     in the same row, so the corner reads as one thing rather than three. */
  #topleft{position:absolute;left:var(--s-10);top:var(--s-10);z-index:var(--z-bar);display:flex;align-items:center;gap:var(--s-5);max-width:calc(100% - 40px)}
  #youchip{display:flex;align-items:center;gap:var(--s-1);padding:var(--s-17);border-radius:var(--pill)}
  #youchip button{flex:none;width:38px;height:38px;border-radius:50%;display:grid;place-items:center;color:var(--fg-3);
    transition:background var(--t-base),color var(--t-base)}
  #youchip button:hover{background:var(--hover);color:var(--hi)}
  #youchip .i{width:18px;height:18px}
  /* v37. The button shows the mode you are in: a moon at night, a sun by day. */
  #m-theme .i-sun{display:none} :root[data-theme=light] #m-theme .i-sun{display:block} :root[data-theme=light] #m-theme .i-moon{display:none}
  #youchip #b-me{padding:0;overflow:hidden;color:var(--hi)}
  #youchip #b-me .avatar{width:34px;height:34px;border-radius:50%;display:grid;place-items:center;overflow:hidden;
    background:var(--hover);font:var(--fw-semibold) var(--fs-13) var(--ui);box-shadow:var(--ring-subtle)}
  #youchip #b-me .avatar .av{width:100%;height:100%}
  #roomchip{position:relative;z-index:var(--z-hud);display:flex;align-items:center;
    padding:var(--s-20) var(--s-10);border-radius:var(--pill);max-width:min(40cqi,460px)}
  #roomchip .rc{flex:none;margin-left:var(--s-5);padding:var(--s-21) var(--s-18);border-radius:var(--r-999);font:var(--fw-semibold) var(--fs-12)/var(--lh-1) var(--ui);color:var(--fg-2);background:var(--line-2);font-variant-numeric:tabular-nums}
  #roomchip .rc:empty{display:none}
  #hint{position:absolute;left:50%;bottom:var(--s-22);transform:translateX(-50%);z-index:var(--z-hud);max-width:min(520px,calc(100% - 24px));display:flex;gap:var(--s-6);align-items:center;
    padding:var(--s-6) var(--s-7) var(--s-6) var(--s-8);border-radius:var(--r-3xl);background:var(--glass-solid);box-shadow:var(--elev-hint);font:var(--fw-medium) var(--fs-14)/var(--lh-1-4) var(--ui);color:var(--hi)}
  #hint button{flex:none;width:30px;height:30px;border-radius:50%;display:grid;place-items:center;color:var(--fg-3)}
  #hint button:hover{background:var(--hover);color:var(--hi)}
  /* v47. The orientation. Five steps over the real venue, in the arrivals room,
     the first time somebody lands. The scrim never takes the pointer — every
     step is done on the control itself, not on a picture of one, so the ring
     is a hole in the dark rather than a lid over it. */
  #orient{position:fixed;inset:0;z-index:var(--z-orient);pointer-events:none;font:var(--fw-regular) var(--fs-14)/var(--lh-1-5) var(--ui)}
  #orient .ring{position:absolute;left:50%;top:50%;width:0;height:0;border-radius:var(--r-4xl);
    box-shadow:var(--ring-focus-scrim);
    transition:left var(--t-travel) var(--ease-move),top var(--t-travel) var(--ease-move),width var(--t-travel) var(--ease-move),height var(--t-travel) var(--ease-move)}
  /* A step with nothing to point at still darkens the venue: the same scrim
     with no hole and no bright edge, drawn off-screen so nothing shows. */
  #orient .ring.blank{box-shadow:var(--scrim-orient);left:calc(var(--s-12) * -1);top:calc(var(--s-12) * -1);width:0;height:0}
  #orient .card{position:absolute;left:50%;top:50%;width:min(360px,calc(100vw - 32px));pointer-events:auto;
    padding:var(--s-9) var(--s-9) var(--s-8);border-radius:var(--r-16);background:var(--glass-solid);color:var(--hi);
    box-shadow:var(--elev-orient);
    transition:left var(--t-travel) var(--ease-move),top var(--t-travel) var(--ease-move)}
  #orient .o-top{display:flex;align-items:center;gap:var(--s-5);margin-bottom:var(--s-6)}
  #orient .o-count{font:var(--fw-semibold) var(--fs-11)/var(--lh-1) var(--ui);letter-spacing:var(--ls-009em);text-transform:uppercase;color:var(--fg-3)}
  #orient .o-dots{display:flex;gap:var(--s-17);margin-left:auto}
  #orient .o-dots i{width:6px;height:6px;border-radius:50%;background:var(--line-2)}
  #orient .o-dots i.on{background:var(--hi)}
  #orient .o-dots i.past{background:var(--fg-3)}
  #orient h3{font:var(--fw-regular) var(--fs-21)/var(--lh-1-2) var(--brand);letter-spacing:var(--ls-02px);color:var(--hi);margin:0 0 var(--s-3)}
  #orient p{margin:0;color:var(--fg-2)}
  /* The step that asks for something says so, and says when it has happened.
     The tick replaces the ask in place, so the card never grows a line. */
  #orient .o-ask{display:flex;align-items:center;gap:var(--s-4);margin-top:var(--s-6);padding:var(--s-18) var(--s-20);border-radius:var(--r-xl);
    background:var(--field);box-shadow:var(--ring);font:var(--fw-semibold) var(--fs-12-5)/var(--lh-1-3) var(--ui);color:var(--fg-2)}
  #orient .o-ask.done{color:var(--hi)}
  #orient .o-ask svg{flex:none;width:15px;height:15px;opacity:.7}
  #orient .o-ask.done svg{opacity:1}
  #orient .o-foot{display:flex;align-items:center;gap:var(--s-4);margin-top:var(--s-8)}
  /* v126. Rings added on .o-ask/.o-foot button: --field == the card's own
     surface now. */
  #orient .o-foot button{height:36px;padding:0 var(--s-23);border-radius:var(--r-xl);font:var(--fw-semibold) var(--fs-13) var(--ui);color:var(--fg-2);background:var(--field);box-shadow:var(--ring)}
  #orient .o-foot button:hover{color:var(--hi);background:var(--hover)}
  #orient .o-foot button.go{margin-left:auto;background:var(--solid);color:var(--on-solid)}
  #orient .o-foot button.go:hover{background:var(--solid);opacity:.9}
  /* v129. Skip is plain text: cancel the ring/background .o-foot button sets. */
  #orient .o-foot .o-skip{margin-left:auto;font:var(--fw-medium) var(--fs-12-5) var(--ui);color:var(--fg-3);background:none;box-shadow:none;padding:0 var(--s-2);height:auto}
  #orient .o-foot .o-skip + .go{margin-left:0}
  #orient .o-foot button:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
  /* v166. The card's placement (placeOrient, venue.js) now keeps clear of
     whatever a step's own `ask` opens beside its target, but the popover
     itself still needs to outrank #orient's scrim/card on the z-axis too —
     #pop (step 3's own status menu) and #side's mobile "mobile-open" sheet
     (step 4's chat pane on a narrow screen) both normally sit well under
     --z-orient, so the scrim would dim them and a card that ends up
     overlapping anyway would still win the click. Scoped to while #orient is
     actually open, so ordinary (non-tour) stacking — #pop under toast, the
     mobile sidebar under nothing in particular — is unchanged. */
  body:has(#orient:not(.hidden)) #pop{z-index:calc(var(--z-orient) + 1)}
  /* The mobile chat sheet (#side.mobile-open) is already position:fixed —
     only its z needs raising. The desktop sidebar has no position of its own
     at all, so giving it one (relative, no offsets — it does not move) is
     what lets a z-index apply; media-scoped so it never fights the >=901px
     rule that resets a stray .mobile-open class back to static/auto (further
     down) — this still wins there on specificity, but there is no reason to
     rely on that when the breakpoint already says which #side this is. */
  body:has(#orient:not(.hidden)) #side.mobile-open{z-index:calc(var(--z-orient) + 1)}
  @media(min-width:901px){ body:has(#orient:not(.hidden)) #side{position:relative;z-index:calc(var(--z-orient) + 1)} }
  @media (prefers-reduced-motion:reduce){#orient .ring,#orient .card{transition:none}}
  /* Under the sidebar breakpoint the card is a sheet at the bottom: there is no
     room beside a highlighted control on a phone, and a card that floats over
     the thing it is describing hides it. */
  @media (max-width:900px){
    #orient .card{left:var(--s-6)!important;right:var(--s-6);top:auto!important;bottom:var(--s-6);width:auto}
  }
  #pop .pop-avail{display:grid;grid-template-columns:1fr;gap:var(--s-3);padding-top:var(--s-6);border-top:1px solid var(--line)}
  /* v126. Ring added: --field == the popover's own surface now. */
  #pop .pop-avail button{height:34px;border-radius:var(--r-xl);font:var(--fw-semibold) var(--fs-12) var(--ui);color:var(--fg-2);background:var(--field);box-shadow:var(--ring)}
  #pop .pop-avail button.on{background:var(--solid);color:var(--on-solid)}
  #roomchip .rn{font:var(--fw-regular) var(--fs-21)/var(--lh-1-1) var(--brand);letter-spacing:var(--ls-02px);color:var(--hi);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}

  /* The floor's bottom row is one grid, so the tip, the dock and the view
     cluster cannot land on top of each other at any width — they used to be
     three absolutely-positioned things hoping to miss. */
  #floorbar{position:absolute;left:0;right:0;bottom:0;z-index:var(--z-dock);display:grid;
    grid-template-columns:1fr auto 1fr;align-items:end;row-gap:var(--s-6);column-gap:var(--s-8);
    padding:0 var(--s-10) var(--s-14);pointer-events:none}
  #floorbar > *{pointer-events:auto}
  #floorbar .dockwrap{grid-area:2/1/3/4;position:relative;display:grid;justify-items:center}
  #dock{position:relative;z-index:var(--z-dock);
    display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:var(--s-3);padding:var(--s-4);border-radius:var(--pill);max-width:calc(100% - 40px)}
  /* Wrapping, not overflowing: the buttons do not shrink, so a narrow floor
     used to push Leave out past the pill's own rounded edge. */
  #dock .dsep{flex:none;width:1px;height:26px;margin:0 var(--s-3);background:var(--line-2)}
  #dock button{flex:none;width:46px;height:46px;border-radius:50%;display:grid;place-items:center;color:var(--fg-2);position:relative;
    transition:background var(--t-base),color var(--t-base),transform var(--t-base) var(--ease-press)}
  #dock button:hover:not(:disabled){background:var(--hover);color:var(--hi)}
  #dock button:active:not(:disabled){transform:scale(.94)}
  #dock button:disabled{opacity:.32;cursor:default}
  #dock button.on{background:var(--accent);color:var(--on-accent)}
  /* The microphone is the primary control, so it is the only one wearing a
     label and the only white surface on the floor. */
  #dock #d-mic{width:auto;height:52px;padding:0 var(--s-16) 0 var(--s-24);border-radius:var(--pill);gap:var(--s-20);
    display:flex;align-items:center;background:var(--solid);color:var(--on-solid);font:var(--fw-semibold) var(--fs-15) var(--ui)}
  #dock #d-mic .i{width:21px;height:21px;stroke-width:1.9}
  #dock #d-mic:hover{background:var(--solid);color:var(--on-solid)}
  /* v115. Solid gold, not a wash: on-gold dark text needs a background it
     can actually sit on, and the wash (a translucent gold over a video
     floor) can't guarantee that the way the old red wash's light text could. */
  #dock #d-mic.off{background:var(--accent);color:var(--on-accent);box-shadow:none}
  #dock #d-mic.off:hover{background:var(--accent-deep);color:var(--on-accent)}
  /* One authored moment: the pill breathes with your own voice while you talk. */
  #dock #d-mic .lvl{flex:none;width:3px;height:20px;border-radius:var(--r-xs);background:currentColor;opacity:.22;
    transform:scaleY(calc(.25 + var(--lvl,0) * .75));transform-origin:center;transition:transform var(--t-90) var(--linear),opacity var(--t-settle)}
  #dock #d-mic.talking .lvl{opacity:.9}
  #dock #d-mic.off .lvl{display:none}
  /* v34. Blocked / no mic / reconnecting: the same pill, dimmed, with the word. */
  #dock #d-mic.blocked{background:var(--line-2);color:var(--fg-2);box-shadow:var(--ring)}
  #dock #d-mic:disabled{opacity:.6;cursor:default}
  /* Speaker off says so with a crossed-out speaker. A red dot reads as an
     alert — something you have to deal with — and it is a setting. */
  #dock #d-spk.off{color:var(--fg-3)}
  #dock #d-leave{color:var(--fg-2)}
  #dock #d-leave:hover{background:var(--hover);color:var(--hi)}

  /* The view cluster is not one of your controls, so it does not share the
     dock's row — a centred dock and a right-hand cluster cannot both have
     the floor's full width. Top right, opposite who you are. */
  /* v32. The view cluster lives in the header row, after the room name. */
  #viewdock{position:static;margin-left:auto;flex:none;z-index:var(--z-dock);display:flex;align-items:center;gap:var(--s-1);padding:var(--s-3);border-radius:var(--pill)}
  #viewdock button{flex:none;width:38px;height:38px;border-radius:50%;display:grid;place-items:center;color:var(--fg-3);transition:background var(--t-base),color var(--t-base)}
  #viewdock button:hover{background:var(--hover);color:var(--hi)}
  #viewdock button.on{background:var(--accent);color:var(--on-accent)}
  #viewdock .i{width:17px;height:17px}
  #viewdock #z-pct{width:auto;padding:0 var(--s-4);border-radius:var(--pill);font:var(--fw-semibold) var(--fs-12) var(--ui);color:var(--fg-3);font-variant-numeric:tabular-nums}
  #viewdock .vsep{flex:none;width:1px;height:20px;margin:0 var(--s-17);background:var(--line-2)}
  .hcount{position:absolute;right:0;top:0;min-width:16px;height:16px;padding:0 var(--s-21);border-radius:var(--r-lg);background:var(--accent);color:var(--on-accent);font:var(--fw-bold) var(--fs-10) var(--ui);display:grid;place-items:center}
  /* v42. The preview notice was centred on top of the header row and landed
     straight on the room chip and the view cluster. It is a member of that
     row now: it shrinks before the controls do, and its sentence gives way to
     two words when the row runs out of width. */
  #preview-bar{position:static;flex:0 1 auto;min-width:0;z-index:var(--z-bar);display:flex;align-items:center;gap:var(--s-6);padding:var(--s-4) var(--s-5) var(--s-4) var(--s-8);border-radius:var(--pill);font:var(--fw-medium) var(--fs-13) var(--ui);color:var(--white);background:var(--glass);box-shadow:var(--ring);-webkit-backdrop-filter:var(--blur-12);backdrop-filter:var(--blur-12)}
  #preview-bar > span{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  #preview-bar button{flex:none}
  #preview-bar .short{display:none}
  @container stage (max-width:1240px){ #preview-bar .long{display:none} #preview-bar .short{display:block} }
  /* v113. The switcher — the same four choices as small chips, so trying a
     different one is a click, never a trip back through the menu. Overrides
     the ellipsis rule above: this span holds buttons, not text. */
  #preview-bar .pv-switch{display:flex;gap:var(--s-3);overflow:visible;flex:none}
  .pv-chip{padding:var(--s-21) var(--s-5);border-radius:var(--pill);font:var(--fw-semibold) var(--fs-11) var(--ui);color:var(--white);background:var(--on-dark-16);box-shadow:var(--ring-chip)}
  .pv-chip.on{background:var(--white);color:var(--ink)}
  @container stage (max-width:1240px){ #preview-bar .pv-switch{display:none} }
  /* On a phone the row has no width to give: the notice takes a line of its
     own under the chips rather than pushing the room name off the screen. */
  @media(max-width:900px){
    #topleft{flex-wrap:wrap;row-gap:var(--s-4)}
    #preview-bar{order:9;flex:1 0 100%;justify-content:space-between}
  }
  #preview-bar button{padding:var(--s-17) var(--s-6);border-radius:var(--pill);font:var(--fw-semibold) var(--fs-12) var(--ui);color:var(--ink);background:var(--white)}
  .menu button{position:relative}

  /* One responsive message region. Connection, save, and transient messages
     stack instead of occupying the same corners or covering the phone header. */
  #status-stack{position:absolute;right:var(--s-10);top:var(--s-10);z-index:var(--z-toast);display:grid;justify-items:end;gap:var(--s-3);width:min(calc(100% - 40px),46ch);max-height:calc(100% - 120px);overflow-y:auto;overscroll-behavior:contain;pointer-events:none}
  #conn{position:static;max-width:100%;font:var(--fw-medium) var(--fs-13) var(--ui);color:var(--hi);background:var(--glass-solid);box-shadow:var(--ring);padding:var(--s-25) var(--s-7);border-radius:var(--r);text-align:left}
  #conn.removed{background:var(--coral);color:var(--on-accent)}
  #savestate{position:static;max-width:100%;font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--hi);background:var(--glass-solid);box-shadow:var(--ring);padding:var(--s-17) var(--s-5);border-radius:var(--r);opacity:.9;text-align:left}
  #savestate.failed{background:var(--coral);color:var(--on-accent)}
  /* The venue's only way of telling you something, so it is the loudest thing
     on the floor rather than the quietest. It was a dark chip on a dark
     painting, low over the dock, where a room full of candles and snow hid it —
     a full room and a moved arrival both went unread. Now it wears the same
     white-on-charcoal the primary buttons wear (inverted with the theme by
     --solid/--on-solid), and sits top right of the FLOOR, clear of the
     sidebar: the one corner the venue puts nothing else in.
     `#toast` is a child of #stage, which ends where the sidebar begins, so
     `right` here is already outside it. */
  /* On the floor's grid, not beside it. Everything the venue puts over a room
     starts at 20 from the edge and the top row runs 20-70: #topleft, #viewdock
     and the left rail all do. This sat at top:52, lined up with nothing, which
     is what reads as an element bolted on afterwards. It takes the top row's
     right end, and #savestate - the only thing that was at 14 - comes onto the
     same gutter underneath it. */
  #toast{position:static;max-width:100%;font:var(--fw-semibold) var(--fs-14)/var(--lh-1-45) var(--ui);
    /* Not --solid/--on-solid. Those follow the theme, and the theme does not
       reach the floor: the room's picture is the same painted, mostly dark
       scene in light mode as in dark, so the light theme turned this into a
       charcoal chip on a charcoal cathedral — the exact thing being fixed.
       White always, charcoal text (15.9:1), with a hairline so it still holds
       an edge over snow. */
    color:var(--ink);background:var(--white);
    box-shadow:var(--elev-toast);
    /* 13/20 puts a one-line toast at 46px, the height of the room chip it is
       now level with, on that row's 20px horizontal padding. The radius is
       half that height: a stadium while it is one line, like every other
       chip on this row, and a well-rounded card once it wraps. */
    padding:var(--s-26) var(--s-10);border-radius:var(--r-23);opacity:0;pointer-events:none;
    /* 46ch, not 70% of the floor. At 70% the longest of these ran a 643px line
       across the top of the room; on a phone the same rule squeezed it to 188px
       and five stacked lines over your own face. */
    text-align:left;
    transform:translateY(-8px);transition:opacity var(--t-panel),transform var(--t-panel) var(--ease-press)}
  #toast.on{opacity:1;transform:none}
  /* v142. A toast that carries a choice (card trade Accept/Decline, "Block
     too?" after a report) must be pressable and readable: the toast itself
     ignores the pointer so it never blocks the floor, and .btn.plain is
     white-on-transparent, which vanished on this always-white chip. */
  #toast.on button{pointer-events:auto}
  #toast .btn{margin:var(--s-2) 0 0 var(--s-2)}
  #toast .btn.plain{color:var(--ink);box-shadow:var(--ring-toast-btn)}
  /* Wide only. The toast now holds the top row's right end, so the other
     top-right chip moves onto the same 20px gutter beneath it instead of
     sharing the corner. Narrow layouts keep their own stacking: there the
     toast sits below the header band, not in it. */
  @media (prefers-reduced-motion:reduce){#toast{transform:none;transition:opacity var(--t-panel)}}
  /* v135. "How it works" moved off the sidebar header — #floor-help is a
     child of #stage (same as #toast above), so `right` already sits
     outside the sidebar; bottom-right, the 20px gutter every floor corner
     uses, clear of the centred #dock (docs/ui-backlog.md's "Help button"). */
  #floor-help{position:absolute;right:var(--s-10);bottom:var(--s-10);z-index:var(--z-dock);display:flex;align-items:center;gap:var(--s-3);padding:0 var(--s-7);height:34px;border-radius:var(--pill);color:var(--fg-2);font:var(--fw-semibold) var(--fs-13) var(--ui);white-space:nowrap}
  #floor-help:hover{color:var(--hi)}
  #floor-help .i{width:16px;height:16px}
  #shout{position:absolute;left:50%;top:var(--s-28);transform:translateX(-50%);z-index:var(--z-floor-ui);display:flex;align-items:center;gap:var(--s-5);background:var(--accent);color:var(--on-accent);padding:var(--s-5) var(--s-9);border-radius:var(--pill);font:var(--fw-bold) var(--fs-19) var(--ui)}
  /* v64. Venue-wide, so it reads differently from #shout (a ROOM's own
     megaphone, in the accent colour): the glass panel every modal-adjacent
     banner already uses (#hint), and it carries its own dismiss rather than
     fading like a toast — a con-wide notice is read, not glanced at. Sits
     above #shout (higher z-index) on the rare visit both are true at once. */
  #annc{position:absolute;left:50%;top:var(--s-28);transform:translateX(-50%);z-index:var(--z-hud);display:flex;align-items:center;gap:var(--s-6);
    max-width:min(calc(100% - 40px),64ch);padding:var(--s-6) var(--s-6) var(--s-6) var(--s-9);
    background:var(--glass-solid);box-shadow:var(--elev-hint);border-radius:var(--r-3xl);
    font:var(--fw-medium) var(--fs-14)/var(--lh-1-4) var(--ui);color:var(--hi)}
  #annc.hidden{display:none}
  #annc>.i{flex:none;width:18px;height:18px;color:var(--accent)}
  #annc span{flex:1;min-width:0}
  #annc button{flex:none;width:30px;height:30px;border-radius:50%;display:grid;place-items:center;color:var(--fg-3)}
  #annc button:hover{background:var(--hover);color:var(--hi)}
  /* The tip sits in the row with the name, not down on the floor: it is the
     first thing anyone reads, and at the bottom it was under the eye and beside
     the controls it was explaining. It shrinks to fit whatever the room name
     left behind, and it is the first thing dropped when the row runs out. */
  #tip{position:relative;z-index:var(--z-floor-ui);flex:0 1 auto;min-width:0;max-width:330px;
    background:var(--glass-solid);box-shadow:var(--ring);border-radius:var(--r);
    padding:var(--s-18) var(--s-29) var(--s-18) var(--s-26);font:var(--fw-regular) var(--fs-12-5)/var(--lh-1-4) var(--ui);color:var(--fg-2)}
  #tip button{position:absolute;right:var(--s-25);top:var(--s-25);color:var(--fg-3)}
  @container stage (max-width:900px){ #topleft #tip{display:none} }

  /* Reactions: they leave from your own face, drift up with a little sway, and
     are gone. Nothing to dismiss, nothing that stacks up on the floor. */
  .react{position:absolute;z-index:var(--z-4);pointer-events:none;font-size:var(--fs-34);line-height:var(--lh-1);
    filter:var(--drop-react);animation:lh-react var(--t-1900) var(--ease-move) forwards}
  /* <i> is italic, and an emoji has no italic face — the browser synthesises
     one by skewing the glyph, which is why every reaction leaned. */
  .react i{display:block;font-style:normal;animation:lh-sway var(--t-1050) var(--ease-in-out) infinite alternate}
  @keyframes lh-react{
    0%{transform:translate(-50%,-50%) scale(.3);opacity:0}
    12%{transform:translate(-50%,-60%) scale(1.12);opacity:1}
    22%{transform:translate(-50%,-72%) scale(1)}
    72%{opacity:1}
    100%{transform:translate(-50%,-215%) scale(.86);opacity:0}}
  @keyframes lh-sway{from{transform:translateX(-11px) rotate(-9deg)}to{transform:translateX(11px) rotate(9deg)}}
  @media (prefers-reduced-motion: reduce){
    .react{animation:lh-react-quiet var(--t-1400) var(--ease-out) forwards}.react i{animation:none}
    @keyframes lh-react-quiet{0%{transform:translate(-50%,-60%) scale(.8);opacity:0}20%{opacity:1;transform:translate(-50%,-70%) scale(1)}100%{transform:translate(-50%,-130%);opacity:0}}}
  /* the tray sits above the dock, anchored to the button that opens it */
  #reacts{position:absolute;bottom:calc(100% + var(--s-5));left:50%;transform:translateX(-50%);z-index:var(--z-bar);display:flex;gap:var(--s-1);padding:var(--s-25);border-radius:var(--pill)}
  #reacts.hidden{display:none}
  #reacts button{width:44px;height:44px;border-radius:50%;font-size:var(--fs-23);line-height:var(--lh-1);display:grid;place-items:center;
    transition:background var(--t-base),transform var(--t-base) var(--ease-press)}
  #reacts button:hover{background:var(--hover);transform:translateY(-3px) scale(1.12)}
  /* On a phone the tray clears the view cluster instead of covering it. */
  @media(max-width:900px){#reacts{max-width:calc(100vw - 24px);overflow-x:auto}#reacts button{width:40px;height:40px;flex:none}}

  /* ---------- bubbles ---------- */
  .bubble{position:absolute;width:var(--d,80px);height:var(--d,80px);margin:calc(var(--d,80px)/-2) 0 0 calc(var(--d,80px)/-2);border-radius:50%;cursor:pointer;transition:opacity var(--t-panel),filter var(--t-panel);z-index:var(--z-2)}
  /* How far your voice carries, shown while you are moving yourself. Everyone
     inside the circle can hear you; past its edge you are silent to them. */
  .bubble .range{position:absolute;left:50%;top:50%;width:var(--range,1400px);height:var(--range,1400px);
    margin:0;transform:translate(-50%,-50%);border-radius:50%;pointer-events:none;z-index:var(--z-under);
    background:var(--on-dark-10);
    box-shadow:var(--ring-range);
    opacity:0;transition:opacity var(--t-ease) var(--ease)}
  .bubble.drag .range{opacity:1}
  @media (prefers-reduced-motion: reduce){.bubble .range{transition:none}}
  .bubble .disc{position:absolute;inset:0;border-radius:50%;overflow:hidden;background:var(--plum);display:grid;place-items:center;font:var(--fw-semibold) var(--fs-40) var(--ui);color:var(--white);box-shadow:var(--ring-disc)}
  .bubble .disc video{width:100%;height:100%;object-fit:cover}
  .bubble.me .disc video{transform:scaleX(-1)}
  /* v115. Speaking ring and pulse: gold, not red. */
  .bubble.speaking .disc{box-shadow:var(--ring-speaking);animation:lh-pulse var(--t-1150) var(--ease-out) infinite}
  @keyframes lh-pulse{0%{box-shadow:var(--ring-pulse-mid)}70%{box-shadow:var(--ring-pulse-end)}100%{box-shadow:var(--ring-pulse-end)}}
  @media (prefers-reduced-motion: reduce){.bubble.speaking .disc{animation:none}}
  .bubble.staff{--d:104px}
  .bubble .ring{position:absolute;inset:calc(var(--s-7) * -1);border-radius:50%;border:3px solid color-mix(in srgb,var(--white) calc(var(--vol,0) * 100%),transparent);pointer-events:none;transition:border-color var(--t-ease)}
  .bubble .badges{position:absolute;left:50%;top:100%;transform:translate(-50%,-7px);display:flex;gap:var(--s-3)}
  .bubble .badges span{width:28px;height:28px;border-radius:50%;background:var(--ink);display:grid;place-items:center;color:var(--coral-icon);
    box-shadow:var(--shadow-badge)}
  .bubble .badges span.on{color:var(--white)}
  .bubble .badges svg{width:15px;height:15px}
  /* A rank is readable before you click it: a chip on the shoulder of the face.
     Gold shield for the host, green sword for a Knight — both staff, both able
     to act on you. An Author wears a plum pen: a different MARK, not a different
     colour of the same one, because they are not staff and the chip should not
     read as a rank of one. White on all three clears 3:1 as a graphical object
     (measured: 6.39 green, 6.59 plum, 3.46 gold). v57.2 retired the moderator's
     blue with the rank itself. */
  .bubble .shield{position:absolute;left:calc(var(--s-2) * -1);top:calc(var(--s-2) * -1);width:26px;height:26px;border-radius:50%;display:grid;place-items:center;
    color:var(--white);background:var(--knight-green);box-shadow:var(--shadow-shield);z-index:var(--z-2)}
  .bubble .shield.host{background:var(--host-gold)}
  .bubble .shield.author{background:var(--author-plum)}
  .bubble .shield svg{width:15px;height:15px}
  /* v33. Availability, a small pill under the name. */
  .bubble .avail{position:absolute;left:50%;top:100%;transform:translate(-50%,34px);white-space:nowrap;font:var(--fw-semibold) var(--fs-11)/var(--lh-1) var(--ui);
    padding:var(--s-17) var(--s-18);border-radius:var(--r-999);background:var(--glass-solid);color:var(--hi);box-shadow:var(--ring);pointer-events:none}
  .bubble .avail.a3{color:var(--fg-3)}
  .bubble .name{position:absolute;left:50%;top:100%;transform:translate(-50%,14px);white-space:nowrap;font:var(--fw-semibold) var(--fs-13) var(--ui);color:var(--white);text-shadow:var(--text-shadow-name)}
  .bubble.me{cursor:grab;z-index:var(--z-3)} .bubble.me.drag{cursor:grabbing}
  /* What was just said, over the head that said it. Scaled by --inv-z so the
     words stay 15px whether the floor is at 40% or 200%; the pointer is a
     rotated square. It never takes the click, the avatar under it does. */
  .bubble .say{position:absolute;left:50%;bottom:calc(100% + var(--s-5));transform:translateX(-50%) scale(var(--inv-z,1));transform-origin:50% 100%;
    max-width:240px;width:max-content;padding:var(--s-4) var(--s-6);border-radius:var(--r-3xl);background:var(--on-dark-96);color:var(--dusk);
    font:var(--fw-medium) var(--fs-15)/var(--lh-1-35) var(--ui);white-space:normal;overflow-wrap:anywhere;text-align:center;box-shadow:var(--elev-say);
    pointer-events:none;z-index:var(--z-4);animation:lh-say var(--t-settle) var(--ease-out)}
  .bubble .say::after{content:'';position:absolute;left:50%;bottom:calc(var(--s-3) * -1);width:12px;height:12px;background:inherit;transform:translateX(-50%) rotate(45deg);border-radius:var(--r-xs)}
  .bubble .say.fading{opacity:0;transition:opacity var(--t-400)}
  @keyframes lh-say{from{opacity:0;translate:0 6px}to{opacity:1;translate:0 0}}
  @media (prefers-reduced-motion: reduce){.bubble .say{animation:none}.bubble .say.fading{transition:none}}
  .bubble .tag{position:absolute;right:calc(var(--s-5) * -1);top:calc(var(--s-5) * -1);display:grid;place-items:center;min-width:34px;height:34px;background:var(--violet);color:var(--white);font:var(--fw-bold) var(--fs-19)/var(--lh-1) var(--ui);padding:0 var(--s-3);border-radius:var(--r-999);border:3px solid var(--ink);box-shadow:var(--elev-tag)}
  .share{position:absolute;background:var(--black);border:3px solid var(--white);border-radius:var(--r-2xl);overflow:hidden;z-index:var(--z-2);width:480px;aspect-ratio:16/9}
  .share video{width:100%;height:100%;object-fit:contain}
  .share .cap{position:absolute;left:var(--s-4);top:var(--s-4);font:var(--fw-semibold) var(--fs-12) var(--ui);background:var(--glass);padding:var(--s-2) var(--s-5);border-radius:var(--r-999)}
  .youpin{position:absolute;width:64px;height:64px;margin:calc(var(--s-30) * -1) 0 0 calc(var(--s-30) * -1);border-radius:50%;background:var(--pin);opacity:.5;display:grid;place-items:center;color:var(--white);pointer-events:none;z-index:var(--z-1)}

  /* pins */
  .pin{position:absolute;z-index:var(--z-1);border-radius:var(--r);overflow:hidden;background:var(--paper);box-shadow:var(--elev-pin);color:var(--ink-2);font:var(--fw-regular) var(--fs-15)/var(--lh-1-5) var(--display)}
  /* A note is paper on a dark floor, so it is light whatever the background is.
     Long notes scroll inside the note rather than growing it - the size an admin
     set is the size it occupies on the floor. */
  .pin.note{background:var(--note-bg,#F3E2AE);color:var(--note-ink);display:grid;grid-template-rows:auto minmax(0,1fr);
    font:var(--fw-regular) var(--fs-15)/var(--lh-1-55) var(--ui);box-shadow:var(--elev-note)}
  .pin.note h4{margin:0;padding:var(--s-7) var(--s-9) 0;font:var(--fw-bold) var(--fs-16)/var(--lh-1-3) var(--ui);color:var(--note-ink-strong)}
  .pin.note h4:empty{padding:0}
  .editing-pins .pin.note h4:empty{padding:var(--s-7) var(--s-9) 0;min-height:1em}
  .pin.link .lktxt{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  .pin.note .body{min-height:0;padding:var(--s-7) var(--s-9) var(--s-8);margin:0;overflow:auto;white-space:pre-wrap;overflow-wrap:anywhere;
    scrollbar-width:thin;scrollbar-color:var(--shade-30) transparent}
  .pin.note .body::-webkit-scrollbar{width:8px}
  .pin.note .body::-webkit-scrollbar-thumb{background:var(--shade-28);border-radius:var(--r-lg);border:2px solid transparent;background-clip:padding-box}
  .pin.note.has-title .body{padding-top:var(--s-4)}
  .notepick{display:flex;gap:var(--s-5);flex-wrap:wrap}
  .notepick button{width:38px;height:38px;border-radius:50%;box-shadow:var(--ring)}
  .notepick button[aria-pressed=true]{outline:2px solid var(--white);outline-offset:3px}
  .pin.link{display:flex;align-items:center;gap:var(--s-5);padding:0 var(--s-9);font-weight:var(--fw-semibold);cursor:pointer;text-decoration:none;color:var(--ink-2)}
  .pin.link:hover{background:var(--paper-2)}
  .pin.image img{width:100%;height:100%;object-fit:cover;display:block}
  /* v68 / Phase 3. No image backups — a pin whose src the Sweep has since
     garbage-collected (venue.js removes the broken <img> itself) shows a
     neutral fill rather than empty space or a broken-image icon. */
  /* v126. Ring added: --paper-2 == the pin surface now, so the missing-image
     fallback needs its own edge to read as a placeholder. */
  .pin.image.missing{background:var(--paper-2);box-shadow:var(--ring)}
  .pin.image.missing::after{content:'';position:absolute;inset:22%;border-radius:var(--r);background:var(--ink-2);opacity:.14}
  .pin iframe{width:100%;height:100%;border:0;display:block}
  /* v68 / Phase 3. Onload animation — a pin fades and scales in once, on
     room entry only (venue.js's pin-enter class, added only inside
     enterRoom, never by redrawPins). The delay itself is written onto the
     element inline (pinStaggerDelayMs); this only supplies the motion. */
  .pin.pin-enter{animation:lh-pin-in var(--t-360) var(--ease-arrive) backwards}
  @keyframes lh-pin-in{from{opacity:0;transform:scale(.92) translateY(6px)}to{opacity:1;transform:none}}
  @media (prefers-reduced-motion: reduce){.pin.pin-enter{animation:none;opacity:1;transform:none}}
  /* Anything placed can be opened by the person who may edit it - a pencil on
     the thing itself, rather than a list three modals away. */
  /* Same corner on every kind of thing, so it is one place to look. */
  .pin .pedit{position:absolute;right:calc(var(--s-4) * var(--inv-z,1));top:calc(var(--s-4) * var(--inv-z,1));width:30px;height:30px;display:none;place-items:center;border-radius:50%;
    transform:scale(var(--inv-z,1));transform-origin:100% 0;
    color:var(--white);background:var(--shade-55);box-shadow:var(--ring);-webkit-backdrop-filter:var(--blur-8);backdrop-filter:var(--blur-8);z-index:var(--z-3)}
  .pin .pedit svg{width:15px;height:15px}
  .pin .pedit:hover{background:var(--accent)}
  /* On hover of the thing itself, on every kind of thing. It used to sit
     there permanently on four pin types, which read as clutter on a busy
     floor; and while arranging it stays put, because that is the mode where
     you are working on the items rather than looking at the room. */
  /* v47. Somebody else's, and not open for anyone to edit: no pencil, no move
     frame, no grip. It is a thing to read. */
  .pin.locked .pedit,.pin.locked .mv,.pin.locked .mvtag,.pin.locked .grip{display:none!important}
  #world.editing-pins .pin.locked{outline:none;cursor:default}
  body.can-edit-content .pin:hover .pedit,
  body.can-edit-content .pin:focus-within .pedit,
  body.can-edit-content #world.editing-pins .pin .pedit{display:grid}

  /* v112. A privacy square. It carries the shared .pin class (venue.js's
     huddleAreaEl) so the arrival animation above, the arrange-mode drag band
     and corner grip further down, and the .locked rule just above all apply
     unchanged — only the shape it draws and the fact that it holds no words
     of its own are different. v120: drawn as a rounded square, matching the
     square membership test currentSquareArea now reads (|dx|<=a.r &&
     |dy|<=a.r) instead of the old Math.hypot(...) <= a.r circle, so what is
     on screen and what actually gates the door still agree — just as a
     square now, not a circle. */
  .pin.harea{background:none;box-shadow:none;border-radius:var(--r-5xl);overflow:visible;
    pointer-events:none;display:grid;place-items:center;color:var(--hi)}
  .pin.harea::before{content:"";position:absolute;inset:0;border-radius:var(--r-5xl);
    background:var(--info-a10);border:2px dashed var(--info-a60)}
  /* Closed reads as a different boundary, not merely a locked note: a solid
     ring instead of a dashed one, and the brand gold every other "closed to
     you" surface in this venue already uses (v115: was red), rather than
     inventing a third colour for the same idea. */
  .pin.harea.closed::before{background:var(--accent-a8);border-style:solid;border-color:var(--accent-a65)}
  /* The boundary is real for an attendee too, so the chip and ring stay
     visible to them — but they cannot drag or resize a square any more than
     they could before this version, so the element takes no pointer events
     at all except for whoever paintEditAffordances has already marked as an
     editor (body.can-edit-content), the same gate a pin's own .pedit uses. */
  body.can-edit-content .pin.harea{pointer-events:auto}
  /* v120: a centered shield (open) or lock (closed) icon, larger than the
     chip's old inline one, sitting in the square's own grid center —
     .pin.harea is already display:grid;place-items:center, so a plain grid
     child needs no positioning of its own. */
  .harea-icon{position:relative;z-index:var(--z-1);width:28px;height:28px}
  .harea-icon svg{width:28px;height:28px}
  /* v120: the label chip moves off the center (now the icon's spot) and
     straddles the bottom border instead — the classic label-on-an-edge
     look. .pin.harea is itself absolutely positioned (inline left/top), so
     it is already a containing block for this. */
  .harea-chip{position:absolute;z-index:var(--z-1);left:50%;bottom:0;transform:translate(-50%, 50%);
    display:inline-flex;align-items:center;gap:var(--s-3);
    max-width:calc(100% - 16px);padding:var(--s-2) var(--s-5);border-radius:var(--pill);overflow:hidden;
    background:var(--glass-solid);color:var(--hi);font:var(--fw-semibold) var(--fs-11) var(--ui);white-space:nowrap;
    text-overflow:ellipsis;pointer-events:none}

  /* The heading owns the top-left of a note, so the pencil takes the other
     corner. Padding could not win it back: .pin.note h4 sets the shorthand
     later in this sheet and a shorthand resets what padding-left had set. */

  .pin .lab{position:absolute;left:var(--s-4);top:var(--s-4);font:var(--fw-semibold) var(--fs-11) var(--ui);background:var(--glass);color:var(--white);padding:var(--s-2) var(--s-4);border-radius:var(--r-999);pointer-events:none}
  /* Your own volume for a video, on the video. It is the one control a viewer
     always has, whatever the room's audio rule says. */
  .pin.video .vmute{position:absolute;right:var(--s-4);bottom:var(--s-4);width:34px;height:34px;display:grid;place-items:center;border-radius:50%;
    color:var(--white);background:var(--shade-55);box-shadow:var(--ring);-webkit-backdrop-filter:var(--blur-8);backdrop-filter:var(--blur-8);z-index:var(--z-2)}
  .pin.video .vmute:hover{background:var(--shade-75)}
  .pin.video .vmute svg{width:17px;height:17px}
  .pin.video .vmute.off{color:var(--coral-icon)}
  .pin.video .far{position:absolute;left:var(--s-4);bottom:var(--s-4);font:var(--fw-semibold) var(--fs-11) var(--ui);background:var(--glass);color:var(--white);padding:var(--s-2) var(--s-4);border-radius:var(--r-999);pointer-events:none}

  /* profile popover (white, like theirs) */
  /* The card leads with the person: their face at a size you can read, their
     name, and one line saying who they are here. Everything else is a control
     under a rule. */
  #pop{position:absolute;z-index:var(--z-dock);width:264px;background:var(--paper);border-radius:var(--r-3xl);padding:var(--s-7);
    box-shadow:var(--elev-pop);display:grid;gap:var(--s-6);color:var(--ink-2)}
  #pop .pop-head{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:var(--s-20);padding-right:var(--s-13)}
  /* v126. Was a fixed light-on-dark hairline that vanished on white in light
     theme; --ring is themed so it holds on both. */
  #pop .pop-face{width:44px;height:44px;border-radius:50%;overflow:hidden;background:var(--paper-3);
    display:grid;place-items:center;box-shadow:var(--ring)}
  #pop .pop-face .av{width:100%;height:100%}
  #pop h3{margin:0;font:var(--fw-semibold) var(--fs-15)/var(--lh-1-25) var(--ui);color:var(--ink-3);overflow-wrap:anywhere}
  #pop .pop-role{font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--ink-4)}
  #pop p{margin:0;font:var(--fw-regular) var(--fs-13)/var(--lh-1-5) var(--ui);color:var(--ink-2);white-space:pre-line;overflow-wrap:anywhere}
  #pop .pop-vol{display:grid;gap:var(--s-25);padding-top:var(--s-6);border-top:1px solid var(--line)}
  #pop .lbl{display:flex;justify-content:space-between;font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--ink-4)}
  #pop .lbl b{font-weight:var(--fw-semibold);color:var(--ink-2);font-variant-numeric:tabular-nums}
  /* v126. Ring added: --paper-3 == the popover's own surface now. */
  #pop .track{height:28px;border-radius:var(--r-3xl);background:var(--paper-3);box-shadow:var(--ring);display:flex;align-items:center;padding:0 var(--s-6)}
  #pop input[type=range]{width:100%;accent-color:var(--violet)}
  #pop .pop-actions{display:grid;gap:var(--s-3);padding-top:var(--s-6);border-top:1px solid var(--line)}
  #pop .pop-actions:empty{display:none}
  #pop .pop-actions .btn{height:36px;font-size:var(--fs-13);width:100%}
  /* v60c. Report is not a third thing to do with somebody. It sits under its own
     rule, with no pill and no fill, in the muted ink — a word you find when you
     are looking for it rather than an offer sitting level with "Wave". */
  #pop .pop-actions .btn.report{height:auto;margin-top:var(--s-4);padding-top:var(--s-8);
    border-top:1px solid var(--line);border-radius:0;background:none;box-shadow:none;
    font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--ink-4)}
  #pop .pop-actions .btn.report:hover,#pop .pop-actions .btn.report:focus-visible{background:none;color:var(--hi)}
  #pop .x{position:absolute;right:var(--s-5);top:var(--s-5);width:26px;height:26px;border-radius:50%;display:grid;place-items:center;
    color:var(--ink-4);transition:background var(--t-base),color var(--t-base)}
  #pop .x:hover{background:var(--hover);color:var(--ink-3)}

  /* ================= sidebar ================= */
  /* The sidebar does one job: where the rooms are and who is in them. The
     personal controls moved to the floor's dock. */
  #side{background:var(--panel);display:grid;grid-template-rows:auto auto 1fr auto;min-height:0;min-width:0;overflow:hidden}
  /* v116. The venue name and the head-count chip are gone — both were a
     duplicate of what the Venue menu and each room already show.
     v135. "How it works" left too (floor's own #floor-help) — Reports is
     the one labelled pill left, plus the close button, one row.
     Padding is even (s-6 top/bottom) and its sides match .head's own s-8
     margin below, so both rows' edges line up instead of drifting — the
     "3 widths" docs/ui-backlog.md flagged included this row sitting 4px
     off the tab row under it. */
  #side .brandbar{display:flex;align-items:center;gap:var(--s-4);padding:var(--s-6) var(--s-8);min-width:0}
  #side .brandbar #b-me,#side .brandbar #side-close{flex:none}
  #side .brandbar #room-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--fg-3)}
  #side .brandbar #b-me{width:34px;height:34px;display:grid;place-items:center;border-radius:50%;color:var(--fg-2)}
  /* v126. Ring added: --group == the sidebar's own surface now, and a
     segmented/pill control still has to read as a control. */
  #side .brandbar .bb-btn{flex:none;display:flex;align-items:center;gap:var(--s-3);height:34px;padding:0 var(--s-6);border-radius:var(--pill);background:var(--group);box-shadow:var(--ring);color:var(--fg-2);font:var(--fw-semibold) var(--fs-13) var(--ui);white-space:nowrap}
  #side .brandbar .bb-btn .i{width:16px;height:16px}
  #side .brandbar .bb-btn:hover{background:var(--hover);color:var(--hi)}
  /* Reports gets the venue accent — the one button in this row that means
     "something is waiting," same token family v115 gave every other accent
     (v136: that token family now follows cfg.space.accent, not always gold). */
  #side .brandbar #bb-reports{color:var(--coral-icon)}
  #side .brandbar #bb-reports:hover{background:var(--coral-soft)}
  #side .brandbar #bb-reports .tabcount{margin-left:var(--s-1)}
  #side .brandbar .spacer{flex:1}
  /* v135. Round now, not a rounded square, for consistency — though `#side
     #side-close{display:none!important}` (below, v42) means this element is
     never actually the one on screen; #side-open (also fixed this version,
     further down) is the collapse icon a viewer sees at every width. */
  #side .brandbar #side-close{flex:none;width:34px;height:34px;display:grid;place-items:center;border-radius:50%;color:var(--fg-2)}
  #side .brandbar #side-close:hover{background:var(--hover);color:var(--hi)}
  #side .brandbar #side-close .i{width:20px;height:20px}
  /* Two tabs read as one segmented control, not two loose buttons. */
  /* v126. Ring added, same reason as .bb-btn above. */
  #side .head{display:flex;align-items:center;gap:var(--s-21);margin:0 var(--s-8) var(--s-7);padding:var(--s-21);border-radius:var(--pill);background:var(--group);box-shadow:var(--ring);min-width:0}
  #side .head .spacer{flex:1}
  #side .head button{flex:1;height:34px;border-radius:var(--pill);display:flex;align-items:center;justify-content:center;gap:var(--s-4);font:var(--fw-semibold) var(--fs-13) var(--ui);color:var(--fg-3);transition:background var(--t-base),color var(--t-base)}
  #side .head button .i{width:16px;height:16px}
  #side .head button:hover{color:var(--hi)}
  /* The venue's own button language is a white pill; gold at 13px carries only
     ~2:1 against white (v115: worse than the red it replaced), so the
     selected tab wears the pill instead. */
  #side .head button.on{background:var(--solid);color:var(--on-solid)}
  /* v64. An open-count. Small enough to sit in a pill next to its label,
     hidden at zero so a quiet venue's sidebar reads the same as it always
     has. v116: moved off the (now-deleted) Reports tab onto the header's
     own Reports button, so the rule is no longer scoped to `.head`. */
  .tabcount{padding:1px var(--s-3);border-radius:var(--r-999);background:var(--coral-solid);color:var(--on-accent);font:var(--fw-bold) var(--fs-11)/var(--lh-1-5) var(--ui);min-width:16px;text-align:center}
  .tabcount.hidden{display:none}
  /* v167. A count landing (or a fresh unread row mounting one) pops rather
     than just appearing — this animation plays whenever the element starts
     matching :not(.hidden), whether that's a class toggle or a brand-new
     node arriving already unhidden. */
  .tabcount:not(.hidden){animation:lh-badge-pop var(--t-panel) var(--ease-pop)}
  @keyframes lh-badge-pop{from{transform:scale(.5);opacity:0}to{transform:none;opacity:1}}
  @media (prefers-reduced-motion:reduce){.tabcount:not(.hidden){animation:none}}
  #side .head .qwrap{position:relative;display:flex;align-items:center}
  #side .head .qwrap input{width:0;opacity:0;transition:opacity var(--t-ease);background:transparent;border:0;color:var(--hi);font:var(--fw-regular) var(--fs-15) var(--ui);padding:0}
  #side .head .qwrap.open input{width:150px;opacity:1;margin-left:var(--s-4)}
  .pane{overflow:auto;min-height:0;padding:0 var(--s-5) var(--s-5);scrollbar-gutter:stable}
  /* A group heading breathes above and sits close to what it names. */
  .grpbox{border-radius:var(--r-xl)}
  .grpbox + .grpbox{margin-top:var(--s-16)}
  .grp{display:flex;align-items:center;gap:var(--s-4);height:22px;margin:0 0 var(--s-3);padding:0 var(--s-4);
    font:var(--fw-semibold) var(--fs-11) var(--ui);letter-spacing:var(--ls-01em);text-transform:uppercase;color:var(--fg-3)}
  .grp svg{width:13px;height:13px;color:var(--fg-3)}
  .rm{display:flex;align-items:center;gap:var(--s-20);height:46px;padding:0 var(--s-6);border-radius:var(--r-xl);color:var(--fg-2);cursor:pointer;width:100%;text-align:left;
    transition:background var(--t-base),color var(--t-base)}
  .rm:hover{background:var(--hover);color:var(--hi)}
  /* An editor can pick a room up in the list it lives in. The line shows the
     seat it will take; the row being carried fades. */
  .rm[draggable=true]{cursor:grab}
  .rm[draggable=true]:active{cursor:grabbing}
  .rm.dragging{opacity:.4}
  .rm.dropbefore{box-shadow:inset 0 3px 0 var(--accent)}
  .rm.dropafter{box-shadow:inset 0 -3px 0 var(--accent)}
  /* v126. Was --group; --group is now the sidebar's own surface (solid), so
     it could no longer mark which room is selected. --hover is interaction
     feedback, not a surface — an active row reads the same way a hovered
     one does. */
  .rm.on{background:var(--hover);color:var(--hi)}
  .rm .chev{display:none}
  .rm .ico{flex:none;width:22px;text-align:center;font-size:var(--fs-16);line-height:var(--lh-1);filter:saturate(.9)}
  .rm .nm{flex:1;font:var(--fw-semibold) var(--fs-14) var(--ui);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
  /* Presence, not a form field: the number only appears when someone is there. */
  /* How full, always — the number you are and the number it holds, with the
     ceiling set back so the row still reads as a room and not a form field. */
  .rm .ct{flex:none;display:flex;align-items:baseline;gap:var(--s-17);font:var(--fw-semibold) var(--fs-12) var(--ui);color:var(--fg-2);font-variant-numeric:tabular-nums}
  .rm .ct i{font-style:normal;font-weight:var(--fw-medium);color:var(--fg-3);opacity:.75}
  .rm .ct::before{content:"";align-self:center;width:6px;height:6px;border-radius:50%;background:currentColor;opacity:.55}
  .rm .ct.empty{color:var(--fg-3)}
  .rm .ct.empty::before{opacity:.28}
  .rm .ct.full,.rm .ct.full i{color:var(--coral-icon);opacity:1}
  .rm.on .ct{color:var(--coral-icon)}
  /* v126. Ring added: --group == the sidebar's own surface now. */
  .rm .tagx{flex:none;font:var(--fw-semibold) var(--fs-10) var(--ui);letter-spacing:var(--ls-008em);text-transform:uppercase;color:var(--fg-3);
    padding:var(--s-21) var(--s-25);border-radius:var(--pill);background:var(--group);box-shadow:var(--ring)}
  /* v66b. An icon-only tag — the reason is the title/aria-label, on hover or
     tap, never a second line of text in a rail that is already tight. */
  .rm .tagx.lockmark{display:inline-flex;align-items:center;justify-content:center;padding:var(--s-2)}
  .rm .tagx.lockmark svg{width:10px;height:10px}
  /* v67b. The host-present light (owner_present, venue.js renderRooms) — a lit
     dot only, on the sidebar row and on an overview card; there is nothing to
     show when the host is not there, so the element is only ever drawn on. */
  .tagx.hostdot,.ovcard .hostdot{width:8px;height:8px;padding:0;border-radius:50%;background:var(--live);box-shadow:var(--ring-live)}
  .grp.clickable{cursor:pointer;display:flex;align-items:center;gap:var(--s-3);background:none;border:0;font:inherit;text-align:left;width:100%}
  .grp.clickable:hover{color:var(--fg-1)}
  /* The people in the room you are standing in, tucked under it. Closed, they are
     a row of overlapping faces; the sixth and seventh fade out to say the row is
     not the whole room, and the count on the room row above gives the number. */
  .ppl{margin:var(--s-1) 0 var(--s-1);padding:0 0 0 var(--s-6)}
  .ppl .stack{display:flex;align-items:center;height:36px;padding:0 var(--s-5) 0 var(--s-4);border-radius:var(--r-lg);width:100%;cursor:pointer}
  .ppl .stack:hover{background:var(--hover)}
  .ppl .stack .avatar{width:24px;height:24px;flex:none;font-size:var(--fs-10);box-shadow:0 0 0 2px var(--panel)}
  .ppl .stack .avatar + .avatar{margin-left:calc(var(--s-18) * -1)}
  .ppl .stack .avatar:nth-child(6){opacity:.5}
  .ppl .stack .avatar:nth-child(7){opacity:.22}
  .ppl .stack .chev{margin-left:auto;width:15px;height:15px;flex:none;color:var(--fg-3);transition:transform var(--t-base)}
  .ppl.open .stack .chev{transform:rotate(180deg)}
  /* The list opens rather than appears. A grid row animates from 0fr to 1fr, so
     the names slide out of the row of faces they belong to instead of the panel
     jumping by their height. */
  .ppl .list{display:grid;grid-template-rows:0fr;opacity:0;margin-top:0;
    transition:grid-template-rows var(--t-panel) var(--ease-move),opacity var(--t-base) var(--ease),margin-top var(--t-panel)}
  .ppl .list > *{min-height:0}
  .ppl .list .inner{display:grid;gap:1px;overflow:hidden;min-height:0}
  .ppl.open .list{grid-template-rows:1fr;opacity:1;margin-top:var(--s-1)}
  @media (prefers-reduced-motion: reduce){.ppl .list{transition:none}}
  .pp{display:flex;align-items:center;gap:var(--s-5);height:34px;padding:0 var(--s-5);border-radius:var(--r-lg);font:var(--fw-medium) var(--fs-13) var(--ui);color:var(--fg-2);width:100%;text-align:left}
  .pp:hover{background:var(--hover)} .pp .avatar{width:22px;height:22px;flex:none;font-size:var(--fs-10)}
  .pp .you{color:var(--fg-3);font-weight:var(--fw-regular)} .pp .nm{color:var(--fg-2);white-space:nowrap;overflow:hidden;text-overflow:ellipsis;min-width:0}
  #side .foot{padding:var(--s-5);display:grid;gap:var(--s-4);box-shadow:inset 0 1px 0 var(--line)}
  /* v167. The host's list, under the chat: a quiet block, text then one button. */
  #list-card{display:grid;gap:var(--s-4);padding:var(--s-6);border-radius:var(--r-xl);background:var(--group);box-shadow:var(--ring);animation:lh-pane-in var(--t-panel) var(--ease-arrive)}
  #list-card.hidden{display:none}
  #list-card .lc-text{display:grid;gap:var(--s-1)}
  #list-card .lc-text b{font:var(--fw-semibold) var(--fs-14)/var(--lh-1-3) var(--ui);color:var(--hi)}
  #list-card .lc-text small{font-size:var(--fs-12);color:var(--fg-3)}
  #list-card .btn{width:100%;height:40px;font-size:var(--fs-14)}
  @media (prefers-reduced-motion: reduce){#list-card{animation:none}}
  #addroom,#venueadmin{display:flex;gap:var(--s-5);align-items:center;height:42px;padding:0 var(--s-6);border-radius:var(--r-xl);color:var(--fg-2);font:var(--fw-semibold) var(--fs-14) var(--ui);text-transform:none}
  #addroom:hover,#venueadmin:hover{background:var(--hover)}
  #venueadmin{color:var(--fg-3)}
  /* chat */
  /* v141. Room/Direct sub-tabs, sticky at the top of #pane-chat so they stay
     put while either #chatlog or #dmpane scrolls under them — the same
     "outer chrome fixed, content scrolls" shape the drawer rail already
     keeps for its own header. */
  .chat-subtabs{position:sticky;top:0;z-index:var(--z-1);display:flex;gap:var(--s-4);padding:var(--s-4) 0;background:var(--field)}
  .chat-subtabs button{display:flex;align-items:center;gap:var(--s-3);height:32px;padding:0 var(--s-6);border-radius:var(--r-3xl);color:var(--fg-3);font:var(--fw-semibold) var(--fs-13) var(--ui);background:transparent}
  .chat-subtabs button.on{color:var(--fg-1);background:var(--hover)}
  /* v166. #chatlog carries its own s-4 horizontal padding on top of .pane's
     (both #pane-rooms and #pane-chat share .pane's own inset); #dmpane had
     none of its own, so Direct sat s-4 closer to the sidebar edge than Room
     — the same inset token closes the gap. */
  #dmpane{display:grid;gap:var(--s-4);padding:0 var(--s-4)}
  #dmthreads{display:grid}
  .dm-thread-row{cursor:pointer}
  .dm-thread-row:hover{background:var(--hover)}
  #dmthread-head{display:flex;align-items:center;gap:var(--s-4);padding:var(--s-3) 0;font:var(--fw-semibold) var(--fs-14) var(--ui);color:var(--fg-1)}
  #dmthread-head [data-dm-back]{display:flex;color:var(--fg-3)}
  /* v142. The name grows to push Block/Unblock to the far side of the
     header, the same "chrome fixed, name fills the middle" shape a
     conversation's own header keeps everywhere else in the venue. */
  #dmthread-head span{flex:1}
  /* Block is a consequence, not the header's main action: quiet, the same
     small underlined text Report uses under a message, never a pill that
     outweighs the person's own name. */
  #dmthread-head [data-dm-block]{font:var(--fw-semibold) var(--fs-12) var(--ui);color:var(--fg-3);background:transparent;border:0;box-shadow:none;border-radius:0;padding:0;min-height:0;height:auto;width:auto;text-decoration:underline;text-underline-offset:2px}
  #dmthread-log{display:grid;gap:var(--s-5);padding:var(--s-4) 0}
  .dm-msg{padding-right:var(--s-11)}
  .dm-msg.me{color:var(--hi)}
  .dm-report-btn{margin-top:var(--s-2);font:var(--fw-semibold) var(--fs-11) var(--ui);color:var(--fg-4);background:transparent;text-decoration:underline;text-underline-offset:2px}
  .dm-privacy{padding:var(--s-4) 0}
  #dmblocked-line{padding:var(--s-4) 0}
  /* You › Blocked people — a face the size of a chat line's own avatar,
     inline with the name, the same "small round face beside text" shape
     .desk-face's own .av keeps at a larger size for Faces. */
  [data-blocked] .av{width:28px;height:28px;vertical-align:middle;margin-right:var(--s-3)}
  #chatlog{display:grid;gap:var(--s-5);padding:var(--s-4) var(--s-4);align-content:end;min-height:100%;user-select:text;-webkit-user-select:text}
  /* v32. A message can be copied: select it, or press Copy. A link in it is a
     link, and the click goes through the Leaving card first. */
  .msg{position:relative;padding-right:var(--s-11)}
  /* v126. Ring added: --field == the sidebar's own surface now. */
  .msg .copy{position:absolute;right:0;top:0;height:24px;padding:0 var(--s-4);border-radius:var(--r-md);font:var(--fw-semibold) var(--fs-11) var(--ui);color:var(--fg-3);background:var(--field);box-shadow:var(--ring);opacity:0;transition:opacity var(--t-base)}
  .msg:hover .copy,.msg .copy:focus-visible{opacity:1}
  @media(hover:none){.msg .copy{opacity:.7}}
  .msg a.chatlink{color:var(--hi);text-decoration:underline;text-underline-offset:2px;overflow-wrap:anywhere}
  .msg{font:var(--fw-regular) var(--fs-14)/var(--lh-1-45) var(--ui);color:var(--fg-2);display:grid;gap:var(--s-1);min-width:0;overflow-wrap:anywhere} .msg b{font-weight:var(--fw-semibold);color:var(--hi)} .msg.sys{color:var(--fg-4);font-style:italic}
  /* v105. One field, the whole width. It grows with what is typed (chatGrow,
     venue.js) up to a few sentences, then scrolls; the send is a small icon
     inside it, solid only once there is something to send.
     v166. Munk, live pass: the textarea drew its own rounded, ringed "field"
     box sitting inside #side-foot, which already carries a divider of its own
     (.foot's inset box-shadow, above) — two boxes reading as one nested
     inside the other. The field's own background/ring/radius are gone; the
     whole lower block (#side-foot) IS the typing area now, and focus
     brightens that one divider (#side .foot:focus-within, below) instead of
     drawing a second box around the text. #dmform shares the textarea/send
     shape (it never had its own rule before this — a bare browser textarea,
     unstyled) so Direct gets the same treatment. */
  #chatform,#dmform{position:relative;display:block;padding:0}
  #chatform textarea,#dmform textarea{display:block;width:100%;resize:none;overflow-y:auto;background:transparent;border:0;box-shadow:none;color:var(--hi);border-radius:0;font:var(--fw-regular) var(--fs-15)/var(--lh-22px) var(--ui);padding:var(--s-26) var(--s-31) var(--s-26) var(--s-7);min-height:48px;max-height:136px}
  #chatform textarea:focus-visible,#dmform textarea:focus-visible{outline:none}
  #side .foot:focus-within{box-shadow:inset 0 1px 0 var(--focus)}
  /* v126. Ring added: --group == the textarea's own --field surface now. */
  #chatform #chatsend,#dmform #dmsend{position:absolute;right:var(--s-4);bottom:var(--s-4);width:32px;height:32px;padding:0;border-radius:50%;display:grid;place-items:center;background:var(--group);box-shadow:var(--ring);color:var(--fg-3);transition:background var(--t-base),color var(--t-base)}
  #chatform #chatsend .i,#dmform #dmsend .i{width:17px;height:17px}
  #chatform.has-text #chatsend,#dmform.has-text #dmsend{background:var(--solid);color:var(--on-solid)}
  #chatform #chatsend:disabled,#dmform #dmsend:disabled{opacity:.4}
  /* schedule */
  .sched .ev:hover{background:var(--hover)}
  .sched .ev .t{color:var(--fg-3);font-weight:var(--fw-semibold)} .sched .ev .r{color:var(--fg-4);font-size:var(--fs-12)}
  .sched .ev a{color:var(--info-text);text-decoration:none}

  /* status bar */
  #status .r{margin-left:auto;display:flex;gap:var(--s-8);align-items:center;font-size:var(--fs-13);color:var(--fg-3);font-variant-numeric:tabular-nums}
  #status .r b{color:var(--white);font-weight:var(--fw-semibold)}
  #status .up{height:32px;padding:0 var(--s-7);border-radius:var(--pill);background:var(--white);color:var(--ink);font:var(--fw-semibold) var(--fs-13) var(--ui)}

  /* menus + modals */
  .menu{position:absolute;z-index:var(--z-bar);min-width:240px;background:var(--glass-solid);border-radius:var(--r);padding:var(--s-3);display:grid;box-shadow:var(--elev-menu)}
  .menu button{text-align:left;height:40px;padding:0 var(--s-6);border-radius:var(--r-md);font:var(--fw-medium) var(--fs-14) var(--ui);color:var(--hi);display:flex;gap:var(--s-6);align-items:center}
  .menu button:hover{background:var(--hover)}
  .menu .k{margin-left:auto;font-size:var(--fs-11);color:var(--fg-4)}
  .menu .sep{height:1px;background:var(--line);margin:var(--s-3) var(--s-4)}
  #addmenu{left:50%;bottom:var(--s-32);transform:translateX(-50%);min-width:200px}
  /* The room context menu is placed at the pointer, so it is fixed, not absolute. */
  #roommenu{position:fixed;min-width:220px;max-height:calc(100vh - 16px);max-height:calc(100dvh - 16px);overflow:auto;overscroll-behavior:contain}
  #roommenu .hd{padding:var(--s-4) var(--s-6) var(--s-3);font:var(--fw-semibold) var(--fs-12) var(--ui);letter-spacing:var(--ls-06px);text-transform:uppercase;color:var(--fg-4);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
  #roommenu button[disabled]{opacity:.4}
  #roommenu button[disabled]:hover{background:none}
  .modal{position:fixed;inset:0;z-index:var(--z-modal);display:grid;place-items:center;background:var(--surface-a70);-webkit-backdrop-filter:var(--blur-6);backdrop-filter:var(--blur-6)}
  .modal .card{width:min(480px,92vw);background:var(--paper);color:var(--ink-3);border-radius:var(--r);box-shadow:var(--elev-modal);padding:var(--s-15);display:grid;gap:var(--s-8)}
  .modal h2{margin:0;font:var(--fw-regular) var(--fs-22)/var(--lh-1-25) var(--brand);letter-spacing:var(--ls-06px)}
  .modal p{margin:0;font:var(--fw-regular) var(--fs-16)/var(--lh-1-5) var(--display);color:var(--ink-4)}
  .modal .row{display:flex;gap:var(--s-5);justify-content:flex-end}
  /* v60. White on --accent was under AA on the one button in every destructive
     modal whose label you most need to read; --coral-solid was the brand's own
     darker red, clearing it. v115: --accent and --coral-solid are both gold
     now, and white-on-gold is worse still (~2:1) — see --on-gold above. */
  /* v105. No red buttons: the solid high-contrast button is the only primary. */
  .modal .card,#av-gal .gal{position:relative}
  .modal-x{position:absolute;right:var(--s-7);top:var(--s-7);z-index:var(--z-2);width:36px;height:36px;padding:0;border-radius:50%;display:grid;place-items:center;color:var(--fg-2);background:transparent;transition:background var(--t-base),color var(--t-base)}
  .modal-x:hover,.modal-x:focus-visible{background:var(--hover);color:var(--hi)}
  .modal-x .i{width:20px;height:20px}
  .tool-head{padding-right:var(--s-28)!important}
  #admin .row{padding-right:var(--s-11)}
  #tool-foot-left:empty{display:none}
  #tool-foot-left:empty + .tool-foot-right{margin:0 auto;flex:1;justify-content:center}
  #tool-foot-left:empty + .tool-foot-right .btn{height:52px;min-width:min(320px,100%);font-size:var(--fs-16)}
  .tool-foot:has(#tool-save.hidden):has(#tool-foot-left:empty){display:none}
  /* v106. While the snapshot is made: a star, and three more circling it. */
  #photo-cooking{position:fixed;inset:0;z-index:var(--z-crop);display:grid;place-content:center;justify-items:center;gap:var(--s-8);background:var(--surface-a62);-webkit-backdrop-filter:var(--blur-8);backdrop-filter:var(--blur-8);opacity:0;transition:opacity var(--t-250) var(--ease);color:var(--white)}
  #photo-cooking.on{opacity:1}
  #photo-cooking p{margin:0;font:var(--fw-regular) var(--fs-24)/var(--lh-1-2) var(--brand);letter-spacing:var(--ls-03px);color:var(--white);text-shadow:var(--text-shadow-strong)}
  .cook{position:relative;width:132px;height:132px}
  .cook svg{fill:var(--cook-core);filter:var(--glow-cook)}
  .cook-core{position:absolute;inset:var(--s-33);display:grid;animation:cook-pulse var(--t-1500) var(--ease-in-out) infinite}
  .cook-core svg{width:100%;height:100%}
  .cook-orbit{position:absolute;inset:0;animation:cook-spin var(--t-2400) var(--linear) infinite}
  .cook-orbit svg{position:absolute;width:22px;height:22px;left:50%;top:0;margin-left:calc(var(--s-20) * -1);transform-origin:11px 66px;animation:cook-twinkle var(--t-1200) var(--ease-in-out) infinite}
  .cook-orbit svg:nth-child(2){transform:rotate(120deg);animation-delay:calc(var(--t-400) * -1);width:16px;height:16px;margin-left:calc(var(--s-4) * -1);transform-origin:8px 66px}
  .cook-orbit svg:nth-child(3){transform:rotate(240deg);animation-delay:calc(var(--t-800) * -1);width:19px;height:19px;margin-left:calc(var(--s-34) * -1);transform-origin:9.5px 66px}
  @keyframes cook-spin{to{transform:rotate(360deg)}}
  @keyframes cook-pulse{0%,100%{transform:scale(.82) rotate(0deg);opacity:.85}50%{transform:scale(1.08) rotate(45deg);opacity:1}}
  @keyframes cook-twinkle{0%,100%{opacity:.35}50%{opacity:1}}
  @media (prefers-reduced-motion:reduce){.cook-orbit,.cook-orbit svg{animation:none}.cook-core{animation:cook-twinkle var(--t-1800) var(--ease-in-out) infinite}}
  /* v105. The snapshot: the picture fits the screen, never scrolls. */
  #tool-modal.photo .card{width:min(600px,95vw)}
  #tool-modal.photo .tool-head h2{font:var(--fw-regular) var(--fs-26)/var(--lh-1-2) var(--brand);color:var(--hi)}
  #tool-modal.photo #tool-content{overflow:hidden;justify-items:center;gap:var(--s-6);padding-bottom:var(--s-6)}
  .photo-img{display:block;width:auto;max-width:100%;aspect-ratio:1;height:min(520px,calc(100dvh - 340px));min-height:180px;border-radius:var(--r-xl);box-shadow:var(--ring)}
  .photo-share{display:flex;flex-wrap:wrap;gap:var(--s-5);justify-content:center}
  .photo-share button{width:44px;height:44px;border-radius:50%;display:grid;place-items:center;color:var(--fg-2);background:var(--field);box-shadow:var(--ring);transition:background var(--t-base),color var(--t-base)}
  .photo-share button:hover,.photo-share button:focus-visible{background:var(--solid);color:var(--on-solid)}
  .photo-share .i{width:20px;height:20px}
  .i.brand{fill:currentColor;stroke:none}
  #tool-save .i{width:20px;height:20px}
  /* v105. A locked room, as itself. */
  #tool-modal.locked .card{width:min(480px,95vw)}
  #tool-modal.locked .tool-head h2{font:var(--fw-regular) var(--fs-28)/var(--lh-1-2) var(--brand);color:var(--hi)}
  .lock-hero{position:relative;height:190px;border-radius:var(--r-xl);background:var(--group) center/cover no-repeat;box-shadow:var(--ring);overflow:hidden}
  .lock-hero::after{content:"";position:absolute;inset:0;background:linear-gradient(180deg,var(--surface-a15),var(--surface-a70))}
  .lock-badge{position:absolute;z-index:var(--z-1);left:50%;top:50%;transform:translate(-50%,-50%);width:56px;height:56px;border-radius:50%;display:grid;place-items:center;background:rgba(12,12,14,.55);color:var(--white);box-shadow:var(--ring);-webkit-backdrop-filter:var(--blur-10);backdrop-filter:var(--blur-10)}
  /* v167. Always dark glass: it sits on the room's photo, and light mode's
     --glass-pill is near-white, which left a white lock on a white disc. */
  .lock-badge .i{width:26px;height:26px}
  .lock-say{margin:0;font:var(--fw-medium) var(--fs-17)/var(--lh-1-45) var(--ui);color:var(--hi);text-align:center}
  .lock-fine{margin:0;text-align:center}
  /* v122. The "coming soon" cover — same shell as a locked room (.lock-hero,
     above, reused as-is), its own three lines of text.
     v139. Plus the host's own intro (face/name), their message, and the
     inline signup note — coverHostHTML/coverMessageHTML/coverSignupHTML,
     public/venue.js.
     v167 design polish. The modal's own title (h2#tool-title) repeated the
     room name the cover already prints itself, so it is visually hidden
     here (kept for aria-labelledby — #tool-modal itself carries that,
     shell/index.html) and the × floats over the card's own top-right corner
     instead of sharing a header row with it — .modal-x is already
     position:absolute against .card (the rule above), so shrinking
     .tool-head to just enough padding to clear it is the whole change. */
  #tool-modal.coming-soon .card{width:min(440px,95vw)}
  #tool-modal.coming-soon .tool-head{padding:var(--s-8) var(--s-8) 0}
  #tool-modal.coming-soon .tool-head h2{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
  #tool-modal.coming-soon #tool-content{padding:0 var(--s-9) var(--s-9)}
  /* Editing needs real room for the toolbar (below) — widened only while it
     is actually there, never for the plain read-only card a visitor/member
     sees. :has() already gates other #tool-modal layout in this file
     (.tool-foot, #tool-content:has(.tool-intro), above/below). */
  #tool-modal.coming-soon .card:has(.cs-toolbar){width:min(600px,95vw)}
  @media(max-width:520px){
    #tool-modal.coming-soon .card,#tool-modal.coming-soon .card:has(.cs-toolbar){width:calc(100vw - var(--s-8) * 2)}
  }
  .cs-host{display:flex;align-items:center;justify-content:center;gap:var(--s-4);margin:var(--s-6) 0 0}
  .cs-face{width:48px;height:48px;flex:none;border-radius:50%;box-shadow:var(--ring)}
  .cs-face .av{width:100%;height:100%}
  .cs-host-name{font:var(--fw-semibold) var(--fs-15) var(--ui);color:var(--hi)}
  /* v167. "countdown" (and anything unrecognised) also says "Hosted by
     NAME" now, small and muted — cs-host-countdown, coverHostStyledHTML
     (venue.js) — rather than the bare name the mockup review flagged as
     reading like a mistake. */
  .cs-host-countdown .cs-host-name{font:var(--fw-medium) var(--fs-13) var(--ui);color:var(--fg-3)}
  .cs-host-countdown .cs-face{width:28px;height:28px}
  /* v167. The room name, printed in the card itself now the modal's own
     title is hidden (above) — roomTitleHTML, venue.js. Absent for "letter",
     which folds the name into its own one small line instead (cs-when-letter,
     below).
     v167 design polish. #tool-content p and #tool-modal h3 (both used
     throughout the drawer for ordinary body copy/section labels) are each
     one ID selector, which otherwise beats any of the plain classes below
     on specificity alone and would win the font-size/line-height/margin
     back on every one of these (all <p>/<h3> elements) regardless of source
     order. Every rule that sets those properties for the cover is scoped
     under #tool-modal.coming-soon so it wins instead — this is the one
     change that makes the mockup's actual type sizes (13/14/24px) render at
     all, rather than the drawer's own defaults (14px body text everywhere,
     16px section labels) quietly winning underneath them. */
  #tool-modal.coming-soon .cs-room-title{margin:var(--s-3) 0 0;font:var(--fw-regular) var(--fs-24)/var(--lh-1-2) var(--brand);letter-spacing:0;text-transform:none;color:var(--hi);text-align:center}
  #tool-modal.coming-soon .cs-message{margin:var(--s-5) 0 0;font:var(--fw-regular) var(--fs-14)/var(--lh-1-5) var(--ui);color:var(--fg-2);text-align:center}
  #tool-modal.coming-soon .cs-details{margin:var(--s-4) 0 0;font:var(--fw-regular) var(--fs-14)/var(--lh-1-5) var(--ui);color:var(--fg-2);text-align:center}
  #tool-modal.coming-soon .cs-when{margin:var(--s-3) 0 0;font:var(--fw-semibold) var(--fs-13) var(--ui);color:var(--fg-3);text-align:center}
  #tool-modal.coming-soon .cs-signup-note{flex-basis:100%;margin:var(--s-3) 0 0;font:var(--fw-regular) var(--fs-12)/var(--lh-1-4) var(--ui);color:var(--fg-3);text-align:center}
  /* v167 design polish. The message/details/RSVP fields print as the card's
     own read-only text while a host edits in place — no border box, no
     background, the same font/size/alignment .cs-message/.cs-details
     already use (the field just adds the class alongside cs-edit-field,
     venue.js) — a dashed underline and a faint pencil the only hint they're
     live, and the uppercase "Message"/"Details"/"RSVP link" captions the
     mockup flagged are gone visually, kept only as an aria-label on the
     field itself. */
  .cs-edit-wrap{position:relative;display:block;margin-top:var(--s-4)}
  .cs-edit-field{display:block;width:100%;background:transparent;border:0;border-bottom:1px dashed var(--rule-line);border-radius:0;box-shadow:none;padding:var(--s-2) 0;text-align:center;resize:none;overflow:hidden;field-sizing:content;transition:border-color var(--t-base)}
  /* input[type=url] (venue.css, form basics) is an attribute selector — for
     the RSVP field, (element+attribute) beats a plain class on specificity
     and would win its box-shadow/background/color/height back. Repeating
     the reset at (element+class) ties it, and this is later in the file, so
     it wins instead. Message/details stay textareas, already un-contested,
     so they never take this — it would outrank the letter style's own
     italic-quote color for the message field (cs-message-letter,
     specificity .cs-style-letter .cs-message-letter). */
  input.cs-edit-field{background:transparent;box-shadow:none;height:auto;border-radius:0;padding:var(--s-2) 0;font:var(--fw-regular) var(--fs-14)/var(--lh-1-5) var(--ui);color:var(--fg-2)}
  .cs-edit-field::placeholder{color:var(--fg-4);opacity:1}
  .cs-edit-field:focus{outline:0;border-bottom-style:solid;border-color:var(--accent);box-shadow:none}
  .cs-edit-wrap::after{content:"";position:absolute;right:0;top:var(--s-2);width:14px;height:14px;opacity:0;pointer-events:none;color:var(--fg-3);
    background:currentColor;-webkit-mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4'/%3E%3C/svg%3E") center/contain no-repeat;
    mask:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-width='2' stroke-linecap='round' stroke-linejoin='round' d='M4 20h4l10.5 -10.5a2.828 2.828 0 1 0 -4 -4l-10.5 10.5v4'/%3E%3C/svg%3E") center/contain no-repeat;
    transition:opacity var(--t-base)}
  .cs-edit-wrap:hover::after,.cs-edit-wrap:focus-within::after{opacity:.45}
  /* v167. Three cover styles (cover_style — showComingSoonCover,
     public/venue.js) plus the host's own small editing toolbar above the
     card (canEditCover(r) only) — a quiet surface strip that is clearly
     "editor chrome" and not part of the cover itself: one row on desktop
     (the wider card above makes the room), wraps to two on phone. The
     shared shape — .cs-card/.cs-hero/.cs-panel — is 'countdown' as-is
     (.lock-hero's own rules, above, still apply — .cs-hero only adds what
     changes per style); 'poster' and 'letter' override just enough of it
     below. */
  .cs-toolbar{display:flex;flex-wrap:wrap;align-items:flex-end;gap:var(--s-6);width:100%;margin:0 0 var(--s-8);padding:var(--s-6);border-radius:var(--r);background:var(--field);box-shadow:var(--ring)}
  .cs-toolbar .seg{padding:0;flex:1 1 220px;min-width:0}
  .cs-toolbar .seg button{white-space:nowrap}
  .cs-toolbar > *{max-width:100%}
  .cs-toolbar .f{flex:1 1 108px;min-width:0;margin:0}
  .cs-toolbar select{font-size:var(--fs-13);padding:var(--s-4) var(--s-6)}
  .cs-toolbar > .btn{flex:none;align-self:flex-end}
  @media(max-width:520px){.cs-toolbar .seg,.cs-toolbar .f{flex-basis:100%}}
  .cs-card{display:grid}
  .cs-panel{position:relative;padding-top:var(--s-8);display:flex;flex-direction:column}
  /* v167. Rounded top corners only, flush to the card's own edges — the
     negative inline margin cancels out #tool-content's own side padding
     (above) just for the hero, so the image reaches exactly as far as the
     card does; .lock-hero's own rules (above) still give it the gradient
     scrim underneath everything here. */
  .cs-hero{margin:0 calc(-1 * var(--s-9));border-radius:var(--r-xl) var(--r-xl) 0 0}
  .cs-tiles{display:flex;justify-content:center;gap:var(--s-4);margin:var(--s-6) 0 0}
  .cs-tile{display:grid;justify-items:center;gap:var(--s-1);width:64px;padding:var(--s-4) var(--s-2);border-radius:var(--r-md);background:var(--field);box-shadow:var(--ring)}
  .cs-tile b{font:var(--fw-semibold) var(--fs-22) var(--ui);color:var(--accent)}
  .cs-tile small{font:var(--fw-medium) var(--fs-11) var(--ui);letter-spacing:var(--ls-009em);text-transform:uppercase;color:var(--fg-3)}
  .cs-cta{margin:0;align-self:center}
  .cs-host-poster,.cs-host-letter{display:flex;align-items:center;gap:var(--s-4)}
  .cs-host-info{display:grid;text-align:center}
  .cs-host-role{font:var(--fw-regular) var(--fs-12) var(--ui);color:var(--fg-3)}
  /* v167 design polish. The two quiet/primary buttons and the "sign in"
     note stack in one column, bottom-anchored: the optional second button
     (RSVP/site — coverCtaHTML, quiet/plain already) sits above "Share my
     email", which is now the one full-width solid primary at the foot of
     the card, its consent line centred and small right under it. */
  #tool-modal.coming-soon .tool-actions{flex-direction:column;align-items:stretch;gap:var(--s-4);margin-top:var(--s-7)}
  #cs-join-list,#cs-enter{width:100%;background:var(--solid);color:var(--on-solid);box-shadow:none}
  /* poster: the room's own picture fills the top of the card at a tall,
     fixed height (>=320px, per the mockup's "4:5 or at least 320px tall"),
     flush to its edges, with a bottom-to-top dark scrim that is always dark
     — --surface-a* are literal dark rgba, not theme tokens, so the text
     sitting on it (host/room-title/when, cs-overlay below) reads white in
     both themes, never just in dark mode. Host/title/when are pulled onto
     the image as ONE group (not three separate nudges), so nothing about
     the host row is ever drawn twice — coverHostStyledHTML is still called
     exactly once (venue.js). Message/details flow under the image as
     normal, in the card's own ink, never white. */
  .cs-style-poster .lock-hero{height:340px;min-height:320px}
  .cs-style-poster .lock-hero::after{background:linear-gradient(to top,var(--surface-a70) 0%,var(--surface-a62) 50%,transparent 85%)}
  /* v167. Absolutely positioned against .cs-panel (position:relative,
     above), not nudged up with a negative margin — a fixed offset from
     .cs-panel's own top edge (== the hero's bottom edge) that lands the
     whole cluster inside the scrim's dark zone regardless of how tall the
     host/title/when text actually renders, rather than a margin whose
     correctness depended on that height matching a guess. */
  .cs-style-poster .cs-panel{padding-top:0}
  .cs-style-poster .cs-overlay{position:absolute;left:var(--s-8);right:var(--s-8);top:-108px;z-index:var(--z-1);display:grid;gap:var(--s-2);justify-items:start;color:var(--white)}
  .cs-style-poster .cs-host{margin:0;justify-content:flex-start}
  .cs-style-poster .cs-host-name{color:var(--white)}
  #tool-modal.coming-soon .cs-style-poster .cs-room-title{margin:0;font-size:var(--fs-26);text-align:left;color:var(--white)}
  #tool-modal.coming-soon .cs-style-poster .cs-when{margin:0;text-align:left;color:var(--on-dark-85)}
  .cs-style-poster .cs-body{display:flex;flex-direction:column;padding-top:var(--s-8)}
  /* letter: no picture — a big centred face, the message set as a serif
     quote, one small "ROOM · opens…" line above the buttons. */
  .cs-style-letter .lock-hero{display:none}
  .cs-style-letter .cs-host{justify-content:center}
  .cs-style-letter .cs-host-letter{flex-direction:column;gap:var(--s-3);text-align:center}
  .cs-style-letter .cs-face{width:64px;height:64px}
  #tool-modal.coming-soon .cs-style-letter .cs-message,#tool-modal.coming-soon .cs-style-letter .cs-message-letter{font:var(--fw-regular) var(--fs-22)/var(--lh-1-4) var(--brand);font-style:italic;color:var(--hi);text-align:center;margin:var(--s-13) 0 0}
  #tool-modal.coming-soon .cs-style-letter .cs-when{text-align:center;text-transform:uppercase;letter-spacing:var(--ls-009em);font-size:var(--fs-11);margin-top:var(--s-13)}
  #tool-modal.coming-soon .cs-style-letter .cs-details{margin-top:var(--s-4)}
  /* v167. Member view — a slim bar fixed at the top (enterMemberView/
     memberViewGoTo, public/venue.js) and every staff/host control hidden
     off body.member-view alone: never a role change, so nothing here reads
     me.role/me.tier, only CSS visibility. */
  /* v167. --z-alert, not --z-modal: this bar has to outrank every modal it
     itself opens ("At the door" is a real formModal), so it is never the
     thing a card ends up hidden behind.
     v167 design polish. Was a plain full-width solid strip — now a floating
     pill, centred, dark glass like the venue's other floating bars (#dock's
     own .glasspill/--ring, reused rather than a one-off background). */
  /* v167 redesign. A mode strip, not a floating pill: full width along the
     top edge, one row that never wraps, gold so it reads as "you're in a
     mode" rather than as more floor chrome. The whole page (#app, and any
     card it opens) moves down by its height while it is on, so it covers
     nothing a member would see. */
  #member-view-bar{--mv-h:44px;position:fixed;top:0;left:0;right:0;height:var(--mv-h);z-index:var(--z-alert);
    display:grid;grid-template-columns:minmax(0,1fr) auto minmax(0,1fr);align-items:center;gap:var(--s-6);padding:0 var(--s-7);
    background:var(--gold);color:var(--on-gold);box-shadow:0 1px 0 rgba(0,0,0,.25);font:var(--fw-medium) var(--fs-13) var(--ui)}
  #member-view-bar.hidden{display:none}
  #member-view-bar .mv-say{display:flex;align-items:center;gap:var(--s-3);min-width:0;white-space:nowrap}
  #member-view-bar .mv-say .i{width:18px;height:18px;flex:none}
  #member-view-bar .mv-say b{font-weight:var(--fw-semibold)}
  #member-view-bar .mv-room{overflow:hidden;text-overflow:ellipsis;opacity:.72}
  #member-view-bar .mv-room::before{content:"·";margin:0 var(--s-3) 0 var(--s-1)}
  #member-view-bar .mv-seg{display:flex;gap:2px;padding:3px;border-radius:var(--pill);background:rgba(27,20,7,.12)}
  #member-view-bar .mv-seg button{height:30px;padding:0 var(--s-7);border-radius:var(--pill);background:none;color:var(--on-gold);
    font:var(--fw-semibold) var(--fs-13) var(--ui);white-space:nowrap;transition:background var(--t-base) var(--ease),color var(--t-base) var(--ease)}
  #member-view-bar .mv-seg button:hover{background:rgba(27,20,7,.1)}
  #member-view-bar .mv-seg button[aria-pressed="true"]{background:var(--on-gold);color:var(--gold)}
  #member-view-bar .mv-end{justify-self:end;display:flex;align-items:center;gap:var(--s-4)}
  #member-view-bar .mv-esc{font:var(--fw-semibold) var(--fs-11) var(--ui);padding:2px 6px;border-radius:var(--r-sm);box-shadow:inset 0 0 0 1px rgba(27,20,7,.35);opacity:.8}
  #member-view-bar #mv-back{height:30px;padding:0 var(--s-7);border-radius:var(--pill);background:var(--on-gold);color:var(--gold);
    font:var(--fw-semibold) var(--fs-13) var(--ui);white-space:nowrap;transition:transform var(--t-base) var(--ease-press)}
  #member-view-bar #mv-back:active,#member-view-bar .mv-seg button:active{transform:scale(.97)}
  #member-view-bar button:focus-visible{outline:2px solid var(--on-gold);outline-offset:2px}
  #member-view-bar .mv-short{display:none}
  #member-view-bar:not(.hidden){animation:mv-bar-in var(--t-panel) var(--ease-arrive)}
  #member-view-bar.closing{animation:mv-bar-out var(--t-settle) var(--ease-in) forwards}
  @keyframes mv-bar-in{from{transform:translateY(-100%)}}
  @keyframes mv-bar-out{to{transform:translateY(-100%)}}
  @media (max-width:640px){
    #member-view-bar{grid-template-columns:auto 1fr auto;gap:var(--s-4);padding:0 var(--s-5)}
    #member-view-bar .mv-room,#member-view-bar .mv-esc{display:none}
    #member-view-bar .mv-seg{justify-self:center}
    #member-view-bar .mv-seg button,#member-view-bar #mv-back{padding:0 var(--s-5)}
    #member-view-bar .mv-full{display:none}
    #member-view-bar .mv-short{display:inline}
  }
  @media (prefers-reduced-motion:reduce){
    #member-view-bar:not(.hidden),#member-view-bar.closing{animation:none}
  }
  body.member-view #app{top:44px}
  body.member-view .modal{inset:44px 0 0 0}
  body.member-view #adminrail,body.member-view #venueadmin,body.member-view #addroom,
  body.member-view #d-mega,body.member-view #d-door,body.member-view #m-add,
  body.member-view #m-preview,body.member-view #m-author,body.member-view #r-bg,
  body.member-view .pedit,body.member-view #bb-reports{display:none!important}
  .rm.mv-flash{animation:mv-flash 1.6s ease-out}
  @keyframes mv-flash{0%,35%{box-shadow:0 0 0 2px var(--accent) inset}100%{box-shadow:none}}
  @media (prefers-reduced-motion:reduce){.rm.mv-flash{animation:none;box-shadow:0 0 0 2px var(--accent) inset}}
  /* v105. My cards: a drawer from the left, the side the button that opens it is on. */
  #tool-modal.drawer-left{place-items:stretch start}
  #tool-modal.drawer-left .card{width:min(440px,100vw);height:100%;max-height:none;border-radius:0;box-shadow:var(--elev-drawer-left);animation:drawer-in var(--t-panel) var(--ease-arrive)}
  #tool-modal.drawer-left #tool-form{max-height:none;height:100%}
  #tool-modal.drawer-left #tool-content{align-content:start}
  @keyframes drawer-in{from{transform:translateX(-24px);opacity:0}to{transform:none;opacity:1}}
  /* v167. The generic `.modal.closing .card{animation:lh-card-out}` (below,
     v109) is a centred-modal fade+scale — wrong direction for an edge drawer.
     This ID selector outranks that class-only one, so a drawer leaves the
     same edge it arrived from instead of shrinking toward the middle. */
  #tool-modal.drawer-left.closing .card{animation:drawer-out var(--t-settle) var(--ease-in) forwards}
  @keyframes drawer-out{to{transform:translateX(-24px);opacity:0}}
  @media (prefers-reduced-motion:reduce){#tool-modal.drawer-left .card,#tool-modal.drawer-left.closing .card{animation:none}}
  /* v126. Ring added: --group == the drawer's own surface now. */
  .cards-how{padding:var(--s-7);border-radius:var(--r-xl);background:var(--group);box-shadow:var(--ring);color:var(--fg-2);font:var(--fw-regular) var(--fs-14)/var(--lh-1-5) var(--ui)}
  .cards-how p{margin:0 0 var(--s-4)}.cards-how ol{margin:0;padding-left:1.3em}.cards-how li{margin:var(--s-1) 0}
  /* v105. Your own controls, on your own face. */
  #pop .pop-mine{display:grid;grid-template-columns:1fr 1fr;gap:var(--s-3);padding-top:var(--s-6);margin-top:var(--s-6);border-top:1px solid var(--line)}
  /* v126. Ring added: --field == the popover's own surface now. */
  #pop .pop-mine button{display:flex;align-items:center;gap:var(--s-3);height:36px;padding:0 var(--s-5);border-radius:var(--r-xl);font:var(--fw-semibold) var(--fs-12) var(--ui);color:var(--fg-2);background:var(--field);box-shadow:var(--ring);white-space:nowrap}
  #pop .pop-mine button:hover{background:var(--hover);color:var(--hi)}
  #pop .pop-mine .i{width:16px;height:16px;flex:none}
  .modal .link{display:flex;gap:var(--s-4);align-items:center;background:var(--field);box-shadow:var(--ring);border-radius:var(--r);padding:var(--s-5) var(--s-7);font:var(--fw-regular) var(--fs-14) var(--ui);color:var(--ink-3);overflow:hidden;white-space:nowrap;text-overflow:ellipsis;user-select:text}
  .modal .link span{flex:1;overflow:hidden;text-overflow:ellipsis}

  #admin{z-index:var(--z-admin)} #admin .card{width:min(720px,94vw);max-height:90vh;max-height:90dvh;overflow:auto;gap:var(--s-7)}
  #admin .row{align-items:center;justify-content:space-between;flex-wrap:wrap}
  #admin h2{flex:1} #admin p{font-size:var(--fs-14)}
  #admin-add{display:flex;align-items:end;gap:var(--s-6)} #admin-add label{flex:1;min-width:0}
  #admin-results{display:grid;gap:var(--s-4)}
  .admin-entry{display:flex;align-items:center;gap:var(--s-6);padding:var(--s-6);background:var(--glass);border-radius:var(--r-lg)}
  .admin-entry .details{flex:1;min-width:0;overflow-wrap:anywhere}
  .admin-entry small{display:block;color:var(--fg-3)}
  .admin-entry select{width:auto;max-width:140px}
  #admin input[type=checkbox]{accent-color:var(--accent)}
  @media (max-width:540px){#admin-add{align-items:stretch;flex-direction:column}.admin-entry{flex-wrap:wrap}}
  @media (max-width:900px){
    :root{--side:0px} #side{display:none}
    #gate{grid-template-rows:56px 1fr 44px} #gate .gmid{padding:var(--s-6)}
    #gate .card{width:100%;grid-template-columns:1fr;gap:var(--s-10);padding:var(--s-10)}
    #gate .gtop .chip:not(.icon){display:none} #gate .gtop .spacer{display:none} #gate .gtop{justify-content:flex-end}
    .gate-right{padding-top:0} .gate-right h1{font-size:var(--fs-22)}
    #gate .gbot{padding:0 var(--s-8);font-size:var(--fs-15)} #gate .gbot small{display:none}
  }

  input[type=number],input[type=time],input[type=date]{font:inherit;background:var(--glass);color:var(--hi);border:1px solid var(--line-2);border-radius:var(--r-lg);padding:var(--s-6);width:100%}
  select option{background:var(--select-option-bg);color:var(--white)}
  #tool-modal{z-index:var(--z-drawer)}
  /* The card is the frame and does not scroll; only the middle row does, so
     the title stays put at the top and the actions stay put at the bottom
     however long the panel gets. */
  #tool-modal .card{width:min(760px,95vw);max-height:92vh;max-height:92dvh;overflow:hidden;padding:0;gap:0}
  /* A profile is one column of short fields. 760 px is the width the room
     editors need, not this. */
  #tool-modal.narrow .card{width:min(440px,94vw)}
  /* v129. .modal .card's box-shadow puts --ring (an inset ring) on the card
     itself, but the card's own children paint on top of it — .tool-foot has
     an opaque background (--paper-2), so it covered the ring along its own
     edges, leaving the border looking like it stopped above the footer. A
     ::after painted last (after every real child) redraws the ring on top of
     everything, footer included, for every non-sheet tool modal. */
  #tool-modal:not(.sheet) .card{box-shadow:var(--elev-modal-shadow)}
  #tool-modal:not(.sheet) .card::after{content:'';position:absolute;inset:0;border-radius:inherit;box-shadow:var(--ring);pointer-events:none}
  #tool-form{display:grid;grid-template-rows:auto minmax(0,1fr) auto auto;gap:0;max-height:92vh;max-height:92dvh;min-height:0}
  .tool-head{padding:var(--s-13) var(--s-15) var(--s-8)}
  /* v131. The drawer's own group label ("This room"), above the item's own
     title (the room name) — one header row, no second one inside a pane. */
  .tool-title-group{display:block;margin:0 0 var(--s-1);font:var(--fw-semibold) var(--fs-11) var(--ui);letter-spacing:var(--ls-01em);text-transform:uppercase;color:var(--ink-4)}
  #tool-content{display:grid;gap:var(--s-7);padding:0 var(--s-15) var(--s-14);overflow:auto;min-height:0;overscroll-behavior:contain}
  /* v126. --paper-2 was a shade lighter than the card body; now it's the
     same --surface, so the seam is a themed hairline (--line-2) instead of
     a highlight that only ever showed on dark. */
  .tool-foot{display:flex;align-items:center;gap:var(--s-5);flex-wrap:wrap;padding:var(--s-8) var(--s-15);
    background:var(--paper-2);box-shadow:inset 0 1px 0 var(--line-2)}
  #tool-foot-left{display:flex;align-items:center;gap:var(--s-4);flex-wrap:wrap;min-width:0}
  #tool-foot-left:empty{display:none}
  .tool-foot-right{display:flex;align-items:center;gap:var(--s-4);flex:none;margin-left:auto}
  .tool-foot .btn{height:44px;padding:0 var(--s-10);font-size:var(--fs-14)}
  /* v167. Help's "Send a message" is a link styled as the footer button. */
  #o-contact{text-decoration:none;display:inline-flex;align-items:center}
  /* Delete sat at the same weight as Close, in a row of four evenly-loud
     buttons. It keeps the same hit area and gives up the surface, until you
     are on it — then it says what it is. */
  .tool-foot .btn.quiet-danger{background:none;box-shadow:none;color:var(--fg-3);padding:0 var(--s-6)}
  .tool-foot .btn.quiet-danger:hover,.tool-foot .btn.quiet-danger:focus-visible{background:var(--coral-soft);color:var(--coral-icon)}
  @media(max-width:520px){.tool-head{padding:var(--s-9) var(--s-9) var(--s-6)}#tool-content{padding:0 var(--s-9) var(--s-9)}.tool-foot{padding:var(--s-6) var(--s-9)}}
  /* Save sits beside Close, at the top. At the foot it was below whatever the
     panel happened to hold — a grid of 39 faces, in the profile's case. */
  #tool-content p{font-size:var(--fs-14);line-height:var(--lh-1-6);margin:0}
  /* Above the bar rather than inside the scroller: a save that fails names
     the reason next to the button that failed, not somewhere up the page.
     v115: --err, not red — the one colour in this file that still means
     "this broke," kept apart from --gold so a failure never reads as brand. */
  #tool-error{color:var(--err);font-size:var(--fs-14);padding:0 var(--s-15) var(--s-7)}
  #tool-error:empty{display:none}
  .form-grid{display:grid;grid-template-columns:1fr 1fr;gap:var(--s-7)}
  /* The room editor is not a dialog you read, it is a set of dials you turn
     while watching the room change. Two columns of fields in a centred box put
     a blurred scrim over the thing being edited and still ran 1034px of content
     through a 683px window. So: a sheet down the right edge, the floor left
     visible beside it, one section at a time chosen from a rail. Nothing the
     panel holds scrolls out of reach, and every slider previews against the
     room it belongs to. */
  #tool-modal.sheet{place-items:stretch;padding:0;background:none;-webkit-backdrop-filter:none;backdrop-filter:none}
  /* A scrim only where the sheet sits, so the floor keeps its own light. */
  #tool-modal.sheet::before{content:'';position:absolute;inset:0;
    background:linear-gradient(to left,var(--surface-a62) 0,var(--surface-a28) 44%,transparent 72%)}
  /* v167. 600px crowded a rail wide enough for "Room controls"/"Ticket codes"
     against a pane that still has to hold a two-up form-grid — widened to
     ~920px (Munk's live pass), still capped to the viewport so a narrower
     desktop window never grows a sheet past its own screen. */
  #tool-modal.sheet.sheet-narrow .card{width:min(640px,100vw)}
  #tool-modal.sheet .card{position:relative;width:min(920px,100vw);max-width:100vw;height:100%;max-height:none;
    margin-left:auto;border-radius:0;box-shadow:var(--elev-drawer-right);
    animation:sheet-in var(--t-panel) var(--ease-arrive)}
  @keyframes sheet-in{from{transform:translateX(24px);opacity:0}to{transform:none;opacity:1}}
  /* v167. Same fix as #tool-modal.drawer-left above: the drawer this task is
     named after now slides back out its own (right) edge on close instead of
     the centred-modal's generic shrink+fade — read closeTool/hideModal
     (venue.js), which already gives every .modal a 'closing' class for
     exactly this length (--t-settle) before it hides. */
  #tool-modal.sheet.closing .card{animation:sheet-out var(--t-settle) var(--ease-in) forwards}
  @keyframes sheet-out{to{transform:translateX(24px);opacity:0}}
  @media (prefers-reduced-motion:reduce){#tool-modal.sheet .card,#tool-modal.sheet.closing .card{animation:none}}
  #tool-modal.sheet #tool-form{max-height:none;height:100%;grid-template-rows:auto minmax(0,1fr) auto auto}
  /* v167. v166 dropped the You group's own header row entirely (`.you-head`)
     because "YOU" + "Profile" only repeated the rail button already open
     beside it. Munk's live-pass audit (docs/ui-backlog.md) asked for it
     back: every group's header — eyebrow + title + × — needs the same one
     full-width hairline under it separating it from rail+pane, You
     included (it just shows "You" + the item's own name, same as every
     other group's own label + title). `.you-head` and its whole absolute-
     position/visually-hidden-title workaround are gone with it — every
     group is an ordinary `.tool-head` row now, `setHeader` (venue.js) no
     longer toggles a class for it. */
  #tool-modal.sheet .tool-head{padding:var(--s-16) var(--s-14) var(--s-7);box-shadow:inset 0 -1px 0 var(--rule-line)}
  /* v167. Munk: Save is a normal-size button at the right of the footer on
     every Save-backed pane (the general #tool-foot-left:empty rule, above,
     still centres it in every OTHER modal). Delete room moved into the Room
     pane (.danger-block), so nothing else shares this row. */
  #tool-modal.sheet .tool-foot .tool-foot-right{margin:0 0 0 auto;flex:none;justify-content:flex-end}
  #tool-modal.sheet .tool-foot .tool-foot-right #tool-save{width:auto;min-width:120px;height:44px;font-size:var(--fs-14)}
  /* v167. This room's own Delete, last in the Room pane: a quiet bordered
     block, text left, button right. */
  .danger-block{display:flex;align-items:center;justify-content:space-between;gap:var(--s-6);margin-top:var(--s-12);padding:var(--s-7) var(--s-8);border-radius:var(--r-xl);box-shadow:var(--ring)}
  .danger-block h3{margin:0 0 var(--s-2)}
  .danger-block .fine{margin:0}
  .danger-block .btn.quiet-danger{flex:none;background:none;box-shadow:var(--ring);color:var(--coral-icon)}
  .danger-block .btn.quiet-danger:hover,.danger-block .btn.quiet-danger:focus-visible{background:var(--coral-soft)}
  /* The rail is a column, the pane is what it opens. Only the pane scrolls. */
  #tool-modal.sheet #tool-content{grid-template-columns:auto minmax(0,1fr);gap:0;padding:0;overflow:hidden;align-items:stretch}
  /* v133. The desk (openDesk/deskTabsHTML), the one surface that ever needed
     .sheet.wide's wider, rail-less single column, is gone — every one of its
     tabs is an ordinary Venue rail item now (paintVenueModPane), painted
     into the same two-column rail+pane grid every other drawer item uses.
     .sheet.wide and its own #tool-content override are gone with it. */
  /* v125. The rail grid above only knew two children kinds, .editnav and
     .editpane. The author editor puts an intro line and its checklist ahead
     of the rail, and hostingSheet has no rail at all — both fell into the
     grid's cells: the intro took the rail's column, the rail dropped below
     an empty band, and the Knight's host controls sat in a narrow clipped
     column. Anything that is not the rail or a pane now spans the full width
     above them, and a sheet with no rail is a plain scrolling column. */
  #tool-modal.sheet:not(.wide) #tool-content > :not(.editnav):not(.editpane){grid-column:1/-1;padding-inline:var(--s-14)}
  #tool-modal.sheet:not(.wide) #tool-content:has(> .tool-intro){grid-template-rows:auto auto minmax(0,1fr)}
  #tool-modal.sheet:not(.wide) #tool-content > .tool-intro{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-4) var(--s-5);margin:0}
  #tool-modal.sheet:not(.wide) #tool-content > .onboard-steps{margin:var(--s-5) 0 var(--s-7)}
  #tool-modal.sheet:not(.wide) #tool-content:not(:has(> .editnav)){grid-template-columns:minmax(0,1fr);overflow:auto;align-content:start;padding-bottom:var(--s-13)}
  /* v146. The rail is its own scroll area now — independent of the pane
     beside it (.editpane, below, already scrolled on its own) — so a long
     rail no longer forces the whole sheet (#tool-content, the card itself)
     to grow past the viewport and scroll as a page. min-height:0 is load-
     bearing: without it a flex column refuses to shrink below its content's
     height, which is exactly how the rail used to escape its box. */
  /* v167. Wider — "Room controls"/"Ticket codes" never crowd against the
     icon column now (docs/ui-backlog.md, Munk's live pass). */
  .editnav{display:flex;flex-direction:column;gap:var(--s-2);padding:var(--s-3) var(--s-6) var(--s-8) var(--s-9);width:208px;
    box-shadow:var(--hairline-rail-v);overflow-y:auto;min-height:0}
  .editnav button span{overflow:hidden;white-space:nowrap;text-overflow:ellipsis}
  .editnav button{position:relative;display:grid;grid-template-columns:18px minmax(0,1fr);align-items:center;gap:var(--s-5);
    padding:var(--s-18) var(--s-5);border-radius:var(--r-7);text-align:left;color:var(--fg-3);background:none;
    font:var(--fw-semibold) var(--fs-13) var(--ui);transition:background var(--t-base),color var(--t-base)}
  .editnav button:hover{background:var(--hover);color:var(--hi)}
  .editnav button[aria-selected="true"]{background:var(--hover);color:var(--hi)}
  /* The current section is marked by a bar on the rail edge, not by an outline
     that would read as the accent colour a room can be given two panes over. */
  .editnav button[aria-selected="true"]::after{content:'';position:absolute;left:calc(var(--s-5) * -1);top:50%;width:3px;height:18px;
    margin-top:calc(var(--s-18) * -1);border-radius:var(--r-xs);background:var(--accent)}
  .editnav .i{width:18px;height:18px}
  /* v167. Reuses #dock's own .hcount shape (the raised-hand count already
     sits on an icon the same way) rather than a grid column of its own —
     positioned over the rail item's icon, so it reads the same whether the
     rail item shows its label (desktop) or just the icon (phone, below).
     Text comes from paintRailBadge/paintVenueAlertBadges (venue.js); the
     span is always in the markup (railHTML) and toggles .hidden itself. */
  .editnav button .railbadge{position:absolute;left:15px;top:5px;min-width:14px;height:14px;padding:0 3px;border-radius:var(--r-999);
    background:var(--coral-solid);color:var(--on-accent);font:var(--fw-bold) var(--fs-10) var(--ui);line-height:14px;text-align:center}
  /* v167. Same pop as .tabcount above, reusing its keyframe: a rail badge
     going from none to one new item earns a tiny scale-in, not a hard cut. */
  .editnav button .railbadge:not(.hidden){animation:lh-badge-pop var(--t-panel) var(--ease-pop)}
  @media (prefers-reduced-motion:reduce){.editnav button .railbadge:not(.hidden){animation:none}}
  /* v167. THIS ROOM/VENUE/YOU used to render two different ways depending on
     whether the group was open (a plain `.editnav-group` label, fs-11) or
     closed (a `.editnav-group-btn` that ALSO carried `.editnav-group`, so it
     should have inherited the very same fs-11 rule) — but `.editnav button`
     (above) is a class-plus-element selector, which beats a class alone
     regardless of source order, so every closed heading's font quietly came
     from THAT rule instead (fs-13, no letter-spacing) and read a size
     bigger than the one open group beside it. One element now, open or
     closed — `.editnav-group-btn` — carrying its own complete font instead
     of two. `.editnav button` still out-specifies a bare `.editnav-group-
     btn` on its own though (class+element beats class alone whatever the
     property), so every rule below that has to survive it — this one, the
     span override, the chevron rotation — is written `.editnav .editnav-
     group-btn...` on purpose, not out of habit.
     It is also always a real button now: an accordion needs a header to
     press whether its own section is the one showing or not (pressing the
     open one just re-lands on its own first item), and keeping the DOM the
     same shape open/closed is what lets `.editnav-groupbody`, below,
     actually transition instead of snapping — the old design swapped a
     `<span>` for a `<button>` on every switch, which is a different element
     each time and gives a CSS transition nothing to animate between. */
  .editnav .editnav-group-btn{display:flex;align-items:center;justify-content:space-between;gap:var(--s-3);white-space:nowrap;width:100%;
    padding:var(--s-4) var(--s-5);border-radius:var(--r-7);text-align:left;background:none;cursor:pointer;
    font:var(--fw-semibold) var(--fs-11) var(--ui);letter-spacing:var(--ls-01em);text-transform:uppercase;color:var(--ink-4);
    transition:background var(--t-base),color var(--t-base)}
  .editnav .editnav-group-btn:hover,.editnav .editnav-group-btn:focus-visible{background:var(--hover);color:var(--hi)}
  /* v167. Munk: clear divisions between THIS ROOM / VENUE / YOU. A hairline
     and some air above every group after the first, and a small accent dot
     ahead of the label so a heading never reads as one of its own items. */
  .editnav .editnav-groupbody + .editnav-group-btn{margin-top:var(--s-5);position:relative}
  .editnav .editnav-groupbody + .editnav-group-btn::before{content:"";position:absolute;left:var(--s-5);right:var(--s-5);top:calc(-1 * var(--s-3));height:1px;background:var(--rule-line)}
  .editnav .editnav-group-btn span::before{content:"";display:inline-block;width:6px;height:6px;margin-right:var(--s-4);border-radius:50%;background:var(--gold);vertical-align:1px;opacity:.55;transition:opacity var(--t-base)}
  /* Gold on the default Theme swatch (Theme is plain white/black); the
     venue's own colour once an admin picks one. */
  :root:not([data-accent="theme"]) .editnav .editnav-group-btn span::before{background:var(--accent)}
  .editnav .editnav-group-btn[aria-expanded="true"] span::before{opacity:1}
  .editnav-groupbody > .inner{padding-bottom:var(--s-2)}
  .editnav .editnav-group-btn[aria-expanded="true"]{color:var(--hi)}
  .editnav .editnav-group-btn span{overflow:visible;flex:1;width:auto}
  /* The chevron points at the row it opens (right, at rest) and turns to
     point down at it once that row is actually open — the one visible sign
     open/closed no longer comes from a font-size jump. */
  .editnav-group-btn .i{width:12px;height:12px;flex:none;opacity:.7;transition:transform var(--t-base)}
  .editnav-group-btn[aria-expanded="true"] .i{transform:rotate(90deg)}
  /* v167. Every group's body is mounted at once now (openDrawer, venue.js),
     the closed ones `inert` rather than removed, so opening one is a height
     change on an element already in the DOM — not the old paintNav's full
     navEl.innerHTML rebuild, which tore down and recreated every node on
     every switch and left an accordion with nothing to transition FROM.
     `.inner` is the single child actually holding the group's own items;
     `overflow:hidden`+`min-height:0` on IT (not just the 0fr/1fr track) is
     what lets a collapsed group settle all the way to zero rather than
     stopping a few px short (the same residual #add-options, the floor's
     own + menu, already had to guard against the same way). */
  .editnav-groupbody{display:grid;grid-template-rows:0fr;overflow:hidden;min-width:0;min-height:0;
    transition:grid-template-rows var(--t-panel) var(--ease-move)}
  .editnav-groupbody[data-open]{grid-template-rows:1fr}
  .editnav-groupbody > .inner{display:flex;flex-direction:column;gap:var(--s-1);overflow:hidden;min-height:0}
  @media (prefers-reduced-motion:reduce){.editnav-groupbody,.editnav-group-btn .i{transition:none}}
  /* v133. A quiet sub-heading ahead of the Venue rail's Reports…Codes run
     (Moderation) — the same eyebrow as .editnav-group-btn, one size down,
     so it reads as a subheading inside the group rather than a second group
     of its own. */
  .editnav-sublabel{display:block;margin-top:var(--s-4);padding:0 var(--s-5);font:var(--fw-semibold) var(--fs-10) var(--ui);letter-spacing:var(--ls-009em);text-transform:uppercase;color:var(--ink-4);opacity:.72}
  .editpane{display:none;gap:var(--s-7);padding:var(--s-7) var(--s-14) var(--s-13) var(--s-10);overflow-y:auto;overflow-x:hidden;min-height:0;overscroll-behavior:contain;align-content:start}
  .editpane[data-open]{display:grid}
  /* v167. Switching drawer items, or Rooms/Chat, fades the new pane in with
     a 4px rise instead of a hard cut. Only fires when a pane goes from
     hidden to shown; repainting a pane's contents never replays it. */
  .editpane[data-open],#side > .pane:not(.hidden){animation:lh-pane-in var(--t-panel) var(--ease-arrive)}
  @keyframes lh-pane-in{from{opacity:0;transform:translateY(4px)}}
  @media (prefers-reduced-motion: reduce){.editpane[data-open],#side > .pane:not(.hidden){animation:none}}
  .editpane > h3{margin:0;font:var(--fw-semibold) var(--fs-12) var(--ui);letter-spacing:var(--ls-01em);text-transform:uppercase;color:var(--ink-4)}
  .editpane > h3 ~ h3{margin-top:var(--s-7)}
  .editpane .idrow{display:grid;grid-template-columns:1fr 76px;gap:var(--s-6)}
  /* v167. A grid item's own min-width defaults to its content's min-content
     size, not 0 — so a native time input, which refuses to shrink much
     below ~90px, could win a tug-of-war with a fixed 76px column and push
     the row (and the pane's own scroller) past the sheet's edge. min-width:0
     lets the field actually take the column it was given instead of its
     own preferred size; `.idrow-eq` is for a pair that both need real room
     (Countdown's date/time, authorRoomPaneHTML) rather than a fixed 76px
     column sized for the Room/Icon row above it. overflow-x:hidden on
     `.editpane`, above, is the belt, not the fix. */
  .editpane .idrow > label{min-width:0}
  .editpane .idrow input{min-width:0}
  .editpane .idrow.idrow-eq{grid-template-columns:minmax(0,1fr) minmax(0,1fr)}
  .editpane .rule{height:1px;background:var(--rule-line);margin:var(--s-1) 0}
  /* `.fine` is centred and ruled off because it is a footnote under a gate
     card. In a settings pane it is the note under one control, so it reads
     left with everything else it is about. */
  .editpane .fine{font:var(--fw-regular) var(--fs-12-5)/var(--lh-1-5) var(--ui);color:var(--fg-3);text-align:left;border-top:0;padding-top:0;margin:calc(var(--s-2) * -1) 0 0}
  /* Under 760px the sheet takes the screen and the rail lies down as a tab row:
     there is no floor left to watch beside it. */
  @media(max-width:760px){
    #tool-modal.sheet::before{background:var(--surface-a72)}
    #tool-modal.sheet .card{width:100vw}
    #tool-modal.sheet #tool-content{grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr)}
    /* v125. Intro, checklist, then the rail lying down, then the pane. */
    #tool-modal.sheet:not(.wide) #tool-content:has(> .tool-intro){grid-template-rows:auto auto auto minmax(0,1fr)}
    #tool-modal.sheet:not(.wide) #tool-content > :not(.editnav):not(.editpane){padding-inline:var(--s-9)}
    .editnav{flex-direction:row;width:auto;gap:var(--s-2);padding:0 var(--s-7) var(--s-5);overflow:auto;
      box-shadow:var(--hairline-rail-h);align-items:center;
      /* v167b. QA: nothing hinted that more icons sat off-screen. A soft fade
         at the right edge says "scroll for more". */
      -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 40px),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 40px),transparent)}
    .editnav button{grid-template-columns:18px;gap:0;padding:0 var(--s-7);min-height:44px;justify-items:center;flex:none}
    .editnav button>span:not(.railbadge){display:none}
    .editnav button[aria-selected="true"]::after{left:50%;top:auto;bottom:calc(var(--s-3) * -1);margin:0 0 0 calc(var(--s-5) * -1);width:20px;height:3px}
    .editpane{padding:var(--s-8) var(--s-9) var(--s-14)}
    /* v146. A group's own heading sits in the row the same way an ordinary
       rail button does — a fixed-width pill — since pressing it is exactly
       what an ordinary rail button does. (v167: `.editnav` prefix, same
       specificity reason as the desktop rule above — `.editnav button`'s
       own mobile override, just above, is a class+element and would
       otherwise win.) */
    .editnav .editnav-group-btn{flex:none;width:auto;min-height:44px;padding:0 var(--s-7);margin:0;display:flex;align-items:center;white-space:nowrap}
    /* v167. A group heading has no icon, only its chevron — unlike an
       ordinary rail item (Room, Atmosphere, …), hiding its own label here
       leaves nothing at all naming the group. #tool-modal's own ID is what
       it takes to beat `.editnav button>span:not(.railbadge){display:none}`,
       above (a class+element+:not still outranks a plain class pair). */
    #tool-modal .editnav-group-btn span{display:block}
    /* v167. The rail lies down into one row here; a group's own body lies
       down with it — the same 0fr/1fr trick, turned on its side, collapsing
       WIDTH instead of height. */
    /* v167. flex-shrink:0 is load-bearing here: `.inner`'s own overflow:hidden
       + min-width:0 (shared with desktop, needed to let a CLOSED group
       collapse to true 0 width) also hands the flex row above an automatic
       minimum size of 0 to shrink against — without this, an OPEN group's
       body (grid-template-columns:1fr, otherwise sized to its own content)
       got squeezed down to a few px instead, whenever the row's three
       header pills plus it did not all fit the viewport at once. */
    .editnav-groupbody{grid-template-rows:none;grid-template-columns:0fr;flex:none;transition:grid-template-columns var(--t-panel) var(--ease-move)}
    .editnav-groupbody[data-open]{grid-template-columns:1fr}
    .editnav-groupbody > .inner{flex-direction:row;align-items:center;gap:var(--s-2);min-width:0}
    /* Same treatment for the Moderation sub-label — a quiet divider in the
       lying-down row, not a line of its own. */
    .editnav-sublabel{flex:none;display:flex;align-items:center;margin:0;padding:0 var(--s-6) 0 0;border-right:1px solid var(--rule-line);white-space:nowrap}
  }
  /* SpatialChat-shaped controls: a switch reads as on/off at a glance, a
     slider shows the value you are choosing, a dot shows where you are putting it. */
  .sw{display:flex;align-items:center;justify-content:space-between;gap:var(--s-7);padding:var(--s-20) 0;font:var(--fw-medium) var(--fs-14) var(--ui);color:var(--fg)}
  .sw small{display:block;font:var(--fw-regular) var(--fs-12) var(--ui);color:var(--fg-3);margin-top:var(--s-1)}
  /* v167. Was --group (== --surface, the same fill as the sheet the switch
     sits on) — the off-state track read only through --ring's 1px inset
     border, too faint in light mode to call "clearly visible". --ctrl-track
     gives it real contrast against the paper in both themes; --ring stays,
     for the crisp edge it already drew. */
  .sw input{appearance:none;flex:none;width:44px;height:26px;border-radius:var(--r-999);background:var(--ctrl-track);box-shadow:var(--ring);position:relative;cursor:pointer;transition:background var(--t-base)}
  .sw input::after{content:'';position:absolute;left:var(--s-21);top:var(--s-21);width:20px;height:20px;border-radius:50%;background:var(--white);transition:transform var(--t-base)}
  .sw input:checked{background:var(--accent)}
  /* v115. The knob was white on red (already marginal); white on gold is
     ~2:1, so the checked knob goes dark instead of the track going white. */
  .sw input:checked::after{transform:translateX(18px);background:var(--on-accent)}
  .sw input:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
  /* The value belongs ON the label row, at the end of it. `float` inside a grid
     does nothing, so it used to drop to a line of its own and read as a stray
     number. And the track has to SHOW the value: `accent-color` paints the whole
     bar one colour in Chromium, so 0% and 55% looked identical and the number
     was the only way to tell. The fill is drawn here from --p, set on input. */
  .sld{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:baseline;gap:var(--s-25) var(--s-5);padding:var(--s-18) 0;
    font:var(--fw-medium) var(--fs-13) var(--ui);color:var(--fg-2)}
  .sld .v{justify-self:end;color:var(--fg-3);font-variant-numeric:tabular-nums}
  .sld input{grid-column:1/-1;width:100%;appearance:none;height:22px;background:none;cursor:pointer}
  /* v167. Was --on-dark-14 — invisible in light mode, a white-14% track on
     white paper (docs/ui-backlog.md's light-mode pass). --ctrl-track flips
     with the theme the same way --rule-line does, above. */
  .sld input::-webkit-slider-runnable-track{height:6px;border-radius:var(--r-99);
    background:linear-gradient(90deg,var(--accent) calc(var(--p,0) * 100%),var(--ctrl-track) 0)}
  .sld input::-moz-range-track{height:6px;border-radius:var(--r-99);background:var(--ctrl-track)}
  .sld input::-moz-range-progress{height:6px;border-radius:var(--r-99);background:var(--accent)}
  .sld input::-webkit-slider-thumb{appearance:none;width:16px;height:16px;margin-top:calc(var(--s-17) * -1);border-radius:50%;
    background:var(--white);box-shadow:var(--elev-slider-thumb);transition:transform var(--t-tap)}
  .sld input::-moz-range-thumb{width:16px;height:16px;border:0;border-radius:50%;background:var(--white);
    box-shadow:var(--elev-slider-thumb)}
  .sld input:active::-webkit-slider-thumb{transform:scale(1.15)}
  .sld input:focus-visible{outline:2px solid var(--accent);outline-offset:4px;border-radius:var(--r-99)}
  .seg{display:grid;gap:var(--s-25);padding:var(--s-18) 0;font:var(--fw-medium) var(--fs-13) var(--ui);color:var(--fg-2)}
  /* v126. Ring added: a segmented control still has to read as a control
     once its fill matches the panel it sits on. */
  .seg .opts{display:flex;gap:var(--s-2);padding:var(--s-21);border-radius:var(--pill);background:var(--group);box-shadow:var(--ring)}
  .seg button{flex:1;padding:var(--s-25) var(--s-5);border-radius:var(--pill);font:var(--fw-semibold) var(--fs-12) var(--ui);color:var(--fg-3);background:none}
  .seg button[aria-pressed="true"]{background:var(--accent);color:var(--on-accent)}
  /* One column of rows, each row a question and its answers as icons. A row of
     five dropdowns hid the setting inside a closed list; this shows every
     available answer and which one is on, without opening anything. */
  .permlist{display:grid;gap:var(--s-2)}
  .permrow{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--s-7);
    padding:var(--s-20) 0;border-bottom:1px solid var(--on-dark-07)}
  .permrow:last-child{border-bottom:0}
  .permrow .q{min-width:0;font:var(--fw-semibold) var(--fs-14) var(--ui);color:var(--fg-2)}
  .permrow .q small{display:block;margin-top:var(--s-1);font:var(--fw-regular) var(--fs-12) var(--ui);color:var(--fg-3)}
  /* v126. Ring added, same reason as .seg .opts above. */
  .permrow .opts{display:flex;gap:var(--s-21);padding:var(--s-21);border-radius:var(--pill);background:var(--group);box-shadow:var(--ring);flex:none}
  .permrow .opts button{width:42px;height:36px;border-radius:var(--pill);display:grid;place-items:center;color:var(--fg-3);
    background:none;transition:background var(--t-base),color var(--t-base)}
  .permrow .opts button:hover{color:var(--hi)}
  .permrow .opts button[aria-pressed="true"]{background:var(--coral-solid);color:var(--on-accent)}
  .permrow .opts .i{width:17px;height:17px}
  .spawn{display:grid;gap:var(--s-4);padding:var(--s-18) 0;font:var(--fw-medium) var(--fs-13) var(--ui);color:var(--fg-2)}
  .spawn .map{position:relative;border-radius:var(--r);overflow:hidden;background:var(--group);box-shadow:var(--ring);cursor:crosshair;touch-action:none}
  .spawn .map img{display:block;width:100%;height:auto;pointer-events:none;-webkit-user-drag:none}
  .spawn .dot{position:absolute;width:26px;height:26px;margin:calc(var(--s-26) * -1) 0 0 calc(var(--s-26) * -1);border-radius:50%;background:var(--accent);border:3px solid var(--white);box-shadow:var(--elev-spawn-dot);pointer-events:none}
  .bgpick{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:var(--s-4);padding:var(--s-3) 0}
  /* v167. Same tile press as the face gallery above — a background swap had
     no hover/press feel before the outline snapped on. */
  .bgpick button{padding:0;border-radius:var(--r);overflow:hidden;box-shadow:var(--ring);aspect-ratio:16/9;background:var(--group);transition:transform var(--t-base) var(--ease-press)}
  .bgpick button:active{transform:scale(.97)}
  .bgpick button[aria-pressed="true"]{outline:3px solid var(--accent);outline-offset:1px}
  /* An uploaded background sits in the same grid as a built-in one, with the
     one thing a built-in cannot have: a way to remove it. */
  .bgpick .bgtile{position:relative;display:block}
  .bgpick .bgtile button[data-v]{width:100%}
  .bgpick .bgdel{position:absolute;right:var(--s-17);top:var(--s-17);height:24px;min-width:24px;padding:0 var(--s-3);aspect-ratio:auto;
    border-radius:var(--r-2xl);display:grid;grid-auto-flow:column;align-items:center;justify-content:center;gap:var(--s-2);
    color:var(--white);background:var(--deep-a82);box-shadow:var(--ring-bgdel);
    font:var(--fw-semibold) var(--fs-11) var(--ui);transition:background var(--t-base)}
  .bgpick .bgdel:hover,.bgpick .bgdel:focus-visible{background:var(--accent)}
  /* Armed state: one click asks, the second deletes. The control is on screen
     all the time now, and an upload does not come back. */
  .bgpick .bgdel.arm{background:var(--accent);padding:0 var(--s-18)}
  .bgpick .bgdel .i{width:12px;height:12px}
  .bgpick img{width:100%;height:100%;object-fit:cover;display:block}
  /* Thirteen small tiles rather than the background grid's four wide ones: a
     preset is named, not previewed, and the preview happens on the real room
     behind the panel the moment one is pressed. */
  .atmospick{display:grid;grid-template-columns:repeat(auto-fill,minmax(84px,1fr));gap:var(--s-4);padding:var(--s-3) 0}
  .atmospick button{display:grid;justify-items:center;align-content:center;gap:var(--s-3);padding:var(--s-6) var(--s-3);border-radius:var(--r);
    box-shadow:var(--ring);background:var(--group);color:var(--fg-3);transition:background var(--t-base),color var(--t-base),transform var(--t-base) var(--ease-press)}
  /* v167. Press feedback, matching the other tile pickers (bgpick, gal-grid). */
  .atmospick button:active{transform:scale(.97)}
  .atmospick button:hover{background:var(--hover);color:var(--hi)}
  .atmospick button[aria-pressed="true"]{outline:3px solid var(--accent);outline-offset:1px;color:var(--hi);background:var(--hover)}
  .atmospick .nm{font:var(--fw-semibold) var(--fs-12) var(--ui);letter-spacing:var(--ls-01px)}
  /* The tile only has to name a preset: the preview happens on the floor
     beside the sheet the moment one is pressed. */
  .editpane .atmospick{grid-template-columns:repeat(4,1fr);gap:var(--s-3);padding:0}
  .editpane .atmospick button{padding:var(--s-5) var(--s-2);gap:var(--s-17);border-radius:var(--r-lg)}
  .editpane .atmospick button[aria-pressed="true"]{outline-width:2px}
  .editpane .atmospick .i{width:18px;height:18px}
  .editpane .atmospick .nm{font-size:var(--fs-11);letter-spacing:0}
  .editpane .sld,.editpane .sw,.editpane .seg,.editpane .spawn{padding:0}
  /* v136. Venue › Details' "Venue colour" row — round swatches, radiogroup
     semantics (role/aria-checked wired in venue.js, arrow keys move focus the
     same way the rail's own tablist does). Each swatch carries its own fixed
     preview colour via an inline style (VENUE_ACCENTS' hex) rather than the
     live --accent token, so every option stays visible regardless of which
     one is currently applied to the page. */
  .accentswatches{display:flex;flex-wrap:wrap;gap:var(--s-6);padding:var(--s-3) 0 var(--s-2)}
  .accentswatch{width:34px;height:34px;padding:0;border:0;border-radius:50%;box-shadow:var(--ring);cursor:pointer;transition:box-shadow var(--t-base),transform var(--t-base) var(--ease-press)}
  .accentswatch:hover{box-shadow:var(--ring),0 0 0 3px var(--hover)}
  .accentswatch:active{transform:scale(.97)}
  .accentswatch[aria-checked="true"]{box-shadow:var(--ring-selected-accent)}
  .accentswatch:focus-visible{outline:2px solid var(--focus);outline-offset:3px}
  /* Theme has no single hex of its own — the split diagonal says "follows
     dark/light" the way a solid tile says "this specific colour" for the rest. */
  .accentswatch-theme{background:linear-gradient(135deg,var(--white) 50%,var(--ink) 50%)}
  /* A venue is allowed sixty pictures. Three across and capped at about three
     rows, the gallery scrolls inside itself and the upload, the sliders and the
     landing map underneath stay where they were — twenty images used to push
     all of that a thousand pixels down a pane with one scrollbar. */
  .editpane .bgpick{grid-template-columns:repeat(3,1fr);gap:var(--s-4);padding:var(--s-1);max-height:242px;overflow:auto;
    overscroll-behavior:contain;scrollbar-width:thin;scrollbar-color:var(--scroll) transparent}
  .bghead{display:flex;align-items:baseline;justify-content:space-between;gap:var(--s-6)}
  .bgcount{font:var(--fw-medium) var(--fs-11) var(--ui);color:var(--fg-3);font-variant-numeric:tabular-nums}
  /* A picture a room is standing on says so where its delete would have been,
     rather than leaving a gap that reads as a tile that failed to draw. */
  .bgused{position:absolute;right:var(--s-17);top:var(--s-17);display:grid;place-items:center;width:24px;height:24px;
    border-radius:var(--r-md);background:var(--surface-a70);color:var(--fg-3)}
  .bgused .i{width:12px;height:12px}
  .editpane .accentpick{margin:0}
  .tool-actions{display:flex;gap:var(--s-4);flex-wrap:wrap}
  /* v104. The door's confirm step: one primary action, two quiet exits. */
  /* v106. Centred, the mark on top, one heading, one button. The exits are text. */
  .auth-confirm{display:grid;justify-items:center;text-align:center;gap:var(--s-5)}
  #gate .gate-right:has(.auth-confirm) #gate-title{display:none}
  .auth-logo{display:block;width:200px;height:auto;margin:0 auto var(--s-4);filter:var(--drop-logo)}
  .auth-confirm h2{margin:0;font:var(--fw-regular) var(--fs-30)/var(--lh-1-15) var(--brand);letter-spacing:var(--ls-02px);color:var(--hi)}
  .auth-confirm .tool-intro{margin:0;max-width:30ch;color:var(--fg-2)}
  .auth-confirm .desk-claim:empty{display:none}
  .auth-primary{width:100%;margin-top:var(--s-7)}
  .auth-primary .btn{width:100%;height:56px;font-size:var(--fs-17)}
  .auth-secondary{display:flex;flex-wrap:wrap;justify-content:center;align-items:center;gap:var(--s-2) var(--s-8);margin-top:var(--s-5)}
  .auth-secondary .btn{flex:none;height:auto;padding:var(--s-2) var(--s-1);border-radius:var(--r-sm);background:none;box-shadow:none;color:var(--fg-3);font:var(--fw-medium) var(--fs-14) var(--ui);text-decoration:underline;text-decoration-color:var(--line-2);text-underline-offset:4px;white-space:nowrap}
  .auth-secondary .btn:hover,.auth-secondary .btn:focus-visible{color:var(--hi);text-decoration-color:currentColor;transform:none}
  /* v108. The email step is the same card. Fields run the card's full width
     and read left to right; only the words around them are centred. */
  .auth-email label.f{width:100%;text-align:left;margin-top:var(--s-5)}
  .auth-email label.f input{height:52px;font-size:var(--fs-16)}
  .auth-email .auth-primary{margin-top:var(--s-2)}
  /* v163. One merged code field, styled like any other field in the card
     (it used to be the access-code box's own rule; the OTP box no longer
     gets a second, centred, button-carrying layout of its own — there is
     only ever one submit button on this card now). */
  #email-code-box{width:100%}
  #email-code-box[hidden]{display:none}
  /* Once the code box is open, "I have a code" has done its job; the one
     submit button on the card stays exactly as solid and full-width as it
     always was — it just does more now (see `submit`, venue.js). */
  .auth-email:has(#email-code-box:not([hidden])) .auth-have{display:none}
  .auth-secondary .btn + .btn{position:relative}
  .auth-secondary .btn + .btn::before{content:"";position:absolute;left:calc(var(--s-8) / -2 - 1.5px);top:50%;width:3px;height:3px;border-radius:50%;background:var(--fg-4)}
  .tool-actions .btn,.setup-row .btn{height:40px;padding:0 var(--s-8);font-size:var(--fs-13)}
  .setup-rooms{display:grid;gap:var(--s-4)}.setup-row{display:flex;align-items:center;justify-content:space-between;gap:var(--s-6);padding:var(--s-6);background:var(--glass);border-radius:var(--r-lg)}
  .setup-row span{min-width:0;overflow-wrap:anywhere}.setup-row small{display:block;color:var(--fg-3);font-size:var(--fs-12);margin-top:var(--s-2)}
  /* v37. Three rows show; the rest scroll. The tile is a seventh of the
     width, so three rows is that, three times, plus the gaps. */
  .profile-avatars-wrap{container-type:inline-size;overflow:auto;max-height:calc(3 * (100cqw - 72px) / 7 + 24px + 12px);padding:var(--s-3);margin:0 calc(var(--s-3) * -1);scrollbar-width:thin}
  .profile-avatars{display:grid;grid-template-columns:repeat(7,1fr);gap:var(--s-6)}
  .profile-avatars button{display:block;position:relative;padding:0;aspect-ratio:1;border-radius:50%;line-height:0;cursor:pointer;transition:transform var(--t-base)}
  .profile-avatars button:hover{transform:translateY(-2px)}
  .profile-avatars .av{display:block;width:100%;height:100%;aspect-ratio:1;filter:saturate(.8) brightness(.88);transition:filter var(--t-base),box-shadow var(--t-base)}
  .profile-avatars button:hover .av,.profile-avatars button[aria-pressed=true] .av{filter:none}
  /* The ring is drawn on the picture itself, so it sits on its edge whatever
     the button around it measures. The badge says chosen without the ring. */
  .profile-avatars button[aria-pressed=true] .av{box-shadow:var(--ring-selected-accent)}
  .profile-avatars button[aria-pressed=true]::after{content:"✓";position:absolute;right:calc(var(--s-21) * -1);bottom:calc(var(--s-21) * -1);width:22px;height:22px;border-radius:50%;
    background:var(--accent);color:var(--on-accent);font:var(--fw-bold) var(--fs-13)/var(--lh-22px) var(--ui);text-align:center;box-shadow:0 0 0 3px var(--paper)}
  .profile-picked{margin:var(--s-5) 0 0;font:var(--fw-medium) var(--fs-13) var(--ui);color:var(--fg-3)}.profile-picked b{color:var(--fg)}
  .person-card .av{width:85px;height:85px}.person-card small{color:var(--fg-3)}
  #stage.drawing{cursor:crosshair;touch-action:none}#world,.bubble{touch-action:none}
  .pin.video{background:var(--deep);border-radius:var(--r);overflow:hidden;display:flex;flex-direction:column;box-shadow:var(--elev-pin)}
  .pin.video iframe{width:100%;flex:1;min-height:0;border:0;display:block}
  .pin.video .lab{position:static;order:-1;left:auto;top:auto;border-radius:0;padding:var(--s-18) var(--s-7);font:var(--fw-semibold) var(--fs-12) var(--ui);letter-spacing:var(--ls-001em);color:var(--white);background:var(--group);border-bottom:1px solid var(--ring-line,rgba(255,255,255,.10));pointer-events:none}
  .pin.video .far{position:absolute;inset:auto auto var(--s-4) var(--s-4);padding:var(--s-21) var(--s-4);border-radius:var(--r-999);font:var(--fw-medium) var(--fs-11) var(--ui);color:var(--white);background:var(--deep-a72);pointer-events:none}
  /* Arrange mode: every placed thing becomes draggable, and a video stops
     swallowing the pointer so it can be moved by its own surface. */
  /* A dashed outline said "this is being edited". It did not say "hold here". The
     frame is a band you can see and aim at: 16px of it all the way round, in the
     accent, with the cursor to match. The middle is left alone so the words are
     still words. */
  /* .pin carries overflow:hidden - a frame drawn outside the box was clipped away
     to a hairline, and the chip above it never appeared at all. While arranging,
     the box lets its own frame out. The note's body keeps its own overflow, so it
     still scrolls. */
  .editing-pins .pin{outline:none;cursor:default;touch-action:none;overflow:visible}
  /* The band has to BE the handle. It was one element with pointer-events:none,
     so a press on the part of it outside the pin's own box reached the stage
     underneath and panned the whole floor - the thing that looked most grabbable
     was the only thing that was not. It is four strips now, each of which takes
     the pointer and hands it to the pin. The middle is left alone, so the words
     are still words. */
  .editing-pins .pin .mv{position:absolute;inset:calc(var(--s-8) * -1);display:block;border-radius:var(--r-4xl);pointer-events:none;z-index:var(--z-2);
    box-shadow:0 0 0 2px var(--accent),inset 0 0 0 2px var(--accent)}
  .editing-pins .pin .mv i{position:absolute;background:var(--accent-a50);pointer-events:auto;cursor:grab;touch-action:none}
  .editing-pins .pin .mv i:nth-child(1){left:0;right:0;top:0;height:16px;border-radius:var(--r-4xl) var(--r-4xl) 0 0}
  .editing-pins .pin .mv i:nth-child(2){left:0;right:0;bottom:0;height:16px;border-radius:0 0 var(--r-4xl) var(--r-4xl)}
  .editing-pins .pin .mv i:nth-child(3){top:var(--s-8);bottom:var(--s-8);left:0;width:16px}
  .editing-pins .pin .mv i:nth-child(4){top:var(--s-8);bottom:var(--s-8);right:0;width:16px}
  .editing-pins .pin.dragging .mv i{cursor:grabbing;background:var(--accent-a72)}
  .pin .mv{display:none}

  /* The band is the handle, so it wears the handle's cursor. */
  .editing-pins .pin.on-edge{cursor:grab}
  .editing-pins .pin.dragging{cursor:grabbing}
  /* And a chip that names the gesture, in case the band is not enough. */
  .editing-pins .pin .mvtag{position:absolute;left:50%;top:calc(var(--s-8) * -1);transform:translate(-50%,-100%);
    display:flex;align-items:center;gap:var(--s-3);height:26px;padding:0 var(--s-20);border-radius:var(--pill);
    background:var(--accent);color:var(--on-accent);font:var(--fw-semibold) var(--fs-11) var(--ui);letter-spacing:var(--ls-004em);white-space:nowrap;
    pointer-events:none;z-index:var(--z-3)}
  .pin .mvtag{display:none}
  .editing-pins .pin .mvtag svg{width:12px;height:12px}
  .editing-pins .pin iframe{pointer-events:none}
  /* Which video is the pencil about to open? A frame sits flush on the
     background, and with the pointer over it nothing changed — on a dark
     picture the edges of a dark player are not edges. An ambience disc cannot
     take the hover itself (the pin passes the pointer through to the floor, and
     its clip-path would cut a shadow off anyway), so the pin box answers for
     the clip inside it. */
  #world.editing-pins .pin.video{transition:box-shadow var(--t-base)}
  #world.editing-pins .pin.video:hover,
  #world.editing-pins .pin.video.ambience:has(.amb-clip:hover){
    box-shadow:var(--ring-ambience-hover)}
  .editing-pins .pin.dragging{opacity:.85;cursor:grabbing}
  #arrangebar{position:absolute;left:50%;bottom:var(--s-22);transform:translateX(-50%);z-index:var(--z-bar);display:flex;align-items:center;gap:var(--s-6);
    padding:var(--s-5) var(--s-7);border-radius:var(--pill);background:var(--glass-solid);box-shadow:var(--ring);color:var(--hi);font:var(--fw-semibold) var(--fs-13) var(--ui)}
  #arrangebar button{height:34px;padding:0 var(--s-8);border-radius:var(--pill);background:var(--solid);color:var(--on-solid);font:var(--fw-semibold) var(--fs-13) var(--ui)}
  #arrangebar .plain{background:transparent;color:var(--hi);box-shadow:var(--ring)}
  /* Direct handling: the text on a note is the note, and a corner drags its size.
     A field in a modal is for the things that are not text - a video's address,
     who hears it - and nothing else. */
  .editing-pins .pin [contenteditable]{pointer-events:auto;outline:none;cursor:text}
  .editing-pins .pin [contenteditable]:focus{box-shadow:inset 0 0 0 2px var(--accent)}
  .editing-pins .pin [contenteditable]:empty::before{content:attr(data-ph);opacity:.45}
  .pin .grip{position:absolute;right:0;bottom:0;width:38px;height:38px;display:none;cursor:nwse-resize;touch-action:none;z-index:var(--z-5)}
  .pin .grip::after{content:"";position:absolute;right:var(--s-25);bottom:var(--s-25);width:15px;height:15px;
    border-right:3px solid var(--white);border-bottom:3px solid var(--white);opacity:.85;filter:drop-shadow(0 1px 2px var(--shade-70))}
  .editing-pins .pin .grip{display:block}
  /* One field, on the object, for the one value that cannot be typed as content. */
  .pin .addr{position:absolute;left:var(--s-4);right:var(--s-4);bottom:var(--s-4);display:none;height:32px;padding:0 var(--s-5);z-index:var(--z-4);
    border:0;border-radius:var(--r);font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--white);background:var(--shade-72);box-shadow:var(--ring);
    transition:box-shadow var(--t-settle),background var(--t-settle)}
  /* Taken. A green rim and a tick drawn in the field's own right-hand end, held
     long enough to be read before the redraw takes the field away. */
  .pin .addr.addr-ok,#pinmenu .maddr.addr-ok{padding-right:var(--s-29);
    box-shadow:var(--ring-valid);
    background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2346B87A' stroke-width='3' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M4 12.5l5 5L20 6.5'/%3E%3C/svg%3E");
    background-repeat:no-repeat;background-position:right 9px center;background-size:13px 13px}
  /* Refused. The rim stays until it is fixed — a shake that ends leaves a field
     that looks exactly like one nobody has touched. */
  .pin .addr.addr-bad,#pinmenu .maddr.addr-bad{box-shadow:inset 0 0 0 1.5px var(--coral-icon);
    animation:addr-no var(--t-340)}
  @keyframes addr-no{
    0%,100%{transform:translateX(0)} 20%{transform:translateX(-4px)}
    45%{transform:translateX(4px)} 70%{transform:translateX(-2px)}
  }
  .editing-pins .pin .addr{display:block}
  .pin.video.blank{display:grid;place-items:center;background:var(--deep);color:var(--fg-3);font:var(--fw-semibold) var(--fs-13) var(--ui)}
  /* Ambience: the same video pin, worn as a disc. The box keeps its real
     size — the player has to stay in the DOM at a real size to keep playing,
     and moving or resizing it later has to mean what it always meant — so
     what changes is the clip, not the geometry. Both states are inset() with
     a radius, because clip-path only interpolates between the same shape
     function; circle() to inset() would snap instead of open. The clip also
     takes the pointer with it, so the floor around the disc stays floor. */
  /* The clip is wider than the button on purpose: the rings pulse out into
     that margin, and anything drawn past the clip is simply gone. */
  .pin.video.ambience{background:transparent;box-shadow:none;pointer-events:none}
  /* 54px is where the disc has always been and stays the default; --amb-r is
     the same number made adjustable, because a hub in a big empty room and a
     hub tucked beside a doorway are not the same size of thing. It is written
     onto the pin from p.discR. The pin's own box does not move with it — the
     player has to stay in the DOM at a real size to keep playing — so what
     changes is the clip, exactly as it was when the clip was a constant. */
  .pin.video.ambience .amb-clip{position:absolute;inset:0;pointer-events:auto;background:var(--deep);border-radius:var(--r-2xl);
    clip-path:inset(calc(50% - var(--amb-r,54px)) calc(50% - var(--amb-r,54px)) round 999px);
    transition:clip-path var(--t-340) var(--ease-press)}
  .pin.video.ambience.open .amb-clip{clip-path:inset(0 round 12px)}
  .pin.video.ambience .pedit{pointer-events:auto}
  .pin.video.ambience .amb-veil{position:absolute;inset:0;background:var(--deep-a84);pointer-events:none;transition:opacity var(--t-travel)}
  .pin.video.ambience.open .amb-veil{opacity:0}
  /* Minified, a hub is not a video and does not pretend to be one. It used to
     show the picture through an 84% veil: a video nobody could make out, still
     decoded and composited every frame. The frame is faded out instead and the
     disc is its own dark surface — what moves on it is the waveform. Opacity,
     not display: the player has to stay real or the sound stops with it. */
  .pin.video.ambience .amb-clip iframe{opacity:0;transition:opacity var(--t-travel)}
  .pin.video.ambience.open .amb-clip iframe{opacity:1}
  .pin.video.ambience .amb-clip iframe{position:absolute;inset:0;width:100%;height:100%}
  .pin.video.ambience .amb-face{position:absolute;inset:0;display:grid;place-items:center;pointer-events:none;
    transition:opacity var(--t-240)}
  /* The face passes the pointer through; the button alone catches it, so
     the rest of the disc is still floor. */
  .pin.video.ambience .amb-btn{pointer-events:auto;cursor:pointer;transition:transform var(--t-base) var(--ease-press)}
  /* v37. The title rides a black band inside the rim and turns once every
     forty seconds. Sized off --amb-r like everything else on the disc. */
  .pin.video.ambience .amb-title{grid-area:1/1;width:calc(2 * var(--amb-r,54px));height:calc(2 * var(--amb-r,54px));pointer-events:none;
    animation:amb-turn var(--t-40000) var(--linear) infinite;font:var(--fw-semibold) var(--fs-13) var(--ui);letter-spacing:var(--ls-014em);text-transform:uppercase;fill:var(--white)}
  .pin.video.ambience.paused .amb-title{animation-play-state:paused}
  @media (prefers-reduced-motion: reduce){.pin.video.ambience .amb-title{animation:none}}
  .pin.video.ambience .amb-btn:hover{transform:scale(1.06)}
  .pin.video.ambience.paused .amb-btn .amb-i-music{animation-play-state:paused}
  .pin.video.ambience.open .amb-face{opacity:0;pointer-events:none}
  /* Arranging is still the room, and the hub is still playing. The one control
     it has — stop it, start it — was drawn out of existence the moment the
     pencil came on, so the only way to silence a hub you were placing was to
     leave edit mode and come back. The face stays, the click-to-open does not:
     the clip is already a rectangle here, so there is nothing to open. */
  #world.editing-pins .pin.video.ambience .amb-face{opacity:1;pointer-events:none}
  /* And the button does NOT take the pointer while arranging. It is 56px on a
     108px disc, so letting it have its clicks left a 26px ring to grab the hub
     by — the disc looked draggable and would not drag. Pausing is something a
     viewer does; arranging is not viewing. */
  #world.editing-pins .pin.video.ambience .amb-btn{pointer-events:none}
  /* The ripple normally reads the volume where you are standing. While you are
     placing the thing that is not the question, so it runs at full — the rings
     are how a hub says it is playing, and a hub drawn without them while being
     arranged is the blank floor he kept being shown. */
  #world.editing-pins .pin.video.ambience .amb-ring{--amb-amp:1}
  /* The pencil belonged to the pin's box, and a small disc in a big box put it
     an inch of floor away from the thing it edits. It rides the disc's own rim
     at 45 degrees, and only goes back to a corner when the hub is opened out
     into a real player. */
  .pin.video.ambience:not(.open) .pedit{right:auto;
    left:calc(50% + var(--amb-r,var(--s-35)) * .707 - var(--s-23));
    top:calc(50% - var(--amb-r,var(--s-35)) * .707 - var(--s-23))}
  /* Everything that says "this is a thing you can drag" hugs the disc, because
     the disc is the thing: a red band an inch out around an invisible rectangle
     is a frame around nothing. The box is still there — the player has to keep
     a real size to keep playing — it is simply not what he is aiming at. */
  #world.editing-pins .pin.video.ambience .mv{inset:calc(50% - var(--amb-r,var(--s-35)) - var(--s-8));
    border-radius:50%;box-shadow:var(--ring-ambience);
    /* The whole thing is the handle — the disc and the halo around it — because
       a hub is small and aiming at a band drawn around it is not placing it. */
    pointer-events:auto;cursor:grab}
  #world.editing-pins .pin.video.ambience.dragging .mv{cursor:grabbing;box-shadow:var(--ring-ambience-drag)}
  #world.editing-pins .pin.video.ambience .mv i{display:none}
  /* The pencil has to stay reachable through that handle. */
  #world.editing-pins .pin.video.ambience .pedit{z-index:var(--z-4)}
  #world.editing-pins .pin.video.ambience .mvtag{top:calc(50% - var(--amb-r,var(--s-35)) - var(--s-7))}
  /* A hub has no size to drag and its address is in the panel, so the grips and
     the address strip across the invisible box are two more things aimed at a
     rectangle nobody can see. */
  #world.editing-pins .pin.video.ambience .grip,
  #world.editing-pins .pin.video.ambience .addr{display:none}
  /* The ring is the rest of the disc: the face fills the pin box, the clip
     trims it to the circle, and the button on top keeps its own clicks. */
  .pin.video.ambience:not(.open) .amb-face{pointer-events:auto;cursor:zoom-in}
  /* The button is the venue's own white pill, made round. */
  .pin.video.ambience .amb-btn{grid-area:1/1;width:56px;height:56px;border-radius:50%;background:var(--white);color:var(--ink);
    display:grid;place-items:center;box-shadow:var(--elev-amb-btn)}
  .pin.video.ambience .amb-btn .i{width:19px;height:19px;stroke-width:1.9}
  .pin.video.ambience .amb-btn .amb-i-music{animation:amb-turn var(--t-26000) var(--linear) infinite}
  .pin.video.ambience .amb-btn .amb-i-act{display:none;animation:none}
  .pin.video.ambience .amb-btn:hover .amb-i-music,
  .pin.video.ambience.paused .amb-btn .amb-i-music{display:none}
  .pin.video.ambience .amb-btn:hover .amb-i-act,
  .pin.video.ambience.paused .amb-btn .amb-i-act{display:block}
  /* Rings leaving the button, and nothing else. Bars, then woven outlines, then
     Siri's coloured ribbons — each one further from what a hub in the corner of
     a room needs to be, which is a sign that something over there is playing.
     No blur, no colour, no script: three crisp rings, evenly out of phase so one
     is always arriving. --amb-amp is the volume where the viewer is standing, so
     they are almost nothing from across the room and clear when you are beside
     it. Where they STOP follows the disc: a ring is born at the button's own
     56px and dies just inside the edge, so --amb-ring-max is (2r - 4) / 56 and
     a resized disc does not leave its rings crossing the rim or stopping short
     in the middle of it. 1.85 is that sum at the default 54. */
  .pin.video.ambience .amb-ring{grid-area:1/1;width:56px;height:56px;border-radius:50%;
    box-shadow:var(--ring-ambience-ring);opacity:0;
    animation:amb-ping var(--t-3400) var(--ease-move) infinite}
  .pin.video.ambience .amb-ring:nth-child(2){animation-delay:calc(var(--t-1130) * -1)}
  .pin.video.ambience .amb-ring:nth-child(3){animation-delay:calc(var(--t-2270) * -1)}
  @keyframes amb-ping{
    0%{transform:scale(1);opacity:0}
    16%{opacity:calc(.6 * var(--amb-amp,.12))}
    100%{transform:scale(var(--amb-ring-max,1.85));opacity:0}
  }
  .pin.video.ambience.paused .amb-ring{animation:none;opacity:0}
  @keyframes amb-turn{to{transform:rotate(360deg)}}
  @media (prefers-reduced-motion: reduce){
    .pin.video.ambience .amb-btn .i{animation:none}
    .pin.video.ambience .amb-ring{animation:none;opacity:calc(.4 * var(--amb-amp,.12));transform:scale(1.5)}
    .pin.video.ambience{transition:none}}

  /* The pencil opens this, beside the thing it belongs to. Every setting that is
     not words lives here as an icon, and nothing about it covers the floor. */
  #pinmenu{position:absolute;z-index:var(--z-popover);width:236px;padding:var(--s-6);border-radius:var(--r-3xl);
    background:var(--glass-solid);box-shadow:var(--elev-pop);
    -webkit-backdrop-filter:var(--blur-14);backdrop-filter:var(--blur-14);display:grid;gap:var(--s-5)}
  #pinmenu .mrow{display:grid;gap:var(--s-3)}
  #pinmenu .mrow > span{font:var(--fw-semibold) var(--fs-11) var(--ui);letter-spacing:var(--ls-008em);text-transform:uppercase;color:var(--fg-3)}
  /* Two icons side by side made him guess which speaker meant what. The
     choices stack and carry their own words; a glyph is a mark on a label
     here, never the label itself. */
  /* Chips, not lozenges. A fully rounded full-width row reads as a button you
     press and are done with; a chip reads as one of a set, one of which is on. */
  #pinmenu .opts{display:grid;gap:var(--s-2)}
  #pinmenu .opts button{min-height:32px;padding:var(--s-3) var(--s-5);border-radius:var(--r-9);display:flex;align-items:center;
    gap:var(--s-18);text-align:left;font:var(--fw-medium) var(--fs-12) var(--ui);line-height:var(--lh-1-25);color:var(--fg-2);
    background:var(--group);box-shadow:inset 0 0 0 1px var(--line);
    transition:background var(--t-base),color var(--t-base),box-shadow var(--t-base)}
  #pinmenu .opts button:hover{box-shadow:inset 0 0 0 1px var(--line-2)}
  #pinmenu .opts button .i{flex:none}
  #pinmenu .opts button:hover{color:var(--hi)}
  #pinmenu .opts button[aria-pressed="true"]{background:var(--coral-solid);color:var(--on-accent);box-shadow:none}
  #pinmenu .opts .i{width:16px;height:16px}
  #pinmenu .mname{height:34px;padding:0 var(--s-20);border:0;border-radius:var(--r);width:100%;
    font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--hi);background:var(--group);box-shadow:var(--ring)}
  #pinmenu .mname::placeholder{color:var(--fg-3)}
  #pinmenu .maddr{height:34px;padding:0 var(--s-20);border:0;border-radius:var(--r);width:100%;
    font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--hi);background:var(--group);box-shadow:var(--ring)}
  .accentpick{display:flex;flex-wrap:wrap;gap:var(--s-4);margin:var(--s-5) 0 var(--s-3)}
  .accentpick button{width:30px;height:30px;border-radius:50%;display:grid;place-items:center;color:var(--fg-3);
    background:var(--group);box-shadow:inset 0 0 0 1px var(--line-2)}
  .accentpick button{cursor:pointer;transition:transform var(--t-base),box-shadow var(--t-base)}
  .accentpick button:hover{transform:scale(1.12)}
  .accentpick button[aria-pressed="true"]{outline:2px solid var(--hi);outline-offset:2px;transform:scale(1.08)}
  .accentpick button svg{width:14px;height:14px}
  #pinmenu .swatches{display:flex;gap:var(--s-25)}
  #pinmenu .swatches button{width:28px;height:28px;border-radius:50%;box-shadow:var(--ring)}
  #pinmenu .swatches button[aria-pressed="true"]{outline:2px solid var(--hi);outline-offset:2px}
  /* v126. Ring added: --group == the menu's own --glass-solid surface now. */
  #pinmenu .drop{height:34px;border-radius:var(--pill);font:var(--fw-semibold) var(--fs-12) var(--ui);color:var(--coral-icon);
    background:var(--group);box-shadow:var(--ring);display:grid;place-items:center}
  #pinmenu .drop:hover{background:var(--leave);color:var(--on-accent)}

  /* A link is a white card on a dark floor: the site's own mark, then its name.
     It reads as something to click before anyone reads the words. */
  .pin.link{background:var(--white);color:var(--ink);display:flex;align-items:center;gap:var(--s-6);padding:0 var(--s-8);
    box-shadow:var(--elev-pin-link);text-decoration:none}
  .pin.link:hover{background:var(--mist)}
  /* v126. Ring added: --paper-3 can now be the same white as the link
     pin's own fixed #fff card in light theme; the glyph follows
     the theme's ink for the same reason. */
  .pin.link .fav{flex:none;width:26px;height:26px;border-radius:var(--r-md);display:grid;place-items:center;overflow:hidden;
    background:var(--paper-3);box-shadow:var(--ring);color:var(--fg-2)}
  .pin.link .fav img{width:100%;height:100%;object-fit:cover;display:block}
  .pin.link .fav svg{width:15px;height:15px}
  .pin.link .fav .emo{font:var(--fw-regular) var(--fs-16)/var(--lh-1) var(--ui)}
  .pin.link .lktxt{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    font:var(--fw-semibold) var(--fs-15) var(--ui);color:var(--ink)}
  .pin.link .host{font:var(--fw-regular) var(--fs-12) var(--ui);color:var(--ink-a55);white-space:nowrap}
  /* Before it points anywhere it is a white box asking for the address. */
  .pin.link.blank{display:grid;place-items:center;padding:0 var(--s-6)}
  .pin.link.blank .addr{position:static;display:block;width:100%;background:var(--mist);color:var(--ink);
    box-shadow:var(--ring-address-invalid)}
  .pin.link.blank .addr::placeholder{color:var(--ink-a50)}
  .pin.link.blank .fav,.pin.link.blank .lktxt,.pin.link.blank .host{display:none}
  /* How far the sound carries, drawn where the sound goes — and only while its
     panel is open. A ring nobody asked for is clutter; a ring under your hand
     while you drag is the answer to the question you are asking. */
  /* v115. Both were red washes; gold, same alphas. */
  #reachring{position:absolute;z-index:0;border-radius:50%;pointer-events:none;
    background:radial-gradient(circle,var(--accent-a20) 0%,var(--accent-a6) 60%,var(--accent-a0) 100%);
    box-shadow:var(--ring-reach)}
  /* Where people land, on the floor itself rather than on a thumbnail of it. */
  #spawnmark{position:absolute;z-index:var(--z-4);width:64px;height:64px;margin:calc(var(--s-30) * -1) 0 0 calc(var(--s-30) * -1);border-radius:50%;
    display:none;place-items:center;cursor:grab;touch-action:none;
    background:var(--accent-a28);box-shadow:var(--ring-spawnmark);color:var(--white)}
  #spawnmark svg{width:24px;height:24px}
  #spawnmark::after{content:"Everyone lands here";position:absolute;top:100%;left:50%;transform:translate(-50%,8px);
    white-space:nowrap;font:var(--fw-semibold) var(--fs-12) var(--ui);color:var(--white);text-shadow:var(--text-shadow-spawnmark)}
  .editing-pins #spawnmark{display:grid}
  #spawnmark.dragging{cursor:grabbing}
  /* Help reads as a list of answers, not a wall. */
  .helplist{display:grid;gap:0}
  .helprow{display:grid;grid-template-columns:minmax(0,116px) minmax(0,1fr);gap:var(--s-8);align-items:baseline;
    padding:var(--s-5) 0;border-bottom:1px solid var(--on-dark-07)}
  .helprow:last-child{border-bottom:0}
  .helprow b{font:var(--fw-semibold) var(--fs-13) var(--ui);color:var(--hi)}
  .helprow span{font:var(--fw-regular) var(--fs-14)/var(--lh-1-5) var(--ui);color:var(--fg-2);min-width:0}
  #tool-modal h3{font:var(--fw-semibold) var(--fs-12) var(--ui);letter-spacing:var(--ls-01em);text-transform:uppercase;color:var(--fg-3)}
  .schedule-go{color:var(--alert-text);text-decoration:underline;padding-top:var(--s-3)}
  #side-close{display:none}#room-label{font-size:var(--fs-13);color:var(--fg-3)}
  #mic-meter{width:100%;height:16px;accent-color:var(--accent)}#audio-message{font-size:var(--fs-13);color:var(--fg-3)}
  #pop{max-height:80vh;overflow:auto}
  @media(max-width:900px){
    #mobile-side{display:grid}#viewdock{top:var(--s-36)}#side.mobile-open{display:grid;position:fixed;right:0;top:0;bottom:0;width:min(360px,94vw);z-index:var(--z-sidebar);box-shadow:var(--shadow-side-mobile)}
    /* One control, one place. The button that opens the drawer becomes the button
       that closes it, so the X is exactly where the menu was. */
    body.side-open #mobile-side{position:fixed;z-index:var(--z-sidebar-open)}
    #side-close{display:none!important}
    /* These rules predate the grid: they still placed the dock, the tip and the
       view cluster by hand. With top and bottom both set, the view cluster
       stretched to 672px tall and sat over the dock. Sizes only now. */
    #floorbar{padding:0 var(--s-6) var(--s-8)}
    #dock{max-width:100%;overflow-x:auto;overflow-y:hidden;padding:var(--s-3);gap:var(--s-2)}
    #dock button{width:42px;height:42px}#dock #d-mic{height:46px;padding:0 var(--s-9) 0 var(--s-7)}
    #viewdock{right:var(--s-6)}
    /* Under 900 the top-right corner is the drawer button, with the view
       cluster below it. The toast clears both rather than fighting them. */
    #status-stack{left:var(--s-6);right:var(--s-6);top:var(--s-37);width:auto;max-height:calc(100% - 220px);justify-items:stretch}
    #topleft{left:var(--s-6);top:var(--s-6);max-width:calc(100% - 76px)}
    #roomchip{padding:var(--s-18) var(--s-8)}#roomchip .rn{font-size:var(--fs-18)}
  }

  /* ------------------------------------------------------------------ PHONE
     Three groups and nothing else: one bar across the top, one column down the
     left, one dock along the bottom, every one of them inset 12. What it
     replaced measured, at 375: a dock wrapped onto four baselines 104px tall, a
     276px view cluster floating on its own second row, and a left rail inset 20
     while everything around it was inset 12.

     The zoom stepper is gone here on purpose - a phone pinches, and spending a
     third of the width on -/41%/+ to repeat the gesture is what made the floor
     feel like a control panel. */
  @media(max-width:700px){
    /* One bar. The room name takes the space between your face and the menu and
       truncates rather than pushing anything, so the row is always one line. */
    #topleft{left:var(--s-6);top:var(--s-6);right:var(--s-38);max-width:none;gap:var(--s-4)}
    #topleft #roomchip{min-width:0;flex:0 1 auto}
    #topleft #roomchip .rn{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
    #youchip{flex:none}

    /* Corners, the same grammar the desktop floor already uses - the phone was
       the one screen still stacking chrome into columns down the middle of the
       art. You top-left, staff tools top-right, the view cluster above the dock,
       the dock along the bottom. Stacked vertically these two pills were a
       500px ladder of seven buttons down the left third of the screen.

       The stepper goes; the percentage stays, because it is also Fit to screen
       and a pinch cannot fit a room to a screen. */
    #viewdock{right:auto;left:var(--s-4);top:auto;bottom:var(--s-39);gap:var(--s-1)}
    #viewdock .vsep,#viewdock #z-in,#viewdock #z-out{display:none}
    #viewdock button{width:42px;height:42px}
    #viewdock #z-pct{width:42px;padding:0}
    #viewdock .i{width:18px;height:18px}
    /* Staff only - an attendee never sees this pill at all. */
    /* Measured at 375x812: the header wraps to two rows (y12-112) and this
       sat at top:66 on the right, laying the venue controls straight over
       the view cluster — 88x44 of overlap. Down to the bottom-left corner,
       above the dock, which nothing else on a phone uses. */
    #adminrail{left:var(--s-6);right:auto;top:auto;bottom:var(--s-40);transform:none;grid-auto-flow:column;gap:var(--s-1)}
    /* v138. The rail runs sideways here (grid-auto-flow:column, above), so
       there is no "up" inside the row for #add-options to grow into the way
       it does in the vertical desktop rail. Pinned just above the whole
       pill instead, its own small glass pill so it still reads as part of
       the rail rather than a stray popover, and capped the same way as the
       desktop version so it cannot run past the top of a short screen. */
    #add-options{order:0;position:absolute;left:0;bottom:calc(100% + var(--s-2));
      display:flex;flex-direction:column-reverse;gap:var(--s-1);grid-template-rows:none;
      opacity:0;pointer-events:none;transition:opacity var(--t-panel) var(--ease-move)}
    #add-options.open{opacity:1;pointer-events:auto}
    #add-options .inner{display:flex;flex-direction:column-reverse;gap:var(--s-1);
      max-height:min(50vh,calc(100vh - 160px));overflow-y:auto;padding:var(--s-17);border-radius:var(--pill);
      background:var(--glass-pill);-webkit-backdrop-filter:var(--blur-20-sat);backdrop-filter:var(--blur-20-sat);
      box-shadow:var(--elev-glasspill)}
    /* v135. The dock goes edge-to-edge below (see #floorbar further down) —
       #floor-help moves up to #viewdock's own row so it clears it, same
       12px gutter the rest of the phone layout uses. */
    #floor-help{right:var(--s-6);bottom:var(--s-39)}

    /* One row, scrolling if a host's extra controls make it long, never wrapping
       - a wrapped dock is what put Leave on a line of its own. */
    #dock{flex-wrap:nowrap;justify-content:flex-start;scrollbar-width:none;
      -webkit-mask-image:var(--mask-dock-fade);
      mask-image:var(--mask-dock-fade)}
    #dock::-webkit-scrollbar{display:none}
    /* Nothing to scroll, nothing to fade: the mask only earns its keep when the
       row is actually longer than the pill. */
    #dock.fits{-webkit-mask-image:none;mask-image:none;justify-content:center}
    /* An attendee's dock fits without scrolling; a host's extra controls are what
       make it scroll, which is the right way round. */
    #floorbar{padding:0 var(--s-4) var(--s-7)}
    #dock button{width:40px;height:40px}
    #dock #d-mic{height:44px;padding:0 var(--s-8) 0 var(--s-6)}
    #dock .dsep{margin:0 var(--s-21)}
    /* Leave stays at the right edge of the pill while the rest of the row
       scrolls under it — the one button a stuck person must always reach. The
       fade mask goes, because it would fade Leave. */
    #dock{-webkit-mask-image:none;mask-image:none}
    #dock #d-leave{position:sticky;right:0;flex:none;background:var(--glass-solid);box-shadow:-10px 0 12px -4px var(--glass-solid)}
    #dock.fits #d-leave{box-shadow:none}
    /* v32. On a phone the dock is a bar across the bottom, edge to edge, sat
       above the home indicator. Buttons spread; a scrolling row still scrolls. */
    #floorbar{padding:0}
    #floorbar .dockwrap{justify-items:stretch}
    #dock{width:100%;max-width:none;border-radius:0;padding:var(--s-4) var(--s-5) calc(var(--s-4) + env(safe-area-inset-bottom,0px));justify-content:space-between;gap:var(--s-1)}
    #dock.fits{justify-content:space-around}
    #dock .dsep{display:none}
    /* Under 700 the view cluster moves into the header row, so the band above
       the floor is one row deep again. */
    /* Under the wrapped header, which runs to y112 once the view cluster
       drops onto its own row. */
    #status-stack{top:var(--s-41)}
    #gate-health-list{grid-template-columns:1fr}
    #hint{bottom:var(--s-42)}
    #roomchip .rc{margin-left:var(--s-3)}
    /* and the view cluster sits in the header row with the name, not on the floor */
    #viewdock{position:static;left:auto;right:auto;top:auto;bottom:auto;margin-left:auto;padding:var(--s-2);gap:0}
    #viewdock button{width:36px;height:36px}
    #viewdock #z-pct{width:auto;padding:0 var(--s-3)}
    #topleft{right:var(--s-43)}
    #arrangebar{bottom:var(--s-40);max-width:calc(100vw - 24px)}
    #arrangebar span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
  }
  /* v31. At 420 the mic's word is the 65 px that pushes Leave off a 390 screen;
     the icon and the coral already say off. */
  @media(max-width:420px){#dock #d-mic{padding:0 var(--s-6)}#dock #d-mic #mic-label{display:none}}
  @media(max-width:520px){.form-grid{grid-template-columns:1fr}.profile-avatars{grid-template-columns:repeat(5,1fr)}.profile-avatars-wrap{max-height:calc(3 * (100cqw - 48px) / 5 + 24px + 12px)}#tool-modal .card{padding:var(--s-9)}#draw-tools{left:var(--s-4);max-width:calc(100% - 16px)}}


  @media(min-width:701px) and (max-width:900px){
    :root{--side:260px}#side{display:grid}#side.mobile-open{position:static;width:auto;box-shadow:none;z-index:auto}
    #mobile-side{display:none!important}#side-close{display:none!important}
    #top .count{display:none}#top .chip{padding:0 var(--s-5)}
  }
  /* Below the sidebar breakpoint the room list is a drawer, so chat needs a
     door on the floor; above it the sidebar's own tab is that door. */
  #dock #d-chat{display:none}
  @media(max-width:900px){#dock #d-chat{display:grid}}

  /* The x in the sidebar's own bar collapses it on a desktop. Below the
     sidebar breakpoint the drawer button is the one control, so it stays
     hidden there and closing the drawer works the way it always did. */
  /* v42. One button does both, and it never moves: top right of the floor,
     the last thing in the header row. The x inside the sidebar is gone above
     the breakpoint - two controls for one state is how you end up hunting for
     the one that is showing. Below the breakpoint the drawer keeps its x. */
  #side #side-close{display:none!important}
  #side-open{display:none}
  @media(min-width:901px){
    /* v106. The sidebar slides, it does not blink. The column's width is what
       animates (a length, so it interpolates); the sidebar keeps its own width
       and is clipped as the column closes over it, fading as it goes. It is
       made `visibility:hidden` only AFTER the slide, so it leaves the tab order
       without cutting the animation short. */
    #app{overflow:hidden;transition:grid-template-columns var(--t-360) var(--ease-arrive)}
    #side{width:var(--side);transition:opacity var(--t-280) var(--ease),visibility 0s}
    body.side-hidden #app{grid-template-columns:1fr 0px}
    body.side-hidden #side{opacity:0;visibility:hidden;pointer-events:none;transition:opacity var(--t-ease) var(--ease),visibility 0s var(--t-360)}
    #side-open{transition:background var(--t-base),color var(--t-base),top var(--t-360) var(--ease-arrive),right var(--t-360) var(--ease-arrive)}
    @media (prefers-reduced-motion:reduce){#app,#side,#side-open{transition:none!important}}
    /* Fixed to the corner, not carried by the header row: #topleft is
       shrink-to-fit, so a button at its end sits wherever the row happens to
       end. This one is always the top right of the screen, over the sidebar
       when the sidebar is there. */
    /* v105. Over the open sidebar it is one more control in the header row:
       no ring, no glass, the row's own 34px. Only once the sidebar is gone
       and it floats over the room does it need a surface to be seen on.
       v135. This — fixed over the brandbar, not #side-close (permanently
       display:none!important below, v42) — is the collapse icon Munk's
       screenshot actually showed. Round now, not a rounded square: the
       row's own height, the same shape every icon-only control elsewhere
       in the app wears (docs/ui-backlog.md's "3 different widths and
       styles"). */
    /* v135: the same top and side inset as the brandbar, so it lines up with Reports.
       v166. That inset was only the OPEN state's own corner (top:s-6/right:s-8,
       34px) — collapsed grew the button to 44px from a DIFFERENT corner
       (top:s-7/right:s-7), which moves its centre (open: 12+17=29 from the
       top, 16+17=33 from the right; collapsed was 14+22=36/36) and reads as a
       jump on every toggle. Collapsed now sits 5px in from the open state's
       own corner each way — half the 34->44px size difference — so both
       states share one centre and nothing jumps. */
    #side-open{display:grid;place-items:center;position:fixed;right:var(--s-8);top:var(--s-6);z-index:var(--z-sidebar-tab);
      width:34px;height:34px;border-radius:50%;color:var(--fg-2);background:transparent}
    body.side-hidden #side-open{right:calc(var(--s-8) - 5px);top:calc(var(--s-6) - 5px);width:44px;height:44px;border-radius:var(--pill);color:var(--hi);background:var(--glass);box-shadow:var(--ring);-webkit-backdrop-filter:var(--blur-12);backdrop-filter:var(--blur-12)}
    /* Room for it at the end of the sidebar's own bar. */
    #side .brandbar{padding-right:calc(var(--s-8) + var(--s-33) + var(--s-4))}
    /* v166. #side-open floats fixed, over the brandbar, from top:s-6 to
       +34px (46px down) — a brandbar shorter than that (an attendee's, with
       no Reports button showing) let the icon bleed into .head, the tab row
       right under it. The same reserved-space formula the padding-right
       above already uses, turned into a floor under the row's own height,
       guarantees the clearance regardless of what the row happens to hold. */
    #side .brandbar{min-height:calc(var(--s-8) + var(--s-33) + var(--s-4))}
    #side-open:hover,body.side-hidden #side-open:hover{background:var(--hover);color:var(--hi)}
    #side-open .i{width:20px;height:20px;transition:transform var(--t-ease) var(--ease-press)}
    /* Open: the chevron points at the sidebar it will push away. Collapsed: it
       points back at the floor, where the sidebar comes from. */
    body.side-hidden #side-open .i{transform:scaleX(-1)}
  }
.setup-row button{flex-shrink:0}.setup-row{flex-wrap:wrap}.setup-row small{overflow-wrap:anywhere}#tool-content h3{margin:var(--s-9) 0 var(--s-4)}

/* v67b. Phase 2 client — countdown chip, closing modal, the Author editor's
   onboarding steps and its own image placement map, and the Author Rooms
   overview grid. Reuses existing tokens/classes (.glasspill, .chip, .spawn
   .map, .bgtile) rather than a second visual language. */
#room-countdown{position:absolute;top:var(--s-6);left:50%;transform:translateX(-50%);z-index:var(--z-countdown);padding:0 var(--s-6);height:36px;display:flex;align-items:center;font:var(--fw-semibold) var(--fs-13) var(--ui);color:var(--fg-2)}
#closing-modal{position:fixed;inset:0;z-index:var(--z-alert);display:grid;place-items:center;background:var(--void-a72)}
#closing-modal .box{background:var(--paper);color:var(--ink);border-radius:var(--r-lg);padding:var(--s-9) var(--s-10);font:var(--fw-semibold) var(--fs-16) var(--ui);box-shadow:var(--elev-closing-modal)}
.onboard-steps{display:flex;flex-wrap:wrap;gap:var(--s-4);margin:var(--s-5) 0 var(--s-7)}
/* v126. Ring added: --group == the modal's own surface now. */
.ostep{font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--fg-3);padding:var(--s-2) var(--s-5);border-radius:var(--pill);background:var(--group);box-shadow:var(--ring)}
.ostep.done{color:var(--live)}
#author-pin-map .dot{position:absolute;width:14px;height:14px;margin:calc(var(--s-25) * -1) 0 0 calc(var(--s-25) * -1);border-radius:50%;background:var(--accent);border:2px solid var(--white);box-shadow:var(--elev-author-dot);pointer-events:none}
.overview-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(160px,1fr));gap:var(--s-6)}
/* v126. Ring added: --group == the modal's own surface now. */
.ovcard{display:flex;flex-direction:column;border:0;padding:0;border-radius:var(--r-lg);overflow:hidden;background:var(--group);box-shadow:var(--ring);cursor:pointer;text-align:left}
.ovcard:hover{outline:2px solid var(--accent)}
.ovbg{display:block;width:100%;aspect-ratio:4/3;background-size:cover;background-position:center;background-color:var(--group)}
.ovinfo{display:flex;align-items:center;gap:var(--s-3);padding:var(--s-4) var(--s-5);flex-wrap:wrap}
.ovinfo b{font:var(--fw-semibold) var(--fs-13) var(--ui);color:var(--fg-1)}
.ovinfo small{flex-basis:100%;color:var(--fg-3);font:var(--fw-regular) var(--fs-11) var(--ui)}
@media(max-width:520px){.overview-grid{grid-template-columns:repeat(auto-fill,minmax(130px,1fr))}}

/* ================= v109. motion ================= */
/* Nothing in a modal is instant. The scrim fades, the card rises into place
   and eases back out (venue.js showModal/hideModal); when one modal becomes
   another, the card reshapes (morphCard); what arrives settles in, top to
   bottom (settleIn, .lh-in). The sheet and the left drawer keep their own
   slide in from the edge they live on. */
.modal:not(.hidden):not(.sheet){animation:lh-scrim-in var(--t-panel) var(--ease-out)}
.modal:not(.hidden):not(.sheet):not(.drawer-left) .card{animation:lh-card-in var(--t-340) var(--ease-arrive)}
.modal.closing{animation:lh-scrim-out var(--t-settle) var(--ease-in) forwards;pointer-events:none}
.modal.closing .card{animation:lh-card-out var(--t-settle) var(--ease-in) forwards}
#av-gal.open{animation:lh-scrim-in var(--t-panel) var(--ease-out)}
#av-gal.open .gal{animation:lh-card-in var(--t-340) var(--ease-arrive)}
#av-gal.closing{animation:lh-scrim-out var(--t-settle) var(--ease-in) forwards;pointer-events:none}
#av-gal.closing .gal{animation:lh-card-out var(--t-settle) var(--ease-in) forwards}
@keyframes lh-scrim-in{from{opacity:0}}
@keyframes lh-scrim-out{to{opacity:0}}
@keyframes lh-card-in{from{opacity:0;transform:translateY(12px) scale(.975);filter:blur(4px)}}
@keyframes lh-card-out{to{opacity:0;transform:translateY(6px) scale(.985)}}
.lh-in{animation:lh-row var(--t-380) var(--ease-arrive) backwards;animation-delay:calc(var(--i, 0) * var(--t-35) + var(--t-60))}
@media (prefers-reduced-motion:reduce){
  .modal:not(.hidden),.modal:not(.hidden) .card,.modal.closing,.modal.closing .card,
  #av-gal.open,#av-gal.open .gal,#av-gal.closing,#av-gal.closing .gal,.lh-in{animation:none!important}
}

/* v114. The rest of the menus and popovers (#roommenu, #groupmenu, #pinmenu,
   #reacts, #pop) eased in already (their own CSS below) but vanished
   the instant a close ran. hideWithExit (venue.js) adds this class first and
   applies the real hide once it finishes, so every one of them now eases out
   the same way a modal does. */
.leaving{animation:lh-pop-out var(--t-140) var(--ease-in) forwards;pointer-events:none}
@keyframes lh-pop-out{to{opacity:0;translate:0 4px;scale:.98}}
@media (prefers-reduced-motion:reduce){.leaving{animation:none!important}}

/* ================= v133. Venue › Access & Moderation ================= */
/* v109/v125 built this as the admin desk's own wide sheet, a sticky
   .desk-tabs row over one scrolling #desk-body (and #desk-foot below it, for
   Words' Save). v133: the desk is gone, so is its tab row and its two named
   rows — Access/Reports/Flagged/Faces/Words/Codes are Venue rail items now,
   each an ordinary .editpane (venue.css's own rail section, above). Every
   card style below (.acc-card, .desk-row, …) is unchanged; only the sheet
   shell around it left. */
/* One card a job. */
.hp-body{display:grid;gap:var(--s-5);align-content:start}
.acc-card{border-radius:var(--r-xl);background:var(--paper-2);box-shadow:var(--ring);padding:var(--s-7) var(--s-8);transition:box-shadow var(--t-ease) var(--ease)}
.acc-card.open{box-shadow:inset 0 0 0 1px var(--fg-4)}
.acc-head{display:flex;align-items:center;gap:var(--s-6)}
.acc-title{flex:1;min-width:0;display:grid;gap:var(--s-2)}
#tool-content .acc-title h3,.hp-body .acc-title h3{margin:0;font:var(--fw-semibold) var(--fs-16)/var(--lh-1-3) var(--ui);letter-spacing:0;text-transform:none;color:var(--hi)}
.acc-title p{margin:0;font:var(--fw-regular) var(--fs-13)/var(--lh-1-45) var(--ui);color:var(--fg-3)}
.acc-pill{flex:none;display:inline-flex;align-items:center;gap:var(--s-3);font:var(--fw-semibold) var(--fs-12)/var(--lh-1) var(--ui);padding:var(--s-25) var(--s-20);border-radius:var(--pill);background:var(--group);box-shadow:var(--ring);color:var(--fg-3);white-space:nowrap}
.acc-pill.is-active{color:var(--hi)}
.acc-pill.is-active::before{content:'';width:7px;height:7px;border-radius:50%;background:var(--live-active)}
.acc-pill.is-warn{background:var(--coral-solid);color:var(--on-accent);box-shadow:none}
.acc-toggle.btn{flex:none;height:36px;padding:0 var(--s-8);font-size:var(--fs-14)}
/* v167. Access cards: title/description are accCardHTML's own direct
   children now, not sharing .acc-head's flex row with the pill and control
   (.acc-head stays as it was, for Words/Ticket codes) — .acc-value is that
   row, always underneath, on every width. */
.acc-value{display:flex;align-items:center;flex-wrap:wrap;gap:var(--s-6);margin-top:var(--s-5)}
.acc-switch.sw{flex:1;min-width:200px;padding:0}
.acc-body{display:grid;margin-top:var(--s-6)}
/* v126. --group == the card's own surface now; the divider draws in
   --line-2 instead. */
.access-row{border-top:1px solid var(--line-2);padding:0}
.access-row:first-child{border-top:0}
.access-row-head{display:flex;align-items:center;gap:var(--s-5);width:100%;text-align:left;cursor:pointer;padding:var(--s-5) var(--s-4);border-radius:var(--r-lg);transition:background var(--t-base) var(--ease)}
.access-row-head:hover{background:var(--hover)}
.acc-row-main{flex:1;min-width:0;display:grid;gap:var(--s-2)}
.acc-row-main .desk-when{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.acc-chev{flex:none;width:18px;height:18px;color:var(--fg-4);transition:transform var(--t-280) var(--ease-arrive)}
.access-row.open .acc-chev{transform:rotate(180deg)}
/* v126. Same fix as the legacy .access-edit above: ring instead of a fill
   that now matches .acc-card's own surface. */
.access-edit{display:grid;gap:var(--s-7);margin:var(--s-3) 0 var(--s-6);padding:var(--s-8);background:var(--paper-3);box-shadow:var(--ring);border-radius:var(--r-lg)}
.acc-body > .access-edit{margin:0}
.acc-pair{display:grid;grid-template-columns:1fr 1fr;gap:var(--s-6)}
.acc-lasts:has(option[value=forever]:checked){grid-template-columns:1fr}
.acc-lasts:has(option[value=forever]:checked) label.f:has(input[type=date]){display:none}
.acc-actions{display:flex;flex-wrap:wrap;gap:var(--s-4);align-items:center}
.acc-actions .btn{height:42px;padding:0 var(--s-10);font-size:var(--fs-14)}
#tool-content .acc-sub{margin:var(--s-2) 0 0;font:var(--fw-semibold) var(--fs-12) var(--ui);letter-spacing:var(--ls-006em);text-transform:uppercase;color:var(--fg-4)}
.access-edit .desk-claim:empty{display:none}
/* v165. The one-time admin code: monospace so every character reads
   unambiguously (the alphabet already avoids 0/O/1/I/L, this is the other
   half of that), wrapping rather than truncating on a narrow drawer. */
.admin-entry-code-row{display:flex;flex-wrap:wrap;align-items:center;gap:var(--s-5)}
.admin-entry-code-row code{font-family:var(--mono,monospace);font-size:var(--fs-16);letter-spacing:var(--ls-006em);word-break:break-all;user-select:all}
.scope{display:grid;gap:var(--s-6)}
.scope .sw{padding:0}
.picks{display:flex;flex-wrap:wrap;gap:var(--s-3)}
.pick{position:relative;cursor:pointer}
.pick input{position:absolute;opacity:0;pointer-events:none}
.pick span{display:inline-flex;align-items:center;height:32px;padding:0 var(--s-7);border-radius:var(--pill);font:var(--fw-medium) var(--fs-13) var(--ui);color:var(--fg-2);box-shadow:var(--ring);transition:background var(--t-base) var(--ease),color var(--t-base) var(--ease),box-shadow var(--t-base) var(--ease)}
.pick:hover span{color:var(--hi);background:var(--hover)}
.pick input:checked + span{background:var(--hi);color:var(--paper);box-shadow:none}
.pick input:focus-visible + span{outline:2px solid var(--accent);outline-offset:2px}
.access-reveal{display:grid;gap:var(--s-5);padding:var(--s-8);border-radius:var(--r-xl);background:var(--paper-2);box-shadow:var(--ring-access-live)}

/* v167. Venue › Words: <details>/<summary> sections instead of a stack of
   boxed .acc-cards (Munk's live-review "lighter" ask) — a plain divider
   between them, a chevron (reusing .acc-chev's own rotate) instead of the
   browser's own disclosure triangle. */
.wpane-sec{border-top:1px solid var(--line-2);padding:var(--s-6) 0}
.wpane-sec:first-of-type{border-top:0}
.wpane-sec summary{cursor:pointer;list-style:none;display:flex;align-items:center;justify-content:space-between;gap:var(--s-6);font:var(--fw-semibold) var(--fs-15) var(--ui);color:var(--hi)}
.wpane-sec summary::-webkit-details-marker{display:none}
.wpane-sec summary:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.wpane-sec[open] > summary .acc-chev{transform:rotate(180deg)}
.wpane-sec-body{display:grid;gap:var(--s-6);margin-top:var(--s-6)}
/* A tier inside "The venue's own words" — no box of its own any more, just a
   heading, its count pill, the one-line meaning and the textarea. */
.wpane-area{display:grid;gap:var(--s-3)}
.wpane-area-head{display:flex;align-items:center;justify-content:space-between;gap:var(--s-6)}
.wpane-area-head h4{margin:0;font:var(--fw-semibold) var(--fs-14)/var(--lh-1-3) var(--ui);color:var(--hi)}
/* v167. "Blocked by default": readable, not shouted — small and muted, with
   a filter box (wireWordsPanel) so nobody has to read all ~400 to check one. */
#wpane-default-filter{margin-bottom:var(--s-2)}
.wpane-default-tier{display:grid;gap:var(--s-2)}
.wpane-default-label{margin:0;font:var(--fw-medium) var(--fs-13) var(--ui);color:var(--fg-3)}
.wpane-default-list{margin:0;font:var(--fw-regular) var(--fs-12)/var(--lh-1-6) var(--ui);color:var(--fg-4)}
.wpane-default-list span{margin-right:var(--s-5)}
@media (max-width:560px){
  .acc-pair{grid-template-columns:1fr}
  .acc-head{flex-wrap:wrap}
  .acc-title{flex-basis:100%}
  .acc-card{padding:var(--s-6)}
}

/* v114. Faces, Flagged, Words and Codes brought to the same standard:
   a row's actions read as one group at the end of the line on a desktop-wide
   card. v133: the word list's own Save is the drawer's footer Save now
   (venueSaveWords, DRAWER_SAVE_ITEMS), not a card of its own. */
.acc-card .desk-acts{justify-content:flex-end}
@media (max-width:560px){.acc-card .desk-acts{justify-content:flex-start}}

/* ================= v109. everything else arrives too ================= */
/* Menus and popovers rise out of the control that opened them; a sidebar tab
   or a room-editor pane fades up when it is switched to. All CSS: each runs
   when the element goes from display:none to shown, so a repaint of
   something already open never replays it. `translate`/`scale` are their
   own properties, so an element already positioned with `transform`
   (#reacts is centred with one) keeps it. */
.menu:not(.hidden),#pop:not(.hidden),#reacts:not(.hidden){animation:lh-pop var(--t-ease) var(--ease-arrive)}
@keyframes lh-pop{from{opacity:0;translate:0 6px;scale:.97}}
.pane:not(.hidden){animation:lh-appear-up var(--t-260) var(--ease-arrive)}
@keyframes lh-appear-up{from{opacity:0;translate:0 4px}}
#tool-modal [id^="pane-"][data-open] > *{animation:lh-row var(--t-360) var(--ease-arrive) backwards}
#tool-modal [id^="pane-"][data-open] > :nth-child(2){animation-delay:var(--t-30)}
#tool-modal [id^="pane-"][data-open] > :nth-child(3){animation-delay:var(--t-60)}
#tool-modal [id^="pane-"][data-open] > :nth-child(4){animation-delay:var(--t-90)}
#tool-modal [id^="pane-"][data-open] > :nth-child(5){animation-delay:var(--t-120)}
#tool-modal [id^="pane-"][data-open] > :nth-child(6){animation-delay:var(--t-150)}
#tool-modal [id^="pane-"][data-open] > :nth-child(n+7){animation-delay:var(--t-180)}
@media (prefers-reduced-motion:reduce){
  .menu:not(.hidden),#pop:not(.hidden),#reacts:not(.hidden),.pane:not(.hidden),
  #tool-modal [id^="pane-"][data-open] > *{animation:none}
}

/* ================= v110. the flyout ================= */
/* One positioned popover (openFlyout/closeFlyout, venue.js), reused wherever
   a full #tool-modal would be more ceremony than the task needs. No scrim —
   position:fixed, placed by flyoutPlacement, clamped inside the viewport.
   Same tokens and card feel as .modal .card, at flyout scale. */
#flyout{position:fixed;z-index:var(--z-flyout);width:min(340px,calc(100vw - 24px));background:var(--paper);color:var(--ink-3);
  border-radius:var(--r-xl);box-shadow:var(--elev-flyout);display:grid;grid-template-rows:auto minmax(0,1fr) auto;
  max-height:calc(100vh - 24px);max-height:calc(100dvh - 24px)}
#flyout.hidden{display:none}
.fly-head{position:relative;padding:var(--s-9) var(--s-11) var(--s-4) var(--s-10)}
.fly-head h2{margin:0;font:var(--fw-regular) var(--fs-17)/var(--lh-1-3) var(--brand);letter-spacing:var(--ls-03px)}
#fly-form{display:contents}
#fly-content{display:grid;gap:var(--s-6);padding:0 var(--s-10) var(--s-7);overflow:auto;min-height:0;overscroll-behavior:contain;font-size:var(--fs-14)}
#fly-content .fine{margin:0;color:var(--fg-3);font-size:var(--fs-12);line-height:var(--lh-1-5)}
/* v115. --err, same reasoning as #tool-error above. */
#fly-error{padding:0 var(--s-10);color:var(--err);font:var(--fw-medium) var(--fs-13)/var(--lh-1-4) var(--ui);min-height:0}
#fly-error:empty{display:none}
.fly-foot{display:flex;justify-content:flex-end;padding:var(--s-6) var(--s-10) var(--s-8)}
.fly-foot.hidden{display:none}
.fly-foot .btn{height:38px;padding:0 var(--s-8);font-size:var(--fs-13)}
/* v124. The Venue flyout used to be one flat pile (name field, calendar
   field, three record buttons, the role picker, the emergency button, all
   at the same visual weight). .fly-group is a generic "labelled cluster of
   fields/actions" any flyout body can reach for — the heading style copies
   .hp-hands h4/.editpane>h3 (same ink-4 uppercase eyebrow already used for a
   panel's own group headings elsewhere), and the border between groups
   (not on the first one) is what makes the last group — Emergency — read as
   visually separated from the rest, same as v113 wanted the lockdown row to
   never sit beside an everyday action by accident. */
.fly-group{display:grid;gap:var(--s-5)}
.fly-group h4{margin:0;font:var(--fw-semibold) var(--fs-12) var(--ui);letter-spacing:var(--ls-008em);text-transform:uppercase;color:var(--ink-4)}
.fly-group+.fly-group{padding-top:var(--s-6);border-top:1px solid var(--line-2)}
/* Replaces the va-name-save row's old inline style="display:flex;gap:var(--s-4);align-items:flex-end" —
   a generic flex row for a field/button pair, reusable by any flyout body. */
.fly-row{display:flex;gap:var(--s-4);align-items:center;justify-content:flex-end}
/* An arrow, on the side the card sits relative to its anchor — a small
   pointer back at the button that opened it. */
#flyout::before{content:"";position:absolute;width:10px;height:10px;background:var(--paper);transform:rotate(45deg);pointer-events:none}
#flyout[data-side="above"]::before{left:var(--s-10);bottom:calc(var(--s-17) * -1)}
#flyout[data-side="below"]::before{left:var(--s-10);top:calc(var(--s-17) * -1)}
#flyout[data-side="right"]::before{left:calc(var(--s-17) * -1);top:var(--s-10)}
#flyout[data-side="left"]::before{right:calc(var(--s-17) * -1);top:var(--s-10)}
/* Bottom sheet, <=560px (flyoutPlacement's own caller checks the width). */
#flyout.sheet{left:0!important;right:0;bottom:0;top:auto!important;width:100%;max-width:100%;
  border-radius:var(--r-xl) var(--r-xl) 0 0;max-height:80vh;max-height:80dvh}
#flyout.sheet::before{display:none}
#flyout:not(.hidden):not(.closing){animation:lh-card-in var(--t-260) var(--ease-arrive)}
#flyout.closing{animation:lh-card-out var(--t-settle) var(--ease-in) forwards;pointer-events:none}
#flyout.sheet:not(.hidden):not(.closing){animation:lh-sheet-in var(--t-260) var(--ease-arrive)}
#flyout.sheet.closing{animation:lh-sheet-out var(--t-settle) var(--ease-in) forwards}
@keyframes lh-sheet-in{from{opacity:0;transform:translateY(100%)}}
@keyframes lh-sheet-out{to{opacity:0;transform:translateY(100%)}}
@media (prefers-reduced-motion:reduce){
  #flyout:not(.hidden),#flyout.closing{animation:none!important}
}

/* Group heading menu — same look as #roommenu (.menu), same positioning
   contract (fixed, placed at the trigger). */
#groupmenu{position:fixed;min-width:200px}

/* ================= v110. designed tooltips ================= */
/* Replaces native title tooltips and #adminrail's own ::after — one #tip
   element (wireTooltips, venue.js), shown after a hover delay or on
   :focus-visible, positioned on the side away from the edge the control
   lives on. Skipped entirely on touch. */
#tip{position:fixed;z-index:var(--z-tip);max-width:220px;padding:var(--s-25) var(--s-20);border-radius:var(--r-lg);
  background:var(--glass-solid);box-shadow:var(--elev-tip);
  font:var(--fw-medium) var(--fs-13)/var(--lh-1-35) var(--ui);color:var(--hi);pointer-events:none;opacity:0;
  transform:translateY(4px);transition:opacity var(--t-tap) var(--ease),transform var(--t-tap) var(--ease)}
#tip.on{opacity:1;transform:none}
#tip::after{content:"";position:absolute;width:8px;height:8px;background:var(--glass-solid);transform:rotate(45deg)}
#tip[data-side="top"]::after{left:50%;bottom:calc(var(--s-2) * -1);margin-left:calc(var(--s-2) * -1)}
#tip[data-side="bottom"]::after{left:50%;top:calc(var(--s-2) * -1);margin-left:calc(var(--s-2) * -1)}
#tip[data-side="left"]::after{right:calc(var(--s-2) * -1);top:50%;margin-top:calc(var(--s-2) * -1)}
#tip[data-side="right"]::after{left:calc(var(--s-2) * -1);top:50%;margin-top:calc(var(--s-2) * -1)}
@media (prefers-reduced-motion:reduce){#tip{transition:opacity var(--t-tap) var(--ease);transform:none!important}}

/* ================= v117. Hosting lives in the room editor ================= */
/* Replaces the v111 sidebar tab: one short screen at a time, inside whichever
   editor opened it (roomEditor's own rail pane for an admin, the same shell
   for an author, or hostingSheet's plain .sheet for a Knight, who has no
   roomEditor of their own — see the venue.js comment on hostingSheet). */
.icon-only{width:36px;height:36px;padding:0!important;flex:none;display:grid;place-items:center;border-radius:50%}
.icon-only .i{width:17px;height:17px}
/* The open-times chip reuses .acc-pill's look even as a <button>. */
button.acc-pill{cursor:pointer;border:0;font:inherit}
button.acc-pill:hover{background:var(--hover)}
button.acc-pill:disabled{cursor:default}
/* Room name, the host chip and the open-times chip on one line — the one
   thing every tab below shares, so it is painted once, not per tab. */
.hp-head{display:grid;gap:var(--s-4);margin-bottom:var(--s-2)}
.hp-head h3{margin:0;font:var(--fw-semibold) var(--fs-17) var(--ui);color:var(--hi)}
.hp-head-row{display:flex;align-items:center;gap:var(--s-4);flex-wrap:wrap}
/* v135. Was its own .hp-host rule here — hostingChipsHTML (venue.js) now
   reuses .acc-pill (below) for both chips instead. */
/* The gold segmented bar (v115 tokens) — People/Rules/Reports/Data, or Data
   alone for anyone hostPowers(r) has not lit up yet (README v117). */
.hp-tabs{display:flex;gap:var(--s-1);margin:var(--s-2) 0 var(--s-6);padding:var(--s-2);border-radius:var(--pill);background:var(--group);box-shadow:var(--ring)}
.hp-tabs button{flex:1 1 auto;min-width:0;font:var(--fw-semibold) var(--fs-13)/var(--lh-1) var(--ui);padding:var(--s-18) var(--s-5);border-radius:var(--pill);color:var(--fg-3);background:transparent;box-shadow:none;cursor:pointer;transition:background var(--t-ease) var(--ease),color var(--t-ease) var(--ease)}
.hp-tabs button:hover{color:var(--hi)}
.hp-tabs button[aria-selected=true]{color:var(--on-accent);background:var(--accent)}
.hp-body{min-width:0}
/* People. One row: avatar, name, a single mic icon, a "…" for the rest. */
.hp-hands{margin-bottom:var(--s-6)}
.hp-hands h4{margin:0 0 var(--s-3);font:var(--fw-semibold) var(--fs-12) var(--ui);letter-spacing:var(--ls-006em);text-transform:uppercase;color:var(--ink-4)}
/* v126. --group == the sheet's own surface now; the divider draws in
   --line-2 instead. */
.hp-person{display:flex;align-items:center;gap:var(--s-4);padding:var(--s-4) var(--s-2);border-top:1px solid var(--line-2)}
.hp-person:first-child{border-top:0}
.hp-person-name{flex:1;min-width:0;font:var(--fw-semibold) var(--fs-14) var(--ui);color:var(--hi);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.hp-search{width:100%;margin-bottom:var(--s-4)}
/* Rules. A gold strip that mutes the room in one press, then five rows with
   a compact dropdown chip apiece — no more icon-toggle groups. */
.hp-mute-strip{display:block;width:100%;margin-bottom:var(--s-6);padding:var(--s-6) var(--s-7);border:0;border-radius:var(--r-lg);background:var(--accent);color:var(--on-accent);font:var(--fw-semibold) var(--fs-14) var(--ui);cursor:pointer;text-align:center}
.hp-mute-strip:hover{background:var(--accent-deep)}
/* v126. Same divider fix as .hp-person above. */
.hp-rule-row{display:grid;grid-template-columns:minmax(0,1fr) auto;align-items:center;gap:var(--s-6);padding:var(--s-5) 0;border-top:1px solid var(--line-2)}
.hp-rule-row:first-child{border-top:0}
.hp-rule-q{font:var(--fw-semibold) var(--fs-14) var(--ui);color:var(--fg-2)}
/* v135. hostingRulesHTML nests the hint as a <small> inside this span, same
   as .sw does for a switch's own hint — without .sw's own rule (its small
   is display:block) the two ran together on one line ("Who can enterA
   staff-only room…", docs/ui-backlog.md's "Titles and subtitles"). */
.hp-rule-q small{display:block;font:var(--fw-regular) var(--fs-12) var(--ui);color:var(--fg-3);margin-top:var(--s-1)}
.hp-rule-chip{flex:none;font:var(--fw-semibold) var(--fs-12) var(--ui);padding:var(--s-4) var(--s-13) var(--s-4) var(--s-6);border-radius:var(--pill);background:var(--group);box-shadow:var(--ring);color:var(--hi);border:0;cursor:pointer;appearance:none;
  background-image:url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23999' stroke-width='2'><path d='M6 9l6 6 6-6'/></svg>");
  background-repeat:no-repeat;background-position:right 8px center;background-size:12px}
/* v137. The entry row (Rules' "Staff only" switch) holds a switch and a
   sentence, not a label and a select — .hp-rule-row's own two-column grid
   would sit them side by side, so this stacks them instead. */
.hp-rule-switch{display:block}
.hp-rule-switch .sw{padding:var(--s-3) 0}
.hp-rule-switch .fine{margin-top:0}
/* v137. A button reading as a line of text inside a sentence — used for the
   "Venue › Access" jump under Rules' entry switch and the Room pane's own
   access paragraph (docs/ui-backlog.md, "Who can enter is unclear"), same
   underline-link treatment .auth-secondary .btn already gives a text-sized
   action, sized down to sit inline in a `.fine` line instead. */
.fine .btn.link{display:inline;height:auto;padding:0;background:none;box-shadow:none;border-radius:0;color:var(--hi);font:inherit;text-decoration:underline;text-decoration-color:var(--line-2);text-underline-offset:3px}
.fine .btn.link:hover,.fine .btn.link:focus-visible{color:var(--accent);text-decoration-color:currentColor;transform:none}
/* Data. v121's author analytics — a small stat grid, shown only once real
   figures arrive (hostingDataHTML falls back to the desk-claim placeholder
   otherwise), reusing the same spacing/radius/colour tokens acc-card and
   hp-rule-row already use rather than inventing new ones. */
.hp-stats{display:grid;grid-template-columns:1fr 1fr;gap:var(--s-5);margin-bottom:var(--s-6)}
/* v126. Ring added: --group == the sheet's own surface now. */
.hp-stat{padding:var(--s-5) var(--s-6);border-radius:var(--r-lg);background:var(--group);box-shadow:var(--ring);display:grid;gap:var(--s-1)}
.hp-stat b{font:var(--fw-bold) var(--fs-20)/var(--lh-1-2) var(--ui);color:var(--hi)}
.hp-stat span{font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--fg-3)}
/* v117. Kept generic so a narrow sidebar never forces a tab wider than its
   share of the row. */
#side .head button{min-width:0}
/* The hosting guide, as a left drawer — its own element beside the editor
   sheet (never #tool-modal itself, so it can sit open next to it), same
   drawer-in motion #tool-modal.drawer-left already uses (My cards, v105). */
#guide-drawer{position:fixed;inset:0;z-index:var(--z-drawer);display:grid;place-items:stretch start;background:var(--shade-45)}
#guide-drawer.hidden{display:none}
#guide-drawer .card{width:min(420px,100vw);height:100%;overflow:auto;padding:var(--s-9);background:var(--paper);box-shadow:var(--elev-drawer-left);animation:drawer-in var(--t-panel) var(--ease-arrive)}
#guide-drawer h2{margin:0 0 var(--s-6);font:var(--fw-semibold) var(--fs-20) var(--ui);color:var(--hi)}
#guide-drawer h3{margin:var(--s-8) 0 var(--s-3);font:var(--fw-semibold) var(--fs-14) var(--ui);color:var(--hi)}
#guide-drawer p,#guide-drawer li{font:var(--fw-regular) var(--fs-14)/var(--lh-1-55) var(--ui);color:var(--fg-2)}
#guide-drawer ol,#guide-drawer ul{margin:0;padding-left:1.3em}
#guide-drawer #guide-close{position:absolute;top:var(--s-7);right:var(--s-7)}
/* v167. closeGuideDrawer (venue.js) used to jump straight to .hidden; it now
   holds .closing for one --t-settle first, same shape as #tool-modal's own
   drawer/sheet close, so this one slides back out its edge too. */
#guide-drawer.closing .card{animation:drawer-out var(--t-settle) var(--ease-in) forwards}
@media (prefers-reduced-motion:reduce){#guide-drawer .card,#guide-drawer.closing .card{animation:none}}
