  /* 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). */
    /* v171. --gold-a40 (the cover editor's own dashed-outline alpha) retired:
       nothing reads it now — the editor uses rgba(var(--accent-rgb),.4)
       instead, Munk's own call ("use the selected accent color as the
       default") — see the cover editor rules 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;
  /* v203. The scroll scrim's own fade distance — --s-15 already sits at 28px,
     so it earns no new rung of its own. */
  --scrim-size: var(--s-15);
  /* 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);
  }
  /* v203 (Munk, "Where something has a scroll, include a soft scrim on top or
     bottom that fades in and out as needed."). One reusable, tokenized scroll
     scrim, replacing #gate-scroll's old v173 overlay pair (a background
     gradient fading to --paper — correct only over a solid fill). This masks
     the scroller's own content instead, so it reads correctly over an image
     or a glass panel too, not just a solid surface.
     --scrim-top/--scrim-bottom (or -left/-right for .scrim-x) are written by
     scrollScrim() (venue.js) — 0px where nothing hides in that direction,
     --scrim-size where something does — and @property lets the mask stop
     itself, not just its opacity, transition smoothly. A browser without
     @property support can't interpolate an unregistered custom property, so
     the transition below silently no-ops there: the scrim still shows or
     hides correctly, just without the animation (Munk's asked-for fallback).
     Only the hidden band within --scrim-size of the edge is ever masked down
     — content already fully visible keeps full mask opacity (1), so this
     never softens contrast on text that isn't inside the fade itself. */
  @property --scrim-top{syntax:'<length>';inherits:true;initial-value:0px}
  @property --scrim-bottom{syntax:'<length>';inherits:true;initial-value:0px}
  @property --scrim-left{syntax:'<length>';inherits:true;initial-value:0px}
  @property --scrim-right{syntax:'<length>';inherits:true;initial-value:0px}
  .scrim-y{
    --scrim-top: 0px; --scrim-bottom: 0px;
    transition: --scrim-top var(--t-250) var(--ease), --scrim-bottom var(--t-250) var(--ease);
    -webkit-mask-image: linear-gradient(to bottom,transparent 0,black var(--scrim-top),black calc(100% - var(--scrim-bottom)),transparent 100%);
    mask-image: linear-gradient(to bottom,transparent 0,black var(--scrim-top),black calc(100% - var(--scrim-bottom)),transparent 100%);
  }
  .scrim-x{
    --scrim-left: 0px; --scrim-right: 0px;
    transition: --scrim-left var(--t-250) var(--ease), --scrim-right var(--t-250) var(--ease);
    -webkit-mask-image: linear-gradient(to right,transparent 0,black var(--scrim-left),black calc(100% - var(--scrim-right)),transparent 100%);
    mask-image: linear-gradient(to right,transparent 0,black var(--scrim-left),black calc(100% - var(--scrim-right)),transparent 100%);
  }
  @media (prefers-reduced-motion: reduce){.scrim-y,.scrim-x{transition:none}}
  /* 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.
     v173 (Munk, live screenshots): that read cramped — restored a rung or
     two, though not all the way back to the pre-166 numbers above. The card
     itself carries no padding of its own any more: it is a flex column of
     exactly two children, #gate-scroll-wrap (the scrolling body, padded) and
     #gate-footer (Join space, pinned — item 5, below), so "restore the
     card's padding" now means padding each of those consistently instead. */
  #gate .card{width:min(440px,94vw);max-height:100%;background:var(--paper);box-shadow:var(--elev-gate);border-radius:var(--r);display:flex;flex-direction:column;color:var(--ink-3);min-width:0}
  /* v191. Nothing on the door card until the first renderGate() decides
     between sign-in and the profile step (no flash of the wrong one). */
  #gate.booting .card{visibility:hidden}
  #gate .gate-left,#gate .gate-right{min-width:0}
  /* v173. The scrolling body: a flex column that shrinks (min-height:0)
     until #join's max-height is reached, then #gate-scroll scrolls. Not
     absolutely positioned — that gave the wrapper no intrinsic height and
     collapsed the card to its footer. */
  #gate-scroll-wrap{position:relative;flex:1 1 auto;min-height:0;display:flex;flex-direction:column}
  /* v196 (Munk: more padding in the sign-in/profile modals). Both the gap
     between pblocks and the padding around them stepped up one rung — the
     age check getting its own section (item 3, below) is exactly the kind
     of separation more room here is for. Checked at 390px: still fits with
     no horizontal scroll. */
  #gate-scroll{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable;display:grid;grid-template-columns:1fr;gap:var(--s-8);padding:var(--s-9) var(--s-15) var(--s-6)}
  /* v173. Pinned: never part of the scroll, always the card's last thing on
     screen. flex:none keeps it at its own content height regardless of how
     much #gate-scroll-wrap above it shrinks. */
  #gate-footer{flex:none;padding:var(--s-6) var(--s-15) var(--s-9);overflow:hidden;scrollbar-gutter:stable}
  #gate-footer .btn{width:100%}
  /* v203. #gate-scroll carries .scrim-y itself now (scrollScrim(), venue.js)
     — masked directly rather than an overlay pair on top of it; see the
     .scrim-y/.scrim-x definition above for why. */
  /* 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}
  /* v173. #enter left this list — it left #me-panel entirely (Join space is
     #gate-footer's now, a sibling the drawer never touches) — and
     #gate-health left it because #gate-health itself is gone (TRUST-03). */
  #me-panel.as-edit #gate-title,#me-panel.as-edit #auth-step,
  #me-panel.as-edit #who,#me-panel.as-edit #access-msg,#me-panel.as-edit #mode-note,
  #me-panel.as-edit #gate-trust{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. */
  /* v196. input[type=tel] (the age check's guardian-phone field) was missing
     from this list — every attribute this selector doesn't name renders as a
     bare, unstyled browser control, which is what tel looked like. */
  input[type=text],input[type=email],input[type=password],input[type=url],input[type=search],input[type=tel],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)}
  /* v185 / FFL SAFE-07. Pre-entry age check — the same understated weight
     #gate-trust already sets for the sound-permission hint in this card.
     v196: its own pblock now (index.html), set apart from Who you are
     rather than folded under About you, still never shown in the drawer's
     Profile pane (#me-panel.as-edit #age-block, unchanged below). The old
     native fieldset/legend/radio group is gone — #age-block's own content is
     built by ensureAgeBlockHTML (venue.js) as the same segmented control
     Sign in | Create account already uses (.seg.auth-mode, styled below,
     near .seg's own base rules) rather than three radios; what is left here
     styles its heading, the "ages 14 and up" line, and the guardian fields'
     reveal. */
  #me-panel.as-edit #age-block{display:none}
  /* Its own section: a hairline and room above, the same divider the drawer puts between pblocks (#me-panel.as-edit .pblock). */
  #gate #age-block{margin-top:var(--s-6);padding-top:var(--s-9);border-top:1px solid var(--line-2)}
  #age-block .pblock-h .req{margin-left:var(--s-2);font-weight:var(--fw-regular);color:var(--fg-4)}
  #age-note,#age-guardian-note,#age-msg{margin:var(--s-3) 0 0;font:var(--fw-regular) var(--fs-12-5)/var(--lh-1-45) var(--display);color:var(--ink-4)}
  /* v196. Height/opacity reveal, not a jump, when 14–15 is picked — the same
     grid-rows collapse #add-options already uses above (0fr/1fr, a single
     .inner child carrying the real content), so this reads as the same
     house move every other field that opens on this page already makes. */
  #age-guardian{display:grid;grid-template-rows:0fr;opacity:0;overflow:hidden;transition:grid-template-rows var(--t-panel) var(--ease-move),opacity var(--t-base) var(--ease)}
  #age-guardian.open{grid-template-rows:1fr;opacity:1;margin-top:var(--s-5)}
  #age-guardian .inner{display:grid;gap:var(--s-5);min-height:0}
  @media (prefers-reduced-motion: reduce){#age-guardian{transition:none}}
  /* v173/TRUST-03. #gate-health and its supporting rules (.gate-health-head,
     .text-action, .health-dot, ...) are gone with the markup — Munk,
     2026-09-27: the door no longer shows a service panel, admin or not. */
  #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)}
  /* v197. Munk, from a live screenshot: a plain black circle sat between the
     face and the "You" name label, and its icon read as invisible — the mic
     badge used --coral-icon for its "off" state, which resolves through
     --accent-icon to --hi; on the light theme's default Theme swatch --hi is
     the same near-black as this badge's own --ink background, so a dark icon
     sat on a dark circle. The badge's circle is fixed --ink in every theme
     (unlike --panel/--paper, which flip), so its icon needs a fixed-contrast
     colour too, not one that rides the room's accent: --on-dark-85 (white at
     85%, the existing ramp's closest step to the old 70% mark) for the
     muted/off glyph, full --white for a live mic — always readable over
     --ink regardless of theme or accent. Moved off the seam between face
     and name to the lower-right shoulder of the face circle, the same
     corner video-call apps use, mirroring .shield's own top-left placement
     below. */
  .bubble .badges{position:absolute;right:calc(var(--s-2) * -1);bottom:calc(var(--s-2) * -1);display:flex;gap:var(--s-3);z-index:var(--z-2)}
  .bubble .badges span{width:28px;height:28px;border-radius:50%;background:var(--ink);display:grid;place-items:center;color:var(--on-dark-85);
    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)}
  /* v199 (Munk: "the header kerning is too much"). The serif is spaced by its own design at display sizes; no added tracking. */
  .modal h2{margin:0;font:var(--fw-regular) var(--fs-22)/var(--lh-1-25) var(--brand);letter-spacing:0}
  .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. */
  /* v169. Larger and wider — Munk's own note reviewing the mockup ("larger
     and wider to fill the space better") — the exact same size the
     full-screen editor's own card uses (.ced-card-wrap, below), since
     coverCardHTML draws them both: min()'s own calc(100% - 32px) already
     covers a narrow phone, no separate media-query override needed. */
  /* Fix round 1. Fixed 560px filled a third of a 1640px window — scales with
     the viewport now, the same clamp() on both the member-facing modal card
     and the editor's own .ced-card-wrap (below), so the two stay the exact
     same size. #tool-modal .card's own generic rule already caps
     max-height at 92dvh with overflow:hidden and #tool-content scrolls
     internally (overscroll-behavior:contain), so a taller card at this
     width never forces the page itself to scroll. */
  #tool-modal.coming-soon .card{width:clamp(min(560px,calc(100% - 32px)),44vw,760px)}
  #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)}
  /* v169. The quiet "Edit cover" button under the read-only card — the real
     gate for an owner/admin's own room (showComingSoonCover), opens the
     full-screen editor. */
  .cs-edit-cover{display:flex;justify-content:center;margin-top:var(--s-6)}
  /* v175. openRoomPreview's own banner — the Room pane's "Preview the room"
     button used to closeTool() and silently walk into the room with nothing
     on screen saying so (Munk: "looks like the editor just closed"). This
     reuses the coming-soon card's own shell (#tool-modal.coming-soon) so
     the preview is exactly the room's own cover, with one persistent line
     pinned to the top of the scrolling card naming the mode and a
     prominent way back — sticky, not fixed, since #tool-content is already
     the one thing here that scrolls. */
  .rpv-banner{position:sticky;top:0;z-index:1;display:flex;align-items:center;justify-content:space-between;gap:var(--s-6);
    margin-bottom:var(--s-6);padding:var(--s-6) var(--s-8);border-radius:var(--r);background:var(--accent);color:var(--on-accent);
    font:var(--fw-semibold) var(--fs-13) var(--ui);box-shadow:var(--ring)}
  .rpv-end{display:flex;align-items:center;gap:var(--s-4)}
  .rpv-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 currentColor;opacity:.8}
  #rpv-back{height:30px;padding:0 var(--s-7);border-radius:var(--pill);background:var(--on-accent);color:var(--accent);font:var(--fw-semibold) var(--fs-13) var(--ui)}
  .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%}
  /* v177. The convention has no face on file — its mark sits in the same circle. */
  .cs-face-mark{display:grid;place-items:center;background:var(--group);color:var(--fg-2);font:var(--fw-semibold) var(--fs-15) var(--brand)}
  .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). The type rules themselves (#tool-content p/#tool-modal h3
     specificity note) moved down beside the rest of v169's larger sizing —
     see .cs-room-title/.cs-message/.cs-details/.cs-when/.cs-signup-note,
     below. */
  /* 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. */
  /* Fix round 1. The outline used to wrap the whole full-width block, even
     empty — a form-box look the mockup never had. The wrap is an
     inline-flex now, shrink-wrapped to the field's own content width
     (field-sizing:content, where supported, growing/shrinking as you type;
     min-width otherwise sensible for either placeholder), and centred with
     align-self on .cs-zone's own flex column — a per-child property, so the
     read-only <p> siblings (no this class) keep stretching full width and
     centre their own text exactly as before. The pencil hint's own
     right:0 (below) now lands on the field's real edge, not the zone's. */
  .cs-edit-wrap{position:relative;display:inline-flex;align-self:center;margin-top:var(--s-4)}
  .cs-edit-field{display:block;width:auto;min-width:min(230px,90%);max-width:100%;background:transparent;border:0;border-bottom:1px dashed var(--rule-line);border-radius:0;box-shadow:none;padding:var(--s-2) var(--s-4);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}
  /* v200 (Munk, from the approved demo: neither field said what it was
     for). A small muted label above each of the two editable fields.
     Fix round 4 (Munk, from the live preview: HEADER sat indented,
     content-width, while MESSAGE was already full-width — the two never
     shared a left edge). Both are full-width boxes now, sharing the
     card's own left/right edge (align-self:stretch overrides
     .cs-edit-wrap's own plain align-self:center, above — every OTHER
     edit field, none, still centres/hugs its own text) with the label
     flush to that same left edge above each. The pencil hint's own top
     offset (::after, above) shifts down to clear the label instead of
     sitting on top of it. */
  .cs-edit-wrap.cs-edit-message,.cs-edit-wrap.cs-edit-details{flex-direction:column;align-items:stretch;align-self:stretch;gap:var(--s-2)}
  .cs-edit-wrap.cs-edit-message::after,.cs-edit-wrap.cs-edit-details::after{top:calc(var(--s-2) + 22px)}
  .cs-edit-wrap.cs-edit-message .cs-edit-field,.cs-edit-wrap.cs-edit-details .cs-edit-field{width:100%;min-width:0}
  .cs-edit-label{margin:0;font:var(--fw-semibold) var(--fs-11) var(--ui);letter-spacing:var(--ls-004em);text-transform:uppercase;color:var(--fg-3)}
  /* The details/Message field's own live "n / 250" (wireCoverEditorCard,
     venue.js) — under the field, right-aligned like a tweet's own counter,
     warning in --err once the host reaches the cap rather than after. */
  .cs-edit-count{align-self:flex-end;margin:0;font:var(--fw-medium) var(--fs-11) var(--ui);color:var(--fg-3)}
  .cs-edit-count.warn{color:var(--err)}
  /* v169. Three cover styles (cover_style — coverCardHTML, public/venue.js).
     The old editing toolbar above the card is gone entirely — Munk
     rejected it as clunky and off-design; every control it held now lives
     in the full-screen editor's own controls panel (openCoverEditor,
     .ced-panel, below). Larger and wider now too, Munk's own note reviewing
     the mockup ("larger and wider to fill the space better") — card padding
     22px, 16px radius. The shared shape — .cs-card/.cs-hero/.cs-panel — is
     'countdown' as-is; 'poster' and 'letter' override just enough of it
     below.
     Fix round 1. Real zone elements now (.cs-zone — who/what, when,
     message+details — plus .tool-actions as the fourth), one per style in
     coverCardHTML, rather than margins guessing at "between" vs "within".
     .cs-panel's own gap is the 18-20px between zones; .cs-zone's own gap
     is the 6-8px within one — every .cs-host/.cs-room-title/.cs-tiles/
     .cs-when/.cs-message/.cs-details margin that used to carry that
     spacing is gone, so this only works through a .cs-zone wrapper.
     coverCardHTML never renders an empty zone (including .tool-actions
     itself — no host, no second button, not open means no actions zone
     and no hairline, not a strip with nothing in it). */
  /* v172. Munk: "we need more padding in this modal around the text." One
     inset for the whole card (--cs-pad), top included: the room title used
     to sit flush under the picture. */
  .cs-card{--cs-pad:clamp(28px,2.6vw,40px);display:grid;border-radius:16px;overflow:hidden;background:var(--panel);box-shadow:var(--ring)}
  .cs-panel{position:relative;padding:var(--cs-pad);display:flex;flex-direction:column;gap:clamp(18px,1.6vw,20px)}
  .cs-zone{display:flex;flex-direction:column;gap:clamp(6px,0.6vw,8px)}
  /* v167. Rounded top corners only, flush to the card's own edges — the
     negative inline margin cancels .cs-panel's own side padding 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.
     Fix round 1. Height scales with the card's own clamp()'d width (A). */
  .cs-hero{height:clamp(220px,20vw,300px);border-radius:0}
  /* v167 design polish carried into v169. #tool-content p / #tool-modal h3
     (below, both used throughout the drawer for ordinary body copy/section
     labels) are each one ID selector, which otherwise beats these plain
     classes on specificity alone regardless of source order — every rule
     here that sets type for an h3/p the card actually uses is scoped under
     both #tool-modal.coming-soon (the member-facing card, still a modal)
     and #cover-editor (the same card, in the full-screen editor) so it wins
     in either place a shared coverCardHTML output can land. Fix round 1:
     no more margin here — the zone/gap system above carries the spacing
     now — and every size is clamp()'d, scaling with the card. */
  #tool-modal.coming-soon .cs-room-title,#cover-editor .cs-room-title{margin:0;font:var(--fw-regular) clamp(24px,2vw,30px)/var(--lh-1-2) var(--brand);letter-spacing:0;text-transform:none;color:var(--hi);text-align:center}
  /* v200. cover_message reads as the card's own Header now (the small
     "Header" label is editor-only, above — this is what makes it READ as
     one everywhere, editable or not: coverMessageStyledHTML never took a
     second, plainer path). Bold and headline-sized in the display serif,
     under .cs-room-title's own clamp so it never outweighs the room's
     actual name. The "letter" style's own big italic quote
     (.cs-style-letter .cs-message, below) already reads as a headline and
     keeps its own rule, unchanged — this is everything else. */
  #tool-modal.coming-soon .cs-message,#cover-editor .cs-message{margin:0;font:var(--fw-bold) clamp(19px,1.6vw,23px)/var(--lh-1-3) var(--brand);color:var(--hi);text-align:center}
  /* v200. details reads as the card's own Message now — roomier (rows=3,
     venue.js) and a touch larger (16-17px, was 15-17) than before, tweet-
     like body text; still the card's own regular weight/colour, unchanged. */
  #tool-modal.coming-soon .cs-details,#cover-editor .cs-details{margin:0;font:var(--fw-regular) clamp(16px,1.2vw,17px)/var(--lh-1-5) var(--ui);color:var(--fg-2);text-align:center}
  #tool-modal.coming-soon .cs-when,#cover-editor .cs-when{margin:0;font:var(--fw-semibold) clamp(13px,1vw,15px) var(--ui);color:var(--fg-3);text-align:center}
  #tool-modal.coming-soon .cs-signup-note,#cover-editor .cs-signup-note{flex-basis:100%;margin:0;font:var(--fw-regular) var(--fs-12)/var(--lh-1-4) var(--ui);color:var(--fg-3);text-align:center}
  .cs-tiles{display:flex;justify-content:center;gap:var(--s-5)}
  .cs-tile{display:grid;justify-items:center;gap:var(--s-1);width:clamp(64px,6vw,80px);padding:var(--s-5) var(--s-3);border-radius:var(--r-md);background:var(--field);box-shadow:var(--ring)}
  .cs-tile b{font:var(--fw-semibold) clamp(22px,1.8vw,26px) 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", its consent line centred and small right under it.
     v169. A hairline above separates the actions zone from message/details,
     per the mockup's own four zones — border-top rather than a rule div, so
     it never shows for a room with nothing above it to separate from
     (:empty-adjacent isn't a thing, but every style always prints at least
     the host row, so this never actually stands alone).
     Fix round 2 (Munk, from a live screenshot: "Share my email" ran full
     width with a small stray "Share…" clipped at its own right edge). Root
     cause was two things stacked: this column-direction container still
     carried the base .tool-actions's own flex-wrap:wrap (only its own
     direction/alignment were ever overridden here), so once the actions
     zone had no free vertical room the consent note — "Shares this email
     with the room's host.", genuinely starting with "Share" — wrapped
     sideways into a second phantom column instead of just taking more
     height, and .cs-card{overflow:hidden} clipped everything past the
     button down to that lone stray word. flex-wrap:nowrap is the actual
     fix (this column always stacks, never wraps sideways); "Share my
     email"'s own new auto width, below, was a second, compounding
     contributor (the wider the forced-full button, the less height was
     left for its own note before that wrap kicked in). */
  .cs-card .tool-actions{flex-direction:column;align-items:stretch;flex-wrap:nowrap;gap:var(--s-4);margin-top:18px;padding-top:18px;border-top:1px solid var(--line)}
  /* Fix round 2. "Share my email" was the one item here with no width of
     its own (unlike .cs-cta's own align-self:center, above) — this
     container's align-items:stretch was the only thing sizing it, forced
     to width:100% besides. Content-sized and centred now, like every other
     action here; #cs-enter (Munk didn't ask for it, and it is the one
     literal call-to-action a full-width, unmissable button still suits)
     keeps its own width:100%. */
  #cs-join-list{align-self:center;background:var(--solid);color:var(--on-solid);box-shadow:none}
  #cs-enter{width:100%;background:var(--solid);color:var(--on-solid);box-shadow:none}
  /* Fix round 3 (Munk, watching the live preview: the buttons here lost
     .btn's own height/padding once they went auto width). .tool-actions
     .btn (the generic small-row variant, elsewhere in the file) was always
     winning on height/padding/font-size — invisible while "Share my email"
     was forced full width (a wide bar reads fine at 40px), obvious the
     moment it is content-sized instead. This restores the plain house .btn
     metrics for every button in the cover card's own actions zone —
     .cs-cta included, so "Visit …'s site" matches Share/Enter now rather
     than reading like a smaller, different control — and drops the plain
     browser underline a bare <a> otherwise keeps (.btn never resets
     text-decoration itself; nothing else here needed it, since this is the
     one .btn-shaped anchor in the file with no existing reset). */
  /* Fix round 4 (Munk: "Share my email" wrapped onto two lines — auto
     width should never mean squeezed). white-space:nowrap and the card's
     own min 560px (.ced-card-wrap, below) do the rest; the card, not the
     button, is what's free to be wide enough. */
  .cs-card .tool-actions .btn{height:48px;padding:0 var(--s-14);font-size:var(--fs-15);white-space:nowrap}
  .cs-cta{text-decoration:none}
  /* Fix round 3, the actual root cause of the squashed buttons above (the
     height/padding override was real too, but not sufficient on its own).
     .cs-signup-note's own flex-basis:100% is a "force this onto its own
     row" trick that only means anything in a ROW-direction .tool-actions —
     every other user of that class. Inside THIS column-direction one it
     asks the flex column for a note as tall as the column's own (indefinite,
     content-driven) height; a grid row sizing itself to this card's
     max-content reads that as a real demand and settles the whole card
     shorter than its buttons actually need at their real height, so the
     final layout pass then had to flex-shrink every button below 48px to
     fit — measurable (getComputedStyle) even though nothing here visibly
     overflowed or scrolled. flex-basis:auto is what a column item already
     wants by default; this only ever needs to undo the row-layout trick —
     in BOTH places .cs-signup-note's own flex-basis:100% rule reaches
     (#tool-modal.coming-soon, the plain member-facing card, above the
     editor's own #cover-editor), since both share this exact markup and
     this exact column .tool-actions. Each #id-prefixed to match (and, on
     the ID alone, already beat) the specificity of the rule it overrides —
     three plain classes lose to that ID no matter the source order. */
  #tool-modal.coming-soon .cs-card .cs-signup-note,#cover-editor .cs-card .cs-signup-note{flex-basis:auto}
  /* 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:clamp(380px,34vw,460px);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(--cs-pad);right:var(--cs-pad);top:-116px;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)}
  .cs-style-poster .cs-room-title{margin:0;text-align:left}
  .cs-style-poster .cs-when{margin:0;text-align:left}
  /* Fix round 4 (Munk: "Always open" on the poster image is dark text on a
     dark photo). .cs-room-title/.cs-when's own base rules — far above —
     are #tool-modal.coming-soon/#cover-editor prefixed (an ID each), which
     already beats these two plain-class overrides no matter the source
     order, so the light colour they were meant to set here never actually
     won; .cs-host-name a few lines up escaped this only because ITS base
     rule carries no ID. Same fix as .cs-signup-note's own flex-basis,
     above: match (here, exceed) the specificity of the rule being
     overridden. */
  #tool-modal.coming-soon .cs-style-poster .cs-room-title,#cover-editor .cs-style-poster .cs-room-title{color:var(--white)}
  #tool-modal.coming-soon .cs-style-poster .cs-when,#cover-editor .cs-style-poster .cs-when{color:var(--on-dark-85)}
  .cs-style-poster .cs-body{display:flex;flex-direction:column;padding-top:18px}
  /* 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:72px;height:72px}
  .cs-style-letter .cs-message,.cs-style-letter .cs-message-letter{font:var(--fw-regular) clamp(20px,1.8vw,24px)/var(--lh-1-4) var(--brand);font-style:italic;color:var(--hi);text-align:center;margin:0}
  .cs-style-letter .cs-when{text-align:center;text-transform:uppercase;letter-spacing:var(--ls-009em);font-size:var(--fs-11);margin:0}
  .cs-style-letter .cs-details{margin:0}
  /* v169. The full-screen cover editor (openCoverEditor, public/venue.js) —
     its own stage above the floor, not #tool-modal's sheet. Dimmed room
     behind (the same scrim/blur .modal already uses), back label top-left,
     the card centred in the space above the dock (the stage itself scrolls
     vertically if the card is taller than the space, never horizontally),
     a floating dock of four pills bottom-centre. Reduced motion: no
     fade/scale, shown or hidden outright — .hidden already does that; the
     transition rules below are simply skipped since nothing ever adds
     .cover-editing's own animation class for them to run. */
  /* Belt-and-suspenders alongside the editor's own z-index and Tab-trap
     (venue.js) — nothing on the dimmed floor can be clicked while it's open. */
  body.cover-editing #app{pointer-events:none}
  /* Fix round 1. The floor's own chrome — its dock, the +/eye/image pills,
     the view cluster (zoom), the corner Help button — used to just sit
     behind the semi-transparent scrim, peeking through and fighting the
     editor's own dock for attention. Hidden outright now; the room's own
     picture and pins stay visible (dimmed), which is the "dimmed room
     behind" the mockup actually asks for. */
  body.cover-editing #floorbar,body.cover-editing #topleft,body.cover-editing #viewdock,
  body.cover-editing #adminrail,body.cover-editing #floor-help{display:none}
  /* v177. The cover editor — the room stays dimmed behind (the same scrim/
     blur .modal already uses), the settings float over it in a detached
     glass panel (Munk: "instead of being a full sidebar, make it a detached
     floating sidebar"), and the card sits centred in the space beside it.
     Every size, colour and timing here is a venue token; the editor's own
     component tokens (--ced-*) are set once, on #cover-editor, and read by
     every rule below. --z-admin, not --z-drawer: the discard-changes confirm
     (#tool-modal, --z-drawer) has to paint above it. */
  #cover-editor{position:fixed;inset:0;z-index:var(--z-admin);
    background:var(--surface-a70);-webkit-backdrop-filter:var(--blur-6);backdrop-filter:var(--blur-6);
    --ced-inset:var(--s-10);--ced-panel-w:clamp(280px,26vw,340px);--ced-pad:var(--s-14);--ced-radius:var(--r-4xl);
    --ced-gap:var(--s-5);--ced-face:var(--s-11);--ced-hot:var(--accent);--ced-hot-wash:rgba(var(--accent-rgb),.07);
    --ced-ring-w:var(--s-1);--ced-dim:.3}
  #cover-editor.hidden{display:none}
  /* v169. showModal/hideModal (venue.js) toggle .hidden/.closing the same
     way every other modal here does, reusing .modal's own keyframes. */
  #cover-editor:not(.hidden){animation:lh-scrim-in var(--t-panel) var(--ease-out)}
  #cover-editor.closing{animation:lh-scrim-out var(--t-settle) var(--ease-in) forwards;pointer-events:none}
  #cover-editor:not(.hidden) .ced-card-wrap{animation:lh-card-in var(--t-340) var(--ease-arrive)}
  #cover-editor:not(.hidden) .ced-panel{animation:lh-card-in var(--t-340) var(--ease-arrive)}
  @media (prefers-reduced-motion:reduce){#cover-editor:not(.hidden),#cover-editor.closing,#cover-editor:not(.hidden) .ced-card-wrap,#cover-editor:not(.hidden) .ced-panel,#cover-editor .ced-fade,#cover-editor .ced-sec,#cover-editor .ced-sec-h{animation:none;transition:none}}

  /* The panel — detached from every edge by --ced-inset, frosted glass. */
  .ced-panel{position:absolute;top:var(--ced-inset);left:var(--ced-inset);bottom:var(--ced-inset);z-index:var(--z-2);width:var(--ced-panel-w);
    display:flex;flex-direction:column;min-height:0;overflow:hidden;border-radius:var(--ced-radius);
    background:var(--surface-a72);box-shadow:var(--elev-flyout);-webkit-backdrop-filter:var(--blur-20-sat);backdrop-filter:var(--blur-20-sat);
    /* Fix round 4 (Munk: the Opens/Closes date and time inputs render as
       solid white boxes with invisible text/icons). The panel's own glass
       is always the dark surface (Fix round 2, above) regardless of theme
       — but only its CSS custom properties were ever rescued for that; a
       native <input type=date/time>'s own chrome (the calendar/clock icon,
       its internal text) is drawn by the browser itself, keyed off the
       color-scheme property, not off --fg/--hi. In light theme this panel
       inherited color-scheme:light from :root, so Chromium drew those icons
       (and the field's native parts) for a LIGHT surface no matter what
       background/color the input rule itself set — color-scheme:dark here
       matches the glass every native control in this panel actually sits
       on, in both themes. */
    color-scheme:dark}
  /* Fix round 2. --surface-a72 is always the dark ink colour (v151's note by
     --surface-a15/28/62/70/72, above — "reuses --surface" (dark theme's own
     value), never a parallel one, on purpose: every scrim/backdrop in the
     file reads it so a dimmed room or a dark crop screen never flips
     light). The panel is the
     one place that dark glass also HOLDS readable text and controls, not
     just dims something behind it — so in light theme every token it reads
     (--hi/--fg-2/--fg-3/--fg-4/--ink-4/--group/--hover/--line/--line-2/
     --field/--ring) needs the same rescue #gate's own dark bars already use
     (:root[data-theme=light] #gate, far above) rather than light theme's
     dark-on-light values, which read as dark-on-dark here and vanish —
     title, back label, section headings, hostcard name/note, "fine" notes
     (--ink-4, the base .fine rule's own colour) and the Save button's own
     ring/hover were all unreadable before this. */
  :root[data-theme=light] #cover-editor .ced-panel{--fg:var(--white);--fg-2:rgba(255,255,255,.82);--fg-3:rgba(255,255,255,.56);--fg-4:rgba(255,255,255,.56);
    --hi:var(--white);--ink-3:var(--white);--ink-4:rgba(255,255,255,.62);--group:rgba(255,255,255,.05);--hover:rgba(255,255,255,.08);
    --line:rgba(255,255,255,.1);--line-2:rgba(255,255,255,.14);--field:rgba(255,255,255,.06);--glass:rgba(255,255,255,.06);
    --ring:inset 0 0 0 1px rgba(255,255,255,.18);color:var(--fg)}
  .ced-head{flex:none;display:grid;justify-items:start;gap:var(--s-2);padding:var(--ced-pad) var(--ced-pad) var(--s-9)}
  .ced-back{display:inline-flex;align-items:center;gap:var(--s-3);height:var(--s-30);margin-bottom:var(--s-6);padding:0 var(--s-6) 0 var(--s-4);border-radius:var(--pill);
    background:none;box-shadow:var(--ring);color:var(--fg-2);font:var(--fw-medium) var(--fs-13) var(--ui);transition:background var(--t-base),color var(--t-base)}
  .ced-back:hover{background:var(--hover);color:var(--hi)}
  .ced-back-icon{width:var(--s-8);height:var(--s-8);transform:scaleX(-1)}
  .ced-title{margin:0;font:var(--fw-semibold) var(--fs-30)/var(--lh-1-1) var(--brand);color:var(--hi)}
  .ced-sub{margin:0;font:var(--fw-regular) var(--fs-13)/var(--lh-1-4) var(--ui);color:var(--fg-3)}
  /* The one part that scrolls — overscroll contained so a fast swipe never
     rubber-bands into the floor behind. */
  .ced-scroll{flex:1 1 auto;min-height:0;overflow-y:auto;overscroll-behavior-y:contain;touch-action:pan-y;scrollbar-width:thin}

  /* A section — heading, then its controls. Lights up (--ced-hot-wash) with
     its zone on the card. */
  .ced-sec{display:grid;gap:var(--ced-gap);padding:var(--s-9) var(--ced-pad);border-top:1px solid var(--line);transition:background var(--t-base) var(--ease-out)}
  .ced-sec.ced-hot{background:var(--ced-hot-wash)}
  .ced-sec-h{margin:0;font:var(--fw-semibold) var(--fs-12) var(--ui);letter-spacing:var(--ls-004em);color:var(--fg-3);transition:color var(--t-base) var(--ease-out)}
  .ced-sec.ced-hot .ced-sec-h{color:var(--ced-hot)}
  .ced-sec .seg,.ced-sec .f,.ced-sec .idrow{padding:0;margin:0}
  .ced-sec .fine{margin:0;text-align:left}
  .ced-sec .tool-actions{margin:0;justify-content:flex-start}
  /* Fix round 4 (Munk: "None / RSVP / Their site" wrapped its own option
     labels onto two lines in the panel's own narrow width). Every segHTML
     picker in the panel (Host, Second button) keeps its option label on
     one line now, tightened up at this narrow a width with the panel's
     own --s-4/--s-3 in place of the option's own generic --s-25/--s-5 —
     "RSVP" (its own label shortened, venue.js) already fit even before this. */
  .ced-sec .seg button{white-space:nowrap;padding:var(--s-4) var(--s-3)}

  /* Host — the face and name the cover shows. */
  .ced-hostcard{display:flex;align-items:center;gap:var(--s-6);min-width:0}
  .ced-face{display:grid;place-items:center;flex:none;width:var(--ced-face);height:var(--ced-face);overflow:hidden;border-radius:50%;
    box-shadow:0 0 0 var(--ced-ring-w) var(--surface),0 0 0 calc(var(--ced-ring-w) * 2) var(--ced-hot)}
  .ced-face .av{width:100%;height:100%}
  .ced-face-mark{background:var(--group);color:var(--fg-2);font:var(--fw-semibold) var(--fs-17) var(--brand)}
  .ced-hostcard-text{display:grid;gap:var(--s-1);min-width:0}
  .ced-hostcard-name{font:var(--fw-semibold) var(--fs-15) var(--ui);color:var(--hi)}
  .ced-hostcard-note{font:var(--fw-regular) var(--fs-12-5)/var(--lh-1-4) var(--ui);color:var(--fg-3)}

  /* Style — three mini previews (coverStyleThumbInnerHTML, venue.js). */
  .ced-stylepick{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:var(--s-4)}
  .ced-style-opt{display:grid;justify-items:center;gap:var(--s-3);padding:var(--s-4);border-radius:var(--r-xl);background:none;box-shadow:var(--ring);color:var(--fg-2);
    font:var(--fw-medium) var(--fs-12) var(--ui);transition:background var(--t-base),box-shadow var(--t-base)}
  .ced-style-opt:hover{background:var(--hover)}
  .ced-style-opt.on{box-shadow:0 0 0 2px var(--accent);color:var(--hi)}
  .ced-style-thumb{position:relative;display:flex;flex-direction:column;justify-content:flex-end;gap:var(--s-1);width:100%;height:var(--s-31);
    border-radius:var(--r-sm);background:var(--field);box-shadow:var(--ring);overflow:hidden;padding:var(--s-2)}
  .ced-thumb-pic{display:block;height:var(--s-13);border-radius:var(--r-xs);background:linear-gradient(135deg,var(--accent),var(--group))}
  .ced-thumb-pic-full{position:absolute;inset:0;height:auto;border-radius:0}
  .ced-thumb-tiles{display:flex;gap:var(--s-1);position:relative;z-index:var(--z-1)}
  .ced-thumb-tiles span{flex:1;height:var(--s-18);border-radius:1px;background:var(--accent);opacity:.55}
  .ced-thumb-bar{position:relative;z-index:var(--z-1);height:var(--s-6);border-radius:1px;background:var(--shade-55)}
  .ced-thumb-face{align-self:center;width:var(--s-9);height:var(--s-9);border-radius:50%;background:var(--accent);opacity:.85;margin-top:var(--s-2)}
  .ced-thumb-lines{display:flex;flex-direction:column;gap:var(--s-1);align-items:center}
  .ced-thumb-lines span{width:var(--s-13);height:var(--s-21);border-radius:1px;background:var(--fg-3)}
  .ced-thumb-lines span:last-child{width:var(--s-8)}

  /* Picture — the venue's pictures, then Upload. */
  .ced-pic-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:var(--s-4)}
  .ced-pic-tile{padding:0;border-radius:var(--r-lg);overflow:hidden;aspect-ratio:4/3;background:none;box-shadow:var(--ring)}
  .ced-pic-tile img{display:block;width:100%;height:100%;object-fit:cover}
  .ced-pic-tile.on{box-shadow:0 0 0 2px var(--accent)}
  .ced-pic-add{display:grid;place-items:center;align-content:center;gap:var(--s-1);box-shadow:none;border:1px dashed var(--line-2);color:var(--fg-3);font:var(--fw-medium) var(--fs-11) var(--ui);transition:color var(--t-base),border-color var(--t-base)}
  .ced-pic-add:hover{color:var(--hi);border-color:var(--fg-3)}
  .ced-pic-add .i{width:var(--s-8);height:var(--s-8)}

  /* The foot — the view toggle right above Save; neither ever moves. The
     toggle's own group label is for screen readers only (aria-label). */
  .ced-foot{flex:none;display:grid;gap:var(--ced-gap);padding:var(--s-8) var(--ced-pad) var(--ced-pad);border-top:1px solid var(--line-2)}
  .ced-foot .seg{padding:0;gap:0;font-size:0}
  .ced-save{background:var(--accent);color:var(--on-accent)!important;font-weight:var(--fw-bold)}
  .ced-save:hover{background:var(--accent-deep)!important}
  .ced-foot .ced-save{width:100%;height:var(--s-11);border-radius:var(--r-2xl);font-size:var(--fs-15)}
  .ced-foot .ced-save:not(.dirty){background:none!important;box-shadow:var(--ring);color:var(--fg-3)!important;font-weight:var(--fw-medium)}

  /* Member view — every section fades back in place; the head and foot stay. */
  .ced-fade{transition:opacity var(--t-base) var(--ease-out)}
  #cover-editor.member-view-mode .ced-fade{opacity:var(--ced-dim);pointer-events:none}

  /* The stage — the card centred in the space between the panel and the
     room's own sidebar (#side), which stays on screen, dimmed, behind the
     scrim while the editor is open (v177, above) rather than being hidden
     outright like the floor's own chrome. Fix round 2. Was centred in
     (100% minus the panel's own reserved width) alone — a bare var(--s-8)
     on the right, so the card actually sat past true centre, drifting
     toward the sidebar the wider that sidebar was. var(--side) is the
     same token #side's own width already reads (0 under 900px, where
     #side is display:none — this collapses to the plain var(--s-8) margin
     the mobile layout below still expects).
     Fix round 4 (Munk: "on a smaller screen, the modal can be wider to
     overflow the right sidebar" — below a comfortable width, drop the
     sidebar as a boundary rather than squeeze the card). Base rule (every
     width from the phone breakpoint up) is single-sided again — clear of
     the panel, a plain var(--s-8) gutter on the right, the card free to
     sit over the dimmed sidebar exactly like a plain scrim would. The
     media query below restores dual-sidebar centring only once there is
     room for it: .ced-card-wrap's own comfortable minimum is 560px
     (below), so the switch-over is wherever this stage's own available
     width, minus both reservations, would first give that card at least
     560px of room — solved once for --ced-panel-w's own worst case (its
     clamp() caps at 340px past 1307px wide) and --side's own 320px,
     algebra kept here rather than re-derived by eye: comfortable at
     w - (340 + var(--ced-inset)*2) - (320 + var(--s-8)) >= 560, i.e.
     w >= 1276px (checked against 1024/1100/1280, per Munk's own list). */
  .ced-stage{position:absolute;inset:0;overflow-y:auto;overflow-x:hidden;overscroll-behavior-y:contain;touch-action:pan-y;display:flex;justify-content:center;
    padding:var(--s-15) var(--s-8) var(--s-15) calc(var(--ced-panel-w) + var(--ced-inset) * 2)}
  @media(min-width:1276px){
    .ced-stage{padding-right:calc(var(--side) + var(--s-8))}
  }
  /* Fix round 1 (A). The exact same clamp() #tool-modal.coming-soon .card
     uses, so the member-facing card and this one are always the same size. */
  .ced-card-wrap{width:clamp(min(560px,calc(100% - 32px)),44vw,760px);align-self:center}
  /* v169. Typed directly on the card — a faint dashed outline in the room's
     own accent (v171) is the only difference from the read-only text. */
  #cover-editor .cs-edit-field{min-height:0;border-bottom:0;outline:1px dashed rgba(var(--accent-rgb),.4);outline-offset:3px;border-radius:var(--r-sm);transition:background var(--t-base),outline-color var(--t-base)}
  #cover-editor .cs-edit-field:hover{background:var(--accent-soft)}
  #cover-editor .cs-edit-field:focus{outline-style:solid;outline-color:var(--accent)}
  #cover-editor .cs-edit-field::placeholder{font-style:italic}
  /* v200. The Message field (details) reads as a roomier, multi-line box
     now, even empty or short — the general min-height:0 above (every
     other edit field here still wants that, hugging one line of text)
     overridden back up to about three of this field's own lines. */
  #cover-editor .cs-edit-details .cs-edit-field{min-height:4.5em}

  /* v177. Linked highlighting — the lit zone on the card gets a ring and a
     tag naming its setting (data-ced-label). Zones are all in-flow elements,
     so anchoring the tag makes them position:relative and nothing else. */
  #cover-editor [data-ced-zone],#cover-editor [data-ced-ring]{position:relative}
  #cover-editor [data-ced-zone].ced-hot,#cover-editor [data-ced-ring].ced-hot{outline:var(--ced-ring-w) solid var(--ced-hot);outline-offset:var(--s-3);border-radius:var(--r-lg)}
  #cover-editor .cs-hero.ced-hot{outline-offset:calc(var(--ced-ring-w) * -2);border-radius:0}
  #cover-editor [data-ced-ring].ced-hot{border-radius:var(--r-16)}
  #cover-editor .ced-hot[data-ced-label]::after{content:attr(data-ced-label);position:absolute;top:calc(var(--s-5) * -1);left:var(--s-6);z-index:var(--z-3);pointer-events:none;
    padding:var(--s-1) var(--s-4);border-radius:var(--r-md);background:var(--ced-hot);color:var(--on-accent);font:var(--fw-semibold) var(--fs-11) var(--ui);letter-spacing:var(--ls-004em)}
  #cover-editor .cs-hero.ced-hot::after,#cover-editor [data-ced-ring].ced-hot::after{top:var(--s-6)}
  #cover-editor [data-ced-ring].ced-hot::after{left:auto;right:var(--s-6)}

  /* Phone (<600px): the stage on top, the panel a bottom sheet — still
     detached from the edges, never a horizontal page scroll. */
  @media(max-width:599px){
    #cover-editor{--ced-inset:var(--s-4);--ced-pad:var(--s-8)}
    .ced-panel{top:auto;right:var(--ced-inset);width:auto;max-height:52vh}
    .ced-stage{padding:var(--s-8) var(--s-4) calc(52vh + var(--ced-inset) * 2)}
    .ced-card-wrap{width:100%;align-self:flex-start}
  }
  /* 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. */
  /* v198. Contact cards are the locked LegendHaven flip card (tradingCardHTML,
     venue.js) — MyBrain vault, convention/element-cards/_element-card-flip.html.
     The card is a physical object — the same dark ink and gold in either
     theme — so its material colours are its own component tokens, named once
     here. Every size is in em off --tcard-w (1em = a thirtieth of the card's
     width), so setting --tcard-w alone scales the whole card, from a request
     row's thumbnail to the card view. No chips, icons, outer border or glow —
     just full-bleed face art, the name in the venue's own display serif
     (--brand), a role line, and a thin gold hairline inset (the flip card's
     own locked material; nothing else on the venue uses gold as an accent —
     see v171). The role line's colour lifts the venue's existing role
     colours (--author-plum, --knight-green, --host-gold). */
  .tcard-b{--tcard-w:300px;--tcard-ink:#141518;--tcard-gold:#C9A35A;--tcard-gold-dim:rgba(201,163,90,.34);--tcard-cream:#F6F2E9;--tcard-mute:#9a978e;--tcard-flip-t:.66s;--tcard-flip-ease:cubic-bezier(.42,.08,.24,1)}
  .tcard-b.tcard-static{position:relative;box-sizing:border-box;width:var(--tcard-w);aspect-ratio:300/420;font-size:calc(var(--tcard-w) / 30);font-family:var(--ui);text-align:left}
  /* The real card: a <button> (all:unset so it carries no browser chrome),
     click-anywhere-to-flip, an edge-proximity lean on hover that springs
     back and never auto-flips — wired by wireTradingCards. */
  .tcard-b:not(.tcard-static){all:unset;box-sizing:border-box;display:block;width:var(--tcard-w);aspect-ratio:300/420;font-size:calc(var(--tcard-w) / 30);font-family:var(--ui);text-align:left;cursor:pointer;perspective:127em}
  .tcard-b:not(.tcard-static):focus-visible{outline:2px solid var(--accent);outline-offset:.3em;border-radius:1.1em}
  .tcard-tilt{display:block;position:relative;width:100%;height:100%;transform-style:preserve-3d;transition:transform var(--t-ease) var(--ease-out);will-change:transform}
  .tcard-flip{display:block;position:relative;width:100%;height:100%;transform-style:preserve-3d;transition:transform var(--t-280) var(--ease-move);will-change:transform}
  .tcard-anim-back{animation:tcard-flip-back var(--tcard-flip-t) var(--tcard-flip-ease) forwards}
  .tcard-anim-front{animation:tcard-flip-front var(--tcard-flip-t) var(--tcard-flip-ease) forwards}
  @keyframes tcard-flip-back{0%{transform:rotateY(0deg)}52%{transform:rotateY(199deg)}72%{transform:rotateY(169deg)}88%{transform:rotateY(185deg)}100%{transform:rotateY(180deg)}}
  @keyframes tcard-flip-front{0%{transform:rotateY(180deg)}52%{transform:rotateY(-19deg)}72%{transform:rotateY(11deg)}88%{transform:rotateY(-5deg)}100%{transform:rotateY(0deg)}}
  /* Either face — an inner hairline inset ~9px (.9em), matched front↔back. */
  .tcard-side{position:absolute;inset:0;border-radius:1.1em;overflow:hidden;background:var(--tcard-ink);box-shadow:0 1.6em 3.4em rgba(0,0,0,.5);backface-visibility:hidden;-webkit-backface-visibility:hidden}
  .tcard-side::after{content:"";position:absolute;inset:.9em;border:1px solid var(--tcard-gold-dim);border-radius:.7em;z-index:5;pointer-events:none}
  .tcard-flip .tcard-back{transform:rotateY(180deg)}
  .tcard-role-host{--tcard-role:var(--host-gold)}.tcard-role-author{--tcard-role:var(--author-plum)}.tcard-role-knight{--tcard-role:var(--knight-green)}.tcard-role-member{--tcard-role:var(--tcard-mute)}

  /* Front: full-bleed face art masked with no hard line, the name centred in
     the display serif, the role as a type line, where you met at the foot. */
  .tcard-art{position:absolute;top:0;left:0;right:0;height:36em;overflow:hidden;-webkit-mask-image:linear-gradient(to bottom,black 0,black 62%,transparent 100%);mask-image:linear-gradient(to bottom,black 0,black 62%,transparent 100%)}
  .tcard-img{position:absolute;inset:-1.6em;background:var(--tcard-ink) var(--tcard-src,none) 50% 15%/cover no-repeat;transition:transform var(--t-ease) var(--ease-out);transform:scale(1.05);will-change:transform}
  .tcard-img-init{position:absolute;inset:0;display:grid;place-items:center;background:var(--tcard-ink);font:var(--fw-semibold) 7.2em/1 var(--brand);color:var(--tcard-gold)}
  .tcard-plate{position:absolute;left:0;right:0;bottom:0;z-index:2;padding:0 2em 2.2em;display:flex;flex-direction:column;align-items:center;text-align:center;transition:transform var(--t-ease) var(--ease-out);will-change:transform}
  .tcard-nm{margin:0;font:var(--fw-bold) 2.7em/1.05 var(--brand);color:var(--tcard-cream);text-wrap:balance;text-shadow:0 .08em .6em rgba(0,0,0,.55)}
  .tcard-ptype{margin-top:.9em;font:var(--fw-bold) 1em/1.2 var(--ui);letter-spacing:.22em;text-transform:uppercase;color:var(--tcard-role,var(--tcard-mute))}
  .tcard-metline{margin-top:1.3em;font:var(--fw-semibold) 1.1em/1.35 var(--ui);letter-spacing:.02em;color:var(--tcard-mute);text-wrap:balance}

  /* Back: the face low behind as a backdrop (adaptive mask set by
     wireTradingCards, once the card has its real height), 2.4em of side
     padding, tagline, site, email, where you met again, card number. */
  .tcard-bart{position:absolute;top:0;left:0;right:0;height:18.6em;overflow:hidden;-webkit-mask-image:linear-gradient(to bottom,black 0,black 38%,transparent 78%);mask-image:linear-gradient(to bottom,black 0,black 38%,transparent 78%)}
  .tcard-bimg{position:absolute;inset:-1.6em;background:var(--tcard-ink) var(--tcard-src,none) 50% 12%/cover no-repeat;opacity:.5;transition:transform var(--t-ease) var(--ease-out);transform:scale(1.05);will-change:transform}
  .tcard-bimg-init{position:absolute;inset:0;display:grid;place-items:center;opacity:.5;font:var(--fw-semibold) 7.2em/1 var(--brand);color:var(--tcard-gold)}
  .tcard-bbody{position:absolute;left:0;right:0;bottom:0;z-index:2;padding:0 2.4em 2.4em;display:flex;flex-direction:column;transition:transform var(--t-ease) var(--ease-out);will-change:transform}
  .tcard-tagline{margin:0 0 .9em;font:italic var(--fw-regular) 1.4em/1.3 var(--brand);color:var(--tcard-cream);text-wrap:balance}
  .tcard-contact{display:grid;gap:.3em;margin:0 0 .9em}
  .tcard-row{overflow:hidden;white-space:nowrap;text-overflow:ellipsis;font:var(--fw-medium) 1.15em/1.3 var(--ui);color:var(--tcard-mute)}
  .tcard-bfoot{font:var(--fw-semibold) .95em/1 var(--ui);letter-spacing:.03em;color:var(--tcard-mute);font-variant-numeric:tabular-nums}

  /* The generic, no-personal-data backer — a request not yet accepted. */
  .tcard-hidden{display:flex;flex-direction:column;align-items:center;justify-content:center;gap:1.4em;background-image:repeating-linear-gradient(45deg,rgba(201,163,90,.07) 0 1px,transparent 1px 1.4em),repeating-linear-gradient(-45deg,rgba(201,163,90,.07) 0 1px,transparent 1px 1.4em)}
  .tcard-mark{position:relative;width:20em;height:auto}
  .tcard-hidden-label{position:relative;font:var(--fw-semibold) 1em/1 var(--ui);letter-spacing:.24em;text-transform:uppercase;color:var(--tcard-mute)}

  /* prefers-reduced-motion: an instant crossfade, no tilt or parallax. */
  @media (prefers-reduced-motion:reduce){
    .tcard-tilt,.tcard-flip,.tcard-img,.tcard-bimg,.tcard-plate,.tcard-bbody{transition:none}
    .tcard-b:not(.tcard-static) .tcard-flip{transform:none}
    .tcard-b:not(.tcard-static) .tcard-front,.tcard-b:not(.tcard-static) .tcard-back{transition:opacity 0s;transform:none}
    .tcard-b:not(.tcard-static).tcard-flipped .tcard-front{opacity:0}
    .tcard-b:not(.tcard-static):not(.tcard-flipped) .tcard-back{opacity:0}
    .tcard-anim-back,.tcard-anim-front{animation:none}
  }

  /* v198. You › Cards and lists — Your card, now the real flip card, then
     trade requests, then the collection grid. */
  .ycards-own{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:var(--s-10);align-items:center;margin-bottom:var(--s-10)}
  .ycards-own-card{--tcard-w:180px;justify-self:center}
  .ycards-own-text{display:grid;gap:var(--s-5);justify-items:start}
  .ycards-own-text h3{margin:0}
  .ycards-own-text p{margin:0;color:var(--fg-2);font:var(--fw-regular) var(--fs-14)/var(--lh-1-5) var(--ui)}
  .ycards-how{margin:0;padding-left:1.3em;color:var(--fg-2);font:var(--fw-regular) var(--fs-13)/var(--lh-1-5) var(--ui)}
  .ycards-req{display:flex;align-items:center;gap:var(--s-6);padding:var(--s-5) 0;border-bottom:1px solid var(--line)}
  .ycards-req>span{flex:1;min-width:0;color:var(--fg-2);font:var(--fw-medium) var(--fs-14) var(--ui)}
  .ycards-req-card .tcard-b{--tcard-w:48px}
  .ycards-count{margin-left:var(--s-3);font-weight:var(--fw-regular);color:var(--fg-3)}
  .ycards-grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(120px,1fr));gap:var(--s-8)}
  .ycards-tile{container-type:inline-size;padding:0;border-radius:var(--r-lg);background:none;transition:transform var(--t-base) var(--ease-move)}
  .ycards-tile .tcard-b{--tcard-w:100cqi}
  .ycards-tile:hover{transform:translateY(calc(var(--s-21) * -1))}
  .ycards-tile:focus-visible{outline:2px solid var(--accent);outline-offset:var(--s-2)}

  /* v188. The trade — their card flips face up while yours slides over. In
     the popover (a request opened from the drawer) there is no room for the
     second card, so only the flip plays there. */
  .ctrade{display:grid;gap:var(--s-8);justify-items:center}
  .ctrade-sub{margin:0;max-width:44ch;text-align:center;color:var(--fg-2);font:var(--fw-regular) var(--fs-14)/var(--lh-1-5) var(--ui)}
  .ctrade-stage{--ctrade-w:240px;display:flex;align-items:center;justify-content:center;gap:var(--s-14);perspective:1400px}
  .ctrade-flip{position:relative;width:var(--ctrade-w);aspect-ratio:300/420;transform-style:preserve-3d;transition:transform var(--t-700) var(--ease-move)}
  .ctrade-flip.on{transform:rotateY(180deg)}
  .ctrade-face{position:absolute;inset:0;backface-visibility:hidden;-webkit-backface-visibility:hidden}
  .ctrade-face .tcard-b{--tcard-w:var(--ctrade-w)}
  .ctrade-front{transform:rotateY(180deg)}
  .ctrade-mine{display:grid;justify-items:center;gap:var(--s-4);opacity:0;transform:translateX(calc(var(--s-28) * -1)) rotate(-4deg);color:var(--fg-3);font:var(--fw-regular) var(--fs-13) var(--ui);
    transition:opacity var(--t-500) var(--ease-out) var(--t-500),transform var(--t-700) var(--ease-move) var(--t-500)}
  .ctrade-mine .tcard-b{--tcard-w:140px}
  .ctrade-mine.on{opacity:1;transform:rotate(4deg)}
  #flyout .ctrade-mine{display:none}
  #flyout .ctrade-stage{--ctrade-w:200px}
  .ctrade-actions{justify-content:center}

  /* v198. One card, large and interactive — click or press it to see the back. */
  .cview{display:grid;justify-items:center;gap:var(--s-8)}
  .cview .tcard-b{--tcard-w:240px}
  .cview-actions{display:grid;gap:var(--s-4);width:100%}
  .cview .fine{margin:0;text-align:center}
  @media (prefers-reduced-motion:reduce){.ctrade-flip,.ctrade-mine,.ycards-tile{transition:none}.ycards-tile:hover{transform:none}}
  /* 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)}
    /* v173. The card itself carries no grid/padding of its own any more
       (#gate-scroll/#gate-footer own that now, unconditionally) — only its
       width still needs a narrow-viewport override. */
    #gate .card{width:100%}
    #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}
  /* v170. docs/ui-backlog.md "Narrow the small action modals" — Munk, on Mute
     NAME: "this modal can be constrained to the width of the save button;
     doesn't need to be so wide." 760px is the width a room editor or a list of
     content needs, not a one-field or confirm dialog. formModal's own
     `{compact:true}` opt-in (never automatic — see formModal) sets this class;
     the width is the save pill's own width (#tool-foot-left:empty + .tool-
     foot-right .btn's old min(320px,100%)) plus the card's own padding,
     rounded up to a number that still reads as a card, not a coincidence. */
  #tool-modal.narrow .card{width:min(440px,calc(100vw - 32px))}
  /* Compact's Save is the one control in the footer, so it runs edge to edge
     inside the footer's own padding rather than sitting as a centred pill with
     empty rail either side (the shape #tool-foot-left:empty + .tool-foot-right
     .btn gives every OTHER save-only footer, sheet and card alike). More IDs
     in this selector than that one, so it wins without !important. */
  #tool-modal.narrow #tool-foot-left:empty + .tool-foot-right{margin:0;flex:1;justify-content:stretch}
  #tool-modal.narrow #tool-foot-left:empty + .tool-foot-right .btn{width:100%;min-width:0}
  /* 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)}
  /* v197. Munk: the Account pane's own Delete my account block read as
     unclear two-column layout — heading/explanation left, button floating at
     the right, with the confirm field and its message squeezed into that
     same narrow left column. This modifier keeps Delete this room (This
     room › Room, short and two-column on purpose — a heading, one line, one
     button) exactly as it was, and stacks the Account pane's longer block
     into one left-aligned column instead: heading, explanation, confirm
     field, message, Delete my account button, top to bottom — the DOM order
     youDataSectionHTML already builds (venue.js), so no markup moved, only
     the axis. Full width is "fine" on a phone, not required at any width;
     the button keeps its own intrinsic size and left edge. */
  .danger-block.acct-delete{flex-direction:column;align-items:stretch;justify-content:flex-start}
  .danger-block.acct-delete .btn.quiet-danger{align-self:flex-start}
  /* v197. You › Account's own "Signed in as" block (youAccountPaneHTML) —
     left-aligned, no border-top/centring (that was .fine's, dropped along
     with it here). --ink-2 is the same theme-flipping "clear body text"
     token the rest of the drawer already reads with (near-white on the dark
     paper, near-black on the light one) rather than the old hardcoded
     --white, which is how the email went invisible on a light-theme drawer. */
  .acct-whoami{margin:0 0 var(--s-8);text-align:left}
  .acct-whoami .acct-lbl{margin:0 0 var(--s-1);font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--ink-4)}
  .acct-whoami .acct-id{margin:0;font:var(--fw-semibold) var(--fs-16) var(--ui);color:var(--ink-2);overflow-wrap:anywhere}
  .acct-whoami .acct-role{margin:var(--s-1) 0 0;font:var(--fw-regular) var(--fs-13) var(--ui);color:var(--ink-4)}
  /* 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: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}
  /* v202. .editnav-narrow (venue.js's own openDrawer) is the narrow rail's
     replacement — built and mounted every time regardless of width, shown
     only below the same 760px this whole section already turns on at. Its
     default (this rule, unconditional) is hidden — the wide accordion just
     below is what every width above 760px actually shows. */
  .editnav-narrow{display:none}
  /* Under 760px the sheet takes the screen. The wide rail's own accordion
     retires outright here rather than lying down (Munk, live screenshot:
     group headings and every group's icon-only items were sharing one row,
     a bare scrollbar running underneath, nothing left readable once the
     label span was hidden) — .editnav-narrow, below, is what shows in its
     place: the group picker on its own row, the open group's own items as
     pills on a second. */
  @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{display:none}
    .editpane{padding:var(--s-8) var(--s-9) var(--s-14)}
    /* v202. Row 1 then row 2, a real gap between them rather than the wide
       rail's own single flex row. #tool-content's own narrow rule above
       already gives .editnav-narrow (a sibling of .editnav, not nested in
       it — venue.js's own openDrawer comment on why) grid-column:1/-1 and
       padding-inline:var(--s-9); this only adds the vertical rhythm. */
    .editnav-narrow{display:flex;flex-direction:column;gap:var(--s-6);padding:var(--s-5) 0 var(--s-6)}
    /* Row 1, more than one group open on this drawer (an admin can have
       This room + Venue + You at once): the house segmented control,
       nothing about .seg itself changed — id-scoped only so this beats
       .seg's own unconditional padding (both single-class selectors, and
       .seg is declared later in this file, which would otherwise win the
       tie regardless of media-query nesting). */
    .editnav-narrow .drawer-groupseg{padding-inline:var(--s-9)}
    /* Row 1, exactly one group (every role but admin): a plain heading in
       its place — same eyebrow style the wide rail's own group heading used. */
    .drawer-groupname{margin:0;padding:0 var(--s-9);font:var(--fw-semibold) var(--fs-11) var(--ui);letter-spacing:var(--ls-01em);text-transform:uppercase;color:var(--ink-4)}
    /* Row 2: the open group's own items as pills, one scrolling row per
       group — only the open one is ever shown (updateNav, venue.js). The
       scrollbar itself is hidden (a visible one running underneath was
       part of Munk's own complaint) and a right-edge fade says there is
       more to reach, the same mask the wide rail's own overflow used above
       this width; scroll-snap keeps a swipe from stopping mid-pill.
       --seg-pick/--seg-on-pick/--seg-hover/--seg-label are the same v199
       tokens .seg itself carries — redeclared here rather than shared
       since a pill row is never a descendant of the .seg it sits beside. */
    .drawer-pillrow{--seg-pick:var(--accent);--seg-on-pick:var(--on-accent);--seg-hover:var(--hover);--seg-label:var(--fg-3);
      display:flex;align-items:center;gap:var(--s-3);overflow-x:auto;overflow-y:hidden;
      padding:0 var(--s-9) var(--s-2);scroll-snap-type:x proximity;scrollbar-width:none;
      -webkit-mask-image:linear-gradient(to right,#000 calc(100% - 32px),transparent);mask-image:linear-gradient(to right,#000 calc(100% - 32px),transparent)}
    .drawer-pillrow::-webkit-scrollbar{display:none}
    .drawer-pillrow[hidden]{display:none}
    .drawer-pillrow button{position:relative;flex:none;display:inline-flex;align-items:center;gap:var(--s-4);
      padding:var(--s-6) var(--s-9);min-height:40px;border-radius:var(--pill);background:var(--group);color:var(--seg-label);
      font:var(--fw-semibold) var(--fs-13) var(--ui);white-space:nowrap;scroll-snap-align:start;
      transition:background var(--t-base),color var(--t-base)}
    .drawer-pillrow button .i{width:16px;height:16px;flex:none}
    .drawer-pillrow button:hover{background:var(--seg-hover);color:var(--hi)}
    /* The current item is filled, the rest stay quiet — the same
       filled/quiet split every other .seg option already reads with. */
    .drawer-pillrow button[aria-current="true"]{background:var(--seg-pick);color:var(--seg-on-pick)}
    .drawer-pillrow button:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
    /* v167's own rail badge, moved out of its absolute icon-overlay spot
       (the wide rail's own icon-only row had nowhere else to put it) and
       into normal flow after the label, since a pill always shows one. */
    .drawer-pillrow button .railbadge{position:static;flex:none;min-width:14px;height:14px;padding:0 3px;margin-left:var(--s-1);
      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}
    .drawer-pillrow button .railbadge:not(.hidden){animation:lh-badge-pop var(--t-panel) var(--ease-pop)}
    @media (prefers-reduced-motion:reduce){.drawer-pillrow button .railbadge:not(.hidden){animation:none}}
    /* Munk: "never a full-size inline heading" for the Moderation
       sub-group — .editnav-sublabel's own mobile treatment, just below, is
       already exactly a thin divider plus a tiny muted label, so it is
       reused here as-is (narrowPillRowHTML, venue.js) rather than given a
       second rule 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)}
  /* v199. Segmented-control component tokens (Munk: one toggle design that works in dark and light). Each maps to a
     theme token that already flips with data-theme, so the control never carries a colour of its own. */
  .seg{--seg-track:var(--group);--seg-ring:var(--ring);--seg-label:var(--fg-3);--seg-label-hover:var(--fg);--seg-hover:var(--hover);--seg-pick:var(--accent);--seg-on-pick:var(--on-accent);--seg-focus:var(--accent)}
  .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{position:relative;display:flex;gap:var(--s-2);padding:var(--s-21);border-radius:var(--pill);background:var(--seg-track);box-shadow:var(--seg-ring)}
  .seg button{position:relative;z-index:1;flex:1;padding:var(--s-25) var(--s-5);border-radius:var(--pill);font:var(--fw-semibold) var(--fs-12) var(--ui);color:var(--seg-label);background:none;transition:color var(--t-280) var(--ease-move),background var(--t-base) var(--ease)}
  .seg button:not([aria-checked="true"]):not([aria-pressed="true"]):hover{color:var(--seg-label-hover);background:var(--seg-hover)}
  /* v174. aria-checked joins aria-pressed — segHTML's buttons are role="radio"
     now (radiogroup semantics), not aria-pressed toggle buttons. */
  .seg button[aria-pressed="true"],.seg button[aria-checked="true"]{background:var(--seg-pick);color:var(--seg-on-pick)}
  .seg button:focus-visible{outline:2px solid var(--seg-focus);outline-offset:2px}
  /* v196. The sliding thumb (paintSegThumb, venue.js) — only ever created
     for a `.seg.auth-mode` control (Sign in | Create account, and the age
     check's 16+ | 14–15), so this stays inert for every ordinary segHTML
     picker (room settings, the cover editor), which never grows one. It
     carries the accent fill and slides; `.auth-mode button[aria-checked]`
     below hands its own solid background over to it, or the two would just
     be the same colour stacked with no visible motion at all. Inset by the
     same --s-21 the pill's own padding already is, so it lines up with the
     buttons' own box exactly rather than the padding around them. */
  .seg-thumb{position:absolute;top:var(--s-21);bottom:var(--s-21);left:0;border-radius:var(--pill);background:var(--seg-pick);
    transition:transform var(--t-280) var(--ease-move),width var(--t-280) var(--ease-move);pointer-events:none;z-index:0}
  @media (prefers-reduced-motion: reduce){.seg-thumb{transition:none}}
  /* v199. Only once the thumb is really drawn (.thumbed, paintSegThumb); until then the picked button keeps its own fill. */
  .auth-mode .opts.thumbed button[aria-checked="true"]{background:none}
  /* 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-logo[hidden]{display:none}
  /* v182 (Munk, live screenshot): the door cards sat 24px top and left, 34px
     right (the scroll gutter reserved on a card that never scrolls), and 8px
     under the last link. One even frame on every side for the sign-in
     cards only; the profile step keeps #gate-scroll's own padding.
     v196 (Munk: more padding): 32px stepped up to 40px, still checked at
     390px with no horizontal scroll. */
  #join:has(.auth-confirm) #gate-scroll{padding:var(--s-12);scrollbar-gutter:auto}
  /* v196. `.step-anim`, added by JS and always removed+re-added to restart
     it (never left matching a selector on its own, so there is no cascade
     moment — booting ending, a class merely staying put — that could fire
     this by accident): a MutationObserver plays it on #auth-step every time
     one of the renderXStep() functions (renderSignInStep, renderEmailStep —
     pinned byte-for-byte, tests/client.test.mjs — renderForgotPasswordStep,
     renderCodeStep/renderCodeEntry/renderCodeGiven, renderSetNewPasswordStep)
     swaps its whole content, without any of them needing to know motion
     exists; renderGate() plays it on #profile-step the one time signing in
     swaps it in for #auth-step, and only then — never on the render that
     first decides which step to show. The same idiom #app.booting's own
     rows already use (lh-say/--ease-arrive). Removed under
     prefers-reduced-motion. */
  #auth-step.step-anim,#profile-step.step-anim{animation:lh-say var(--t-280) var(--ease-arrive) both}
  @media (prefers-reduced-motion: reduce){#auth-step.step-anim,#profile-step.step-anim{animation:none}}
  .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}
  /* v179. Google's own guidance: a neutral, bordered, white button, the same
     regardless of which of the venue's own themes is active — unlike
     --line-2 (which flips to a WHITE-based rgba in dark mode, invisible on
     a white button), --white/--ink/--ink-a20 are declared once in the base
     :root block and never redefined for :root[data-theme=light], so they
     stay the fixed tones this button needs (the same reasoning #toast's own
     border, --ring-toast-btn/--ink-a35, already relies on above). The G
     mark's own colours live in the SVG markup (GOOGLE_G_SVG, venue.js),
     never here. */
  .auth-google-btn{background:var(--white);color:var(--ink);box-shadow:inset 0 0 0 1px var(--ink-a20)}
  .auth-google-btn:hover{box-shadow:inset 0 0 0 1px var(--ink-a20),0 0 0 3px var(--halo)}
  .auth-google-icon{flex:none}
  /* v187. "Continue with email" matches the Google button above it; the
     email/password block and the code link stay hidden until it is pressed. */
  .auth-email-btn .i{flex:none}
  .auth-signin > [hidden]{display:none}
  /* v187. The eye sits inside the password field, right-aligned on the
     input row (the label's text sits above the input, so it anchors to the
     label's bottom edge); the input keeps room for it. */
  .pw-f{position:relative}
  .pw-f input{padding-right:var(--s-12)}
  .pw-eye{position:absolute;right:var(--s-2);bottom:var(--s-2);width:var(--s-12);height:var(--s-12);display:grid;place-items:center;padding:0;border:0;background:none;color:var(--fg-3);cursor:pointer;border-radius:var(--pill)}
  .pw-eye[aria-pressed="true"]{color:var(--accent)}
  .pw-eye:focus-visible{outline:2px solid var(--accent);outline-offset:-2px}
  .auth-email .pw-f input ~ .pw-eye{bottom:var(--s-3)}
  /* 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. */
  /* v181 audit: the card centres its children (justify-items:center), which
     shrank the email/password block to its content — 260px under a 366px
     Google button. The form fills the card width like the button above it. */
  .auth-confirm > .auth-email{justify-self:stretch}
  /* v186. The Sign in / Create account switch runs the card width, directly
     under the intro, like the Google button below it.
     v196: `.auth-confirm >` dropped — the age check's own toggle (its own
     pblock, not a child of .auth-confirm) reuses this exact class and wants
     the same full-width, zero-padding treatment. */
  .auth-mode{justify-self:stretch;padding:0}
  .auth-mode button{padding:var(--s-5);font-size:var(--fs-13)}
  .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)}
  /* v196. Continue with email opening the email/password block is a
     height/opacity reveal now (`.open`, venue.js), not the `hidden`
     attribute's jump — the same grid-rows collapse #add-options and
     #age-guardian already use, `.inner` the one grid child it needs. Scoped
     to the id: renderForgotPasswordStep/renderSetNewPasswordStep put
     `.auth-email` on the whole card instead (no #signin-email-block there),
     so they are untouched. */
  #signin-email-block{display:grid;grid-template-rows:0fr;opacity:0;overflow:hidden;transition:grid-template-rows var(--t-panel) var(--ease-move),opacity var(--t-base) var(--ease)}
  #signin-email-block.open{grid-template-rows:1fr;opacity:1}
  /* Block layout, on purpose — the fields inside already space themselves
     with their own margin-top (.auth-email label.f, .auth-primary, …
     above/below); .inner only needs to be the one grid item the collapse
     technique measures, not a second layout system stacked on top. */
  #signin-email-block .inner{min-height:0}
  @media (prefers-reduced-motion: reduce){#signin-email-block{transition:none}}
  /* v173 (Munk, live screenshots): `.gate-right p` (specificity 0,1,1) was
     quietly beating `.desk-claim` (0,1,0), so the Code hint and the "Check
     your inbox…" status line rendered at --fs-16, centred — whatever the
     fine-print class actually says. Scoped to `.auth-email` (0,2,0, so it
     wins) rather than touching `.desk-claim` everywhere else it is used. */
  .auth-email .desk-claim{text-align:left;font:var(--fw-regular) var(--fs-12)/var(--lh-1-6) var(--ui);color:var(--ink-4);margin-top:var(--s-6)}
  #email-code-box .desk-claim{margin-top:var(--s-4)}
  /* 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)}
  /* v190. The two ways out of the email form sit on one line, split by a thin bar, not a dot. */
  .auth-alt .btn + .btn::before{left:calc(var(--s-8) / -2 - 0.5px);width:1px;height:var(--s-6);border-radius:0;transform:translateY(-50%)}
  .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)}
    #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
   image placement map, and the Author Rooms overview grid. Reuses existing
   tokens/classes (.glasspill, .chip, .spawn .map, .bgtile) rather than a
   second visual language. v175: the onboarding checklist this comment used
   to also cover (.onboard-steps/.ostep) is gone outright — see the
   Room-pane comment on adminRoomPaneHTML/authorIntroHTML, venue.js. */
#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)}
#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)}

/* v174. Munk's live-review pass on Venue › Access itself: "remove the
   design of outlines inside outlines; use more of a toggle layout, since
   this is information dense." Same flat, divider-separated disclosure shape
   .wpane-sec (below) already proved out for Venue › Words — no rule here
   draws a background or a ring on an open section or its body; only the row
   divider and the chevron's turn say anything opened. */
.acc-sec{border-top:1px solid var(--line-2);padding:var(--s-6) 0}
.acc-sec:first-of-type{border-top:0}
.acc-sec-head{display:flex;align-items:center;gap:var(--s-6);width:100%;text-align:left;cursor:pointer;background:none;border:0;font:inherit;color:inherit;padding:var(--s-5) var(--s-4);border-radius:var(--r-lg);transition:background var(--t-base) var(--ease)}
.acc-sec-head:hover{background:var(--hover)}
.acc-sec-head:focus-visible{outline:2px solid var(--accent);outline-offset:2px}
.acc-sec-title{flex:1;min-width:0;display:grid;gap:var(--s-2)}
.acc-sec-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-sec-title p{margin:0;font:var(--fw-regular) var(--fs-13)/var(--lh-1-45) var(--ui);color:var(--fg-3)}
.acc-sec-state{flex:none;display:flex;align-items:center;gap:var(--s-5)}
.acc-sec.open .acc-chev{transform:rotate(180deg)}
.acc-sec-body{display:grid;gap:var(--s-6);margin-top:var(--s-6);padding:0 var(--s-4)}
/* The body's own forms — a plain grid of fields, no second boxed surface. */
.acc-flat{display:grid;gap:var(--s-7)}
@media (max-width:560px){
  .acc-sec-head{flex-wrap:wrap}
  .acc-sec-title{flex-basis:100%}
}

/* v201. Munk: "since I'm the admin, I want to be able to see all codes
   clearly, because I will forget them... a code should look more like a
   single unified bar." One shape for every code on Access — a room code
   (no label, Copy + Delete) and a role code (a coloured dot + label, Copy +
   Change, or an edit state with Save/Cancel) — divided by hairlines inside
   one rounded bar, ~52px tall. Tokens only (no theme block of its own):
   --surface/--line-2/--fg-3/--hi/--accent/--hover already flip with
   :root[data-theme=light] the same way .acc-sec/.acc-pill do, above. */
.acc-codebars{display:grid;gap:var(--s-4)}
.acc-codebar{display:flex;flex-wrap:wrap;align-items:stretch;min-height:52px;border:1px solid var(--line-2);border-radius:var(--r-xl);background:var(--surface);overflow:hidden}
.acc-codebar-label{display:flex;align-items:center;gap:var(--s-4);padding:0 var(--s-6);border-right:1px solid var(--line-2);flex:none;font:var(--fw-medium) var(--fs-13) var(--ui);color:var(--fg-2);white-space:nowrap}
.acc-codebar-dot{width:8px;height:8px;border-radius:50%;flex:none}
.acc-codebar-code{display:flex;align-items:center;padding:0 var(--s-6);border-right:1px solid var(--line-2);font:var(--fw-semibold) var(--fs-17) var(--mono);letter-spacing:var(--ls-006em);color:var(--hi);flex:none;max-width:44vw;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
/* v201 audit. Codes of different lengths no longer push each bar's description to a different start: room for ten characters (a typical code) before it grows. */
.acc-codebar-code{min-width:8.5rem;box-sizing:border-box}
.acc-codebar-label{min-width:7.5rem;box-sizing:border-box}
.acc-codebar-code-empty{font:var(--fw-regular) var(--fs-13) var(--ui);color:var(--fg-3);letter-spacing:0}
/* The edit state: the code segment becomes an accent-bordered input, still
   inside the same bar and the same hairline divider. */
.acc-codebar-code-edit{display:flex;align-items:center;gap:var(--s-4);border:1.5px solid var(--accent);border-radius:var(--r-md);padding:0 var(--s-5);margin:var(--s-3) var(--s-3) var(--s-3) 0}
.acc-codebar-input{width:12ch;min-width:0;background:transparent;border:0;outline:0;font:var(--fw-semibold) var(--fs-16) var(--mono);letter-spacing:var(--ls-006em);color:var(--hi)}
.acc-codebar-clear-link{flex:none;background:none;border:0;padding:0;cursor:pointer;font:var(--fw-medium) var(--fs-12) var(--ui);color:var(--fg-3);text-decoration:underline}
.acc-codebar-clear-link:hover{color:var(--hi)}
.acc-codebar-meta{order:3;flex:1 1 60px;min-width:0;display:flex;align-items:center;padding:0 var(--s-6);font:var(--fw-regular) var(--fs-13) var(--ui);color:var(--fg-3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.acc-codebar-actions{order:4;display:flex;flex:none;margin-left:auto}
.acc-codebar-btn{flex:none;width:44px;min-width:44px;min-height:44px;display:flex;align-items:center;justify-content:center;border:0;border-left:1px solid var(--line-2);background:none;color:var(--fg-3);cursor:pointer;transition:background var(--t-base) var(--ease),color var(--t-base) var(--ease)}
.acc-codebar-btn:hover{background:var(--hover);color:var(--hi)}
.acc-codebar-btn.is-copied{color:var(--live-active)}
.acc-codebar-btn-text{width:auto;padding:0 var(--s-8);font:var(--fw-semibold) var(--fs-13) var(--ui)}
.acc-codebar-dashed{width:100%;border-style:dashed;background:none;color:var(--fg-3);cursor:pointer;justify-content:center;align-items:center;gap:var(--s-3);font:var(--fw-medium) var(--fs-13) var(--ui)}
.acc-codebar-dashed:hover{background:var(--hover);color:var(--hi)}
.acc-codebar-dashed .i{width:16px;height:16px}
/* v201. At 390px: label/code/actions hold their row (actions stay a real
   44px target), the meta line wraps to its own line underneath rather than
   forcing a horizontal scroll — Munk's own allowance ("the meta line may
   wrap under the code or truncate with ellipsis"). */
/* v201 audit (390px live check): the wrapped flex row left the buttons alone
   on a middle line. On a phone the bar is a small grid instead: what it is
   and its buttons on top, the code itself full-width and large, then the
   description — a room code (no label) puts its code beside the buttons. */
@media (max-width:480px){
  .acc-codebar:not(.acc-codebar-dashed){display:grid;grid-template-columns:minmax(0,1fr) auto;grid-template-areas:"code actions" "meta meta"}
  .acc-codebar:has(> .acc-codebar-label){grid-template-areas:"label actions" "code code" "meta meta"}
  .acc-codebar-label{grid-area:label;min-width:0;border-right:0;min-height:44px}
  .acc-codebar-code{grid-area:code;min-width:0;max-width:none;border-right:0;min-height:44px}
  .acc-codebar:has(> .acc-codebar-label) .acc-codebar-code{border-top:1px solid var(--line-2)}
  .acc-codebar-code-edit{margin:var(--s-3)}
  .acc-codebar-actions{grid-area:actions;margin-left:0}
  .acc-codebar-meta{grid-area:meta;white-space:normal;border-top:1px solid var(--line-2);padding:var(--s-4) var(--s-6)}
}

/* 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}}
