  /* Tokens = the legendhaven.com Onepage kit (read off the live site 2026-09-07): charcoal #202125,
     accent #E84548, 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. */
  :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);
  /* 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);
  --shade-55: rgba(0,0,0,.55);
  --shade-60: rgba(0,0,0,.6);
  --shade-70: rgba(0,0,0,.7);
    --ink:#202125; --panel:#17181B; --glass:rgba(255,255,255,.08); --glass-solid:#2A2B30; --ring:inset 0 0 0 1px rgba(255,255,255,.18);
    --fg:#FFFFFF; --fg-2:rgba(255,255,255,.82); --fg-3:rgba(255,255,255,.56); --fg-4:rgba(255,255,255,.56);
    --group:rgba(255,255,255,.05); --hover:rgba(255,255,255,.08);
    --host-gold:#9B885B; --knight-green:#2D6A4F; --author-plum:#6D4C9F;   /* v57.2: --mod-blue retired with the rank */
    --accent:#E84548; --coral:var(--accent); --coral-soft:rgba(232,69,72,.2); --coral-icon:#FF9A9C; --coral-dot:var(--accent); --leave:rgba(232,69,72,.9);
    --violet:var(--accent); --plum:#4A4B52; --plum-2:#5A5B63; --pin:var(--accent);
    --paper:#26272C; --paper-2:#2E2F35; --paper-3:#3B3C43; --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:rgba(255,255,255,.06);
    --focus:#FFFFFF; --focus-ring:rgba(255,255,255,.7); --halo:rgba(255,255,255,.28); --scroll:rgba(255,255,255,.22);
    --glass-pill:rgba(22,23,27,.74); --coral-text:var(--accent); --coral-solid:#C9302F;
    color-scheme:dark;
  }
  /* v35. Light: warm paper under white panels, charcoal text, the same coral.
     Only the chrome changes — the floor is a painting and keeps its own light. */
  :root[data-theme=light]{
    --ink:#202125; --bg:#ECEAE4; --panel:#FFFFFF; --glass:rgba(255,255,255,.66); --glass-solid:#FFFFFF; --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-2:rgba(32,33,37,.84); --fg-3:rgba(32,33,37,.72); --fg-4:rgba(32,33,37,.72);
    --group:rgba(32,33,37,.04); --hover:rgba(32,33,37,.07);
    --paper:#FFFFFF; --paper-2:#F4F3EF; --paper-3:#E9E7E1; --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:rgba(32,33,37,.05);
    --focus:#202125; --focus-ring:rgba(32,33,37,.7); --halo:rgba(32,33,37,.22); --scroll:rgba(32,33,37,.25);
    --glass-pill:rgba(255,255,255,.8);
    /* Coral as TEXT on paper is 3.9:1; this is the same hue, dark enough to read. */
    --coral-text:#B4262A; --coral-icon:#B4262A;
    color-scheme:light;
  }
  :root[data-theme=light] #gate .card{background:rgba(255,255,255,.9)}
  :root[data-theme=light] #stage{background:#2A2B30}
  /* 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)}
  /* The off-mic pill: coral text on a coral wash, dark enough to read on paper. */
  :root[data-theme=light] #dock #d-mic.off{background:rgba(232,69,72,.12);color:#A3161B;box-shadow:inset 0 0 0 1px rgba(232,69,72,.35)}
  :root[data-theme=light] #dock #d-mic.off:hover{background:rgba(232,69,72,.2);color:#7E0F13}
  ::selection{background:var(--accent);color:#fff}
  :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:500 16px/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:50;background:linear-gradient(180deg,rgba(32,33,37,.55),rgba(32,33,37,.82)),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) 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. Centred while it fits, scrollable from the top when it does not. A
     centred grid whose content is taller than the box overflows equally in both
     directions, and the half above the top edge cannot be scrolled back to — on
     a short window that took the face, the heading, or the Join button off the
     screen with no way to reach them. Auto margins on the card centre it the
     same way and give the overflow back to the scroller. */
  #gate .gmid{display:flex;flex-direction:column;align-items:center;padding:var(--s-10);overflow:auto;min-height:0;overscroll-behavior:contain}
  #gate .gmid > *{margin-block:auto;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. */
  #gate .card{width:min(400px,94vw);background:rgba(32,33,37,.86);-webkit-backdrop-filter:blur(18px);backdrop-filter:blur(18px);-webkit-backdrop-filter:blur(18px);box-shadow:var(--ring),0 24px 60px -20px var(--shade-60);border-radius:var(--r);padding:44px 40px;display:grid;grid-template-columns:1fr;gap:26px;color:var(--ink-3);min-width:0}
  #gate .gate-left,#gate .gate-right{min-width:0}
  /* Locked = the password has not been accepted, and the faces go with it.
     The gate's top bar carries one thing at every stage: "Return to website", a way out.
     Menu and the avatar chip were decoration imitating the app's own chrome —
     they read as controls that do nothing until a room is open, so they are the
     app's, not the gate's. */
  #gate.locked .gate-left{display:none}
  #gate .gate-left{display:grid;gap:var(--s-8);justify-items:center}
  /* The face is the picker: pressing it opens the gallery. The little grid
     under it is a third way to do the same thing and shows nowhere now. */
  /* The whole welcome modal is the one surface; the mode note is chatter. */
  #gate #mode-note{display:none}
  /* avatar library: the 39 Story Quest characters, by URL off the Onepage CDN.
     --src is the image; the identity travelling between peers is still the index. */
  .av{--src:none;background-image:var(--src);background-size:cover;background-position:center top;background-repeat:no-repeat;background-color:var(--plum);border-radius:50%;display:inline-block}
  .gate-left{display:grid;gap:var(--s-7);align-content:start}
  .avhead{display:grid;gap:var(--s-6);justify-items:center;text-align:center}
  .avbig{width:112px;height:112px;flex:none;box-shadow:0 0 0 3px #fff,0 0 0 4px rgba(0,0,0,.4);cursor:pointer;transition:transform var(--t-base) var(--ease-press),box-shadow var(--t-base)}
  .avbig:hover{transform:scale(1.04);box-shadow:0 0 0 3px var(--hi),0 0 0 7px var(--halo)}
  .avbig:active{transform:scale(.98)}
  .avttl{font:600 15px var(--ui);color:var(--hi)} .avsub{font:400 13px/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%}
  .gate-right{display:grid;gap:var(--s-8);align-content:start;padding-top:var(--s-8)}
  /* 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:26px;min-width:0}
  #me-panel.as-edit{gap:var(--s-10)}
  #me-panel.as-edit .gate-right{padding-top:0}
  #me-panel.as-edit #gate-title,#me-panel.as-edit #auth-step,#me-panel.as-edit #enter,
  #me-panel.as-edit #who,#me-panel.as-edit #access-msg,#me-panel.as-edit #mode-note,
  #me-panel.as-edit #join-note{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}
  .pblock-h{margin:0;font:600 11px var(--ui);letter-spacing:.12em;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:600;font-variant-numeric:tabular-nums;color:var(--hi)}
  .f.amb{display:grid;gap:var(--s-4)}
  .devrows input[type=range]{accent-color:var(--accent)}
  #me-panel.as-edit .devrows{display:grid}
  .gate-right h1{font:400 26px/1.25 var(--brand);letter-spacing:.6px;margin:0 0 var(--s-2);color:var(--hi);text-wrap:balance}
  .gate-right p{margin:0;font:400 16px/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:500 13px var(--display);color:var(--ink-4)}
  input[type=text],input[type=email],input[type=password],textarea,select{font:400 16px var(--display);color:var(--hi);background:var(--field);border:0;box-shadow:var(--ring);margin:0;height:auto;line-height: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:1.4}
  input:focus,textarea:focus,select:focus{box-shadow:inset 0 0 0 1px var(--focus-ring);outline:0}
  .btn{font:600 15px var(--display);border-radius:var(--pill);height:48px;padding:0 24px;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:#fff}
  /* ---- the face gallery: press the avatar, pick from all of them ---- */
  #av-gal{position:fixed;inset:0;z-index:80;display:none;place-items:center;padding:24px;background:rgba(8,9,11,.72);-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(--ring),0 30px 70px var(--shade-60);padding:26px 28px 22px;display:grid;gap:var(--s-8);min-width:0}
  #av-gal h3{font:400 24px/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:500 14px/1 var(--ui);padding:var(--s-4) var(--s-8);border-radius:999px;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. The desk. 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. */
  .desk-tabs{display:flex;gap:var(--s-4);margin-bottom:var(--s-6)}
  .desk-tabs button{font:500 14px/1 var(--ui);padding:var(--s-4) var(--s-8);border-radius:999px;color:var(--fg-2);background:transparent;box-shadow:var(--ring);cursor:pointer}
  .desk-tabs button[aria-selected=true]{color:var(--paper);background:var(--hi);box-shadow:none}
  .desk-row{display:grid;gap:var(--s-4);padding:var(--s-8) 0;border-top:1px solid var(--paper-3)}
  .desk-row:first-of-type{border-top:0}
  .desk-who{font:600 15px var(--ui);color:var(--hi)}
  .desk-when{font:400 12px var(--ui);color:var(--ink-4)}
  .desk-chips{display:flex;flex-wrap:wrap;gap:var(--s-3)}
  .desk-chips span{font:500 11px/1 var(--ui);letter-spacing:.04em;text-transform:uppercase;padding:5px 9px;border-radius:999px;background:var(--paper-3);color:var(--ink-2)}
  .desk-note{margin:0;font:400 14px/1.5 var(--ui);color:var(--ink-2)}
  .desk-claim{margin:0;font:400 12px/1.5 var(--ui);color:var(--ink-4)}
  .desk-acts{display:flex;flex-wrap:wrap;gap:var(--s-4)}
  .desk-faces{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:var(--s-8)}
  .desk-face{display:grid;gap:var(--s-3);justify-items:center;text-align:center}
  .desk-face .av{width:72px;height:72px;border-radius:50%;background:var(--paper-3) center/cover no-repeat;background-image:var(--src)}
  .desk-face small{font:400 11px/1.3 var(--ui);color:var(--ink-4);word-break:break-all}
  #av-crop{position:fixed;inset:0;z-index:90;display:none;place-items:center;padding:24px;background:rgba(8,9,11,.8);-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(--ring),0 30px 70px var(--shade-60);padding:26px 28px 22px;display:grid;gap:var(--s-8);justify-items:center}
  #av-crop h3{font:400 24px/1.2 var(--brand);margin:0;color:var(--hi);text-align:center}
  #av-crop .crop-stage{position:relative;width:320px;height:320px;max-width:100%;border-radius:var(--r);overflow:hidden;background:var(--paper-3);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:0 0 0 9999px rgba(8,9,11,.55);outline:2px solid rgba(255,255,255,.85);outline-offset:-2px}
  #av-crop .crop-zoom{display:grid;gap:var(--s-3);width:100%;font:500 13px var(--display);color:var(--ink-4)}
  #av-crop .crop-zoom input{width:100%}
  #av-crop .fine{margin:0;text-align:center;font:400 12px/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:500 12px/1 var(--ui);letter-spacing:.12em;text-transform:uppercase;color:var(--fg-2)}
  #av-gal .gal-head:first-child{margin-top:0}
  #av-gal .gal-grid button{display:block;width:96px;height:96px;border-radius:50%;overflow:hidden;position:relative;box-shadow:var(--ring)}
  #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:0 0 0 3px var(--paper),0 0 0 5px var(--hi)}
  #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:500 14px 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 1.15s 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 .06s linear}
  #mic-state{display:flex;align-items:center;gap:var(--s-3);font:600 12px var(--ui);color:var(--ink-4);white-space:nowrap}
  #mic-state.ok{color:#6FCF97} #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:14px}
  .devs .btn.on{background:#fff;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:400 14px/1.45 var(--display);color:var(--ink-4);border-top:1px solid rgba(255,255,255,.12);padding-top:var(--s-7);text-align:center}
  .fine b{color:#fff;font-weight:600} .fine code{font-size:12px;background:var(--on-dark-10);padding:1px 5px;border-radius:var(--r-sm)}
  #gate .gbot{display:flex;align-items:center;padding:0 24px;background:rgba(23,24,27,.7);font:400 18px var(--brand);letter-spacing:.6px}
  #gate .gbot .spacer{flex:1}
  #gate .gbot small{font:600 12px var(--ui);color:var(--fg-3);margin-left:var(--s-10)}

  /* ================= 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:6;display:grid;place-items:center;background:rgba(16,17,20,.82);
    -webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);transition:opacity var(--t-travel) 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:400 24px/1.2 var(--brand);letter-spacing:.2px;color:#fff}
  #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) 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 1.15s var(--ease-loop) infinite}
  @keyframes lh-slide{0%{left:-40%}100%{left:100%}}
  #loading .pct{font:600 12px var(--ui);letter-spacing:.09em;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 1.6s var(--ease-slam) both;z-index: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:4;pointer-events:none;width:9px;height:9px;margin:-4px 0 0 -4px;border-radius:50%;
    background:radial-gradient(circle,#fff7d0 0%,#f1c65a 45%,rgba(241,198,90,0) 72%);filter:drop-shadow(0 0 4px rgba(255,214,110,.9));
    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:3;pointer-events:none;width:120px;height:120px;margin:-60px 0 0 -60px;border-radius:50%;
    box-shadow:0 0 0 2px rgba(255,222,130,.85),0 0 30px rgba(255,200,90,.5);animation:lh-impact .7s 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:.5s}.impact{animation:none;display:none}}
  @media (prefers-reduced-motion: reduce){#worldwrap.arriving{animation:lh-fade var(--t-ease) 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 .44s var(--ease-arrive) backwards}
  #app.booting #topleft{animation-delay:.10s}
  #app.booting #adminrail:not(.hidden){animation-delay:.14s}
  #app.booting #viewdock{animation-delay:.18s}
  #app.booting #floorbar{animation-delay:.22s}
  @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 .42s 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 .4s 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 .42s 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) linear backwards;animation-delay:.04s}
    .grp.enter,.rm.enter{animation:lh-appear var(--t-panel) 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:2;pointer-events:none}
  #world img.bg{display:block;pointer-events:none;-webkit-user-drag:none}
  #world .bg-dim{position:absolute;left:0;top:0;pointer-events:none;background:#101114;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:blur(12px);backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px);border-radius:var(--r);color:var(--fg-2);font:600 14px 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:500 16px var(--ui);text-transform:none;letter-spacing:0;color:var(--fg-2)}
  .chip .pill{font:600 16px var(--display);text-transform:none;letter-spacing:0;background:var(--paper-2);color:rgba(27,27,32,.9);height:28px;padding:0 var(--s-4);border-radius:24px;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:400 16px 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:#fff;display:grid;place-items:center;font:600 13px var(--ui);overflow:hidden;text-transform:none}
  .avatar img{width:100%;height:100%;object-fit:cover}
  #mobile-side{display:none;position:absolute;right:12px;top:12px;z-index:8;width:46px;height:46px;place-items:center;border-radius:var(--r);color:var(--hi);background:var(--glass);box-shadow:var(--ring);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}
  .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:20px;top:50%;transform:translateY(-50%);z-index:8;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{background:var(--accent);color:#fff}
  #adminrail button.hidden{display:none}
  #adminrail .i{width:19px;height:19px}
  /* The label appears beside the icon, so the rail never has to explain itself twice. */
  #adminrail button::after{content:attr(data-label);position:absolute;left:calc(100% + 10px);top:50%;transform:translateY(-50%) translateX(-4px);
    padding:7px var(--s-6);border-radius:var(--r-lg);background:var(--glass-solid);box-shadow:var(--ring),0 10px 26px -10px var(--shade-70);
    font:600 12px var(--ui);color:var(--hi);white-space:nowrap;opacity:0;pointer-events:none;transition:opacity var(--t-base),transform var(--t-base)}
  #adminrail button:hover::after,#adminrail button:focus-visible::after{opacity:1;transform:translateY(-50%) translateX(0)}
  /* v38. The flyout sits where the label would; with it open the label goes. */
  #adminrail button.flyopen::after{opacity:0!important}
  @media (prefers-reduced-motion: reduce){#adminrail button::after{transition: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:blur(20px) saturate(1.15);backdrop-filter:blur(20px) saturate(1.15);-webkit-backdrop-filter:blur(20px) saturate(1.15);
    box-shadow:inset 0 0 0 1px rgba(255,255,255,.13),0 2px 6px rgba(0,0,0,.3),0 20px 46px -16px rgba(0,0,0,.72)}
  /* 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:20px;top:20px;z-index:9;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:5px;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:600 13px var(--ui);box-shadow:0 0 0 1px rgba(255,255,255,.22)}
  #youchip #b-me .avatar .av{width:100%;height:100%}
  #roomchip{position:relative;z-index:7;display:flex;align-items:center;
    padding:11px var(--s-10);border-radius:var(--pill);max-width:min(40cqi,460px)}
  #roomchip .rc{flex:none;margin-left:var(--s-5);padding:3px 9px;border-radius:999px;font:600 12px/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:96px;transform:translateX(-50%);z-index:7;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(--ring),0 12px 30px -10px var(--shade-60);font:500 14px/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:28;pointer-events:none;font:400 14px/1.5 var(--ui)}
  #orient .ring{position:absolute;left:50%;top:50%;width:0;height:0;border-radius:var(--r-4xl);
    box-shadow:0 0 0 3px var(--focus-ring),0 0 0 9999px rgba(8,9,12,.66);
    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:0 0 0 9999px rgba(8,9,12,.66);left:-40px;top:-40px;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:16px;background:var(--glass-solid);color:var(--hi);
    box-shadow:var(--ring),0 26px 60px -20px rgba(0,0,0,.8);
    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:600 11px/1 var(--ui);letter-spacing:.09em;text-transform:uppercase;color:var(--fg-3)}
  #orient .o-dots{display:flex;gap:5px;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:400 21px/1.2 var(--brand);letter-spacing:.2px;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:9px 11px;border-radius:var(--r-xl);
    background:var(--field);font:600 12.5px/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)}
  #orient .o-foot button{height:36px;padding:0 15px;border-radius:var(--r-xl);font:600 13px var(--ui);color:var(--fg-2);background:var(--field)}
  #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}
  #orient .o-foot .o-skip{margin-left:auto;font:500 12.5px var(--ui);color:var(--fg-3);background: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}
  @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:12px!important;right:12px;top:auto!important;bottom:12px;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)}
  #pop .pop-avail button{height:34px;border-radius:var(--r-xl);font:600 12px var(--ui);color:var(--fg-2);background:var(--field)}
  #pop .pop-avail button.on{background:var(--solid);color:var(--on-solid)}
  #roomchip .rn{font:400 21px/1.1 var(--brand);letter-spacing:.2px;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:8;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) 24px;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:8;
    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:#fff}
  /* 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 22px 0 17px;border-radius:var(--pill);gap:11px;
    display:flex;align-items:center;background:var(--solid);color:var(--on-solid);font:600 15px 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)}
  #dock #d-mic.off{background:var(--coral-soft);color:#FFD9DA;box-shadow:inset 0 0 0 1px rgba(232,69,72,.5)}
  #dock #d-mic.off:hover{background:rgba(232,69,72,.3);color:#fff}
  /* 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 .09s 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:8;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:#fff}
  #viewdock .i{width:17px;height:17px}
  #viewdock #z-pct{width:auto;padding:0 var(--s-4);border-radius:var(--pill);font:600 12px var(--ui);color:var(--fg-3);font-variant-numeric:tabular-nums}
  #viewdock .vsep{flex:none;width:1px;height:20px;margin:0 5px;background:var(--line-2)}
  .hcount{position:absolute;right:0;top:0;min-width:16px;height:16px;padding:0 3px;border-radius:var(--r-lg);background:var(--accent);color:#fff;font:700 10px 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:9;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:500 13px var(--ui);color:#fff;background:var(--glass);box-shadow:var(--ring);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px)}
  #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} }
  /* 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:5px var(--s-6);border-radius:var(--pill);font:600 12px var(--ui);color:var(--ink);background:#fff}
  .menu button{position:relative}

  /* toast, timer, shout */
  #conn{position:absolute;left:50%;top:14px;transform:translateX(-50%);z-index:8;font:500 13px var(--ui);color:var(--hi);background:var(--glass-solid);box-shadow:var(--ring);padding:7px var(--s-7);border-radius:var(--r);max-width:70%;text-align:center}
  #conn.removed{background:var(--coral,#e5533d)}
  #savestate{position:absolute;right:14px;top:14px;z-index:8;font:500 12px var(--ui);color:var(--hi);background:var(--glass-solid);box-shadow:var(--ring);padding:5px var(--s-5);border-radius:var(--r);opacity:.9}
  #savestate.failed{background:var(--coral,#e5533d)}
  /* 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:absolute;right:20px;top:20px;z-index:12;font:600 14px/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:#202125;background:#FFFFFF;
    box-shadow:0 0 0 1px rgba(32,33,37,.2),0 2px 6px rgba(0,0,0,.3),0 18px 40px -12px rgba(0,0,0,.75);
    /* 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:13px 20px;border-radius:23px;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. */
    max-width:min(calc(100% - 40px),46ch);text-align:left;
    transform:translateY(-8px);transition:opacity var(--t-panel),transform var(--t-panel) var(--ease-press)}
  #toast.on{opacity:1;transform:none}
  /* 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(min-width:901px){#savestate{right:20px;top:80px}}
  @media (prefers-reduced-motion:reduce){#toast{transform:none;transition:opacity var(--t-panel)}}
  #shout{position:absolute;left:50%;top:64px;transform:translateX(-50%);z-index:6;display:flex;align-items:center;gap:var(--s-5);background:var(--accent);color:#fff;padding:var(--s-5) var(--s-9);border-radius:var(--pill);font:700 19px var(--ui)}
  /* 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:6;flex:0 1 auto;min-width:0;max-width:330px;
    background:var(--glass-solid);box-shadow:var(--ring);border-radius:var(--r);
    padding:9px 30px 9px 13px;font:400 12.5px/1.4 var(--ui);color:var(--fg-2)}
  #tip button{position:absolute;right:7px;top:7px;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:4;pointer-events:none;font-size:34px;line-height:1;
    filter:drop-shadow(0 2px 6px var(--shade-55));animation:lh-react 1.9s 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 1.05s 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 1.4s 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% + 10px);left:50%;transform:translateX(-50%);z-index:9;display:flex;gap:var(--s-1);padding:7px;border-radius:var(--pill)}
  #reacts.hidden{display:none}
  #reacts button{width:44px;height:44px;border-radius:50%;font-size:23px;line-height: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: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:-1;
    background:var(--on-dark-10);
    box-shadow:inset 0 0 0 2px rgba(255,255,255,.5),inset 0 0 0 3px rgba(0,0,0,.22),0 0 0 1px rgba(0,0,0,.22);
    opacity:0;transition:opacity var(--t-ease) 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:600 40px var(--ui);color:#fff;box-shadow:0 0 0 6px rgba(255,255,255,.9)}
  .bubble .disc video{width:100%;height:100%;object-fit:cover}
  .bubble.me .disc video{transform:scaleX(-1)}
  .bubble.speaking .disc{box-shadow:0 0 0 4px #fff,0 0 0 8px rgba(232,69,72,.75);animation:lh-pulse 1.15s ease-out infinite}
  @keyframes lh-pulse{0%{box-shadow:0 0 0 4px #fff,0 0 0 6px rgba(232,69,72,.85),0 0 0 6px rgba(232,69,72,.5)}70%{box-shadow:0 0 0 4px #fff,0 0 0 8px rgba(232,69,72,.55),0 0 0 22px rgba(232,69,72,0)}100%{box-shadow:0 0 0 4px #fff,0 0 0 8px rgba(232,69,72,.55),0 0 0 22px rgba(232,69,72,0)}}
  @media (prefers-reduced-motion: reduce){.bubble.speaking .disc{animation:none}}
  .bubble.staff{--d:104px}
  .bubble .ring{position:absolute;inset:-14px;border-radius:50%;border:3px solid rgba(255,255,255,var(--vol,0));pointer-events:none;transition:border-color var(--t-ease)}
  .bubble .badges{position:absolute;left:50%;top:100%;transform:translate(-50%,-7px);display:flex;gap:var(--s-3)}
  .bubble .badges span{width:28px;height:28px;border-radius:50%;background:var(--ink);display:grid;place-items:center;color:var(--coral-icon);
    box-shadow:0 0 0 2px rgba(0,0,0,.35),0 2px 6px rgba(0,0,0,.4)}
  .bubble .badges span.on{color:#fff}
  .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:-4px;top:-4px;width:26px;height:26px;border-radius:50%;display:grid;place-items:center;
    color:#fff;background:var(--knight-green);box-shadow:0 0 0 2px var(--ink),0 2px 6px rgba(0,0,0,.45);z-index: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:600 11px/1 var(--ui);
    padding:5px 9px;border-radius:999px;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:600 13px var(--ui);color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.8)}
  .bubble.me{cursor:grab;z-index: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% + 10px);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:rgba(255,255,255,.96);color:#1c1a24;
    font:500 15px/1.35 var(--ui);white-space:normal;overflow-wrap:anywhere;text-align:center;box-shadow:0 4px 16px rgba(0,0,0,.35);
    pointer-events:none;z-index:4;animation:lh-say var(--t-settle) ease-out}
  .bubble .say::after{content:'';position:absolute;left:50%;bottom:-6px;width:12px;height:12px;background:inherit;transform:translateX(-50%) rotate(45deg);border-radius:var(--r-xs)}
  .bubble .say.fading{opacity:0;transition:opacity .4s}
  @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:-10px;top:-10px;display:grid;place-items:center;min-width:34px;height:34px;background:var(--violet);color:#fff;font:700 19px/1 var(--ui);padding:0 var(--s-3);border-radius:999px;border:3px solid var(--ink);box-shadow:0 2px 8px rgba(0,0,0,.45)}
  .share{position:absolute;background:#000;border:3px solid #fff;border-radius:var(--r-2xl);overflow:hidden;z-index:2;width:480px;aspect-ratio:16/9}
  .share video{width:100%;height:100%;object-fit:contain}
  .share .cap{position:absolute;left:8px;top:8px;font:600 12px var(--ui);background:var(--glass);padding:var(--s-2) var(--s-5);border-radius:999px}
  .youpin{position:absolute;width:64px;height:64px;margin:-32px 0 0 -32px;border-radius:50%;background:var(--pin);opacity:.5;display:grid;place-items:center;color:#fff;pointer-events:none;z-index:1}

  /* pins */
  .pin{position:absolute;z-index:1;border-radius:var(--r);overflow:hidden;background:var(--paper);box-shadow:var(--ring),0 16px 40px -16px var(--shade-60);color:var(--ink-2);font:400 15px/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:#22201A;display:grid;grid-template-rows:auto minmax(0,1fr);
    font:400 15px/1.55 var(--ui);box-shadow:var(--ring),0 18px 44px -18px var(--shade-70)}
  .pin.note h4{margin:0;padding:var(--s-7) var(--s-9) 0;font:700 16px/1.3 var(--ui);color:#151310}
  .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:rgba(0,0,0,.3) transparent}
  .pin.note .body::-webkit-scrollbar{width:8px}
  .pin.note .body::-webkit-scrollbar-thumb{background:rgba(0,0,0,.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 #fff;outline-offset:3px}
  .pin.link{display:flex;align-items:center;gap:var(--s-5);padding:0 var(--s-9);font-weight:600;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}
  .pin iframe{width:100%;height:100%;border:0;display:block}
  /* 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(8px * var(--inv-z,1));top:calc(8px * 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:#fff;background:var(--shade-55);box-shadow:var(--ring);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);z-index: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}

  /* 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:8px;top:8px;font:600 11px var(--ui);background:var(--glass);color:#fff;padding:var(--s-2) var(--s-4);border-radius:999px;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:8px;bottom:8px;width:34px;height:34px;display:grid;place-items:center;border-radius:50%;
    color:#fff;background:var(--shade-55);box-shadow:var(--ring);-webkit-backdrop-filter:blur(8px);backdrop-filter:blur(8px);z-index:2}
  .pin.video .vmute:hover{background:rgba(0,0,0,.75)}
  .pin.video .vmute svg{width:17px;height:17px}
  .pin.video .vmute.off{color:var(--coral-icon)}
  .pin.video .far{position:absolute;left:8px;bottom:8px;font:600 11px var(--ui);background:var(--glass);color:#fff;padding:var(--s-2) var(--s-4);border-radius:999px;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:8;width:264px;background:var(--paper);border-radius:var(--r-3xl);padding:var(--s-7);
    box-shadow:var(--ring),0 18px 44px -14px rgba(0,0,0,.62);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:11px;padding-right:26px}
  #pop .pop-face{width:44px;height:44px;border-radius:50%;overflow:hidden;background:var(--paper-3);
    display:grid;place-items:center;box-shadow:0 0 0 1px rgba(255,255,255,.16)}
  #pop .pop-face .av{width:100%;height:100%}
  #pop h3{margin:0;font:600 15px/1.25 var(--ui);color:var(--ink-3);overflow-wrap:anywhere}
  #pop .pop-role{font:500 12px var(--ui);color:var(--ink-4)}
  #pop p{margin:0;font:400 13px/1.5 var(--ui);color:var(--ink-2);white-space:pre-line;overflow-wrap:anywhere}
  #pop .pop-vol{display:grid;gap:7px;padding-top:var(--s-6);border-top:1px solid var(--line)}
  #pop .lbl{display:flex;justify-content:space-between;font:500 12px var(--ui);color:var(--ink-4)}
  #pop .lbl b{font-weight:600;color:var(--ink-2);font-variant-numeric:tabular-nums}
  #pop .track{height:28px;border-radius:var(--r-3xl);background:var(--paper-3);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:13px;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:500 12px 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:10px;top:10px;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}
  #side .brandbar{display:flex;align-items:center;gap:var(--s-4);padding:var(--s-8) var(--s-6) var(--s-6) var(--s-8);min-width:0}
  #side .brandbar #b-me,#side .brandbar #side-close{flex:none}
  #side .brandbar .chip{min-width:0;overflow:hidden}
  #side .brandbar .brand{font:400 19px var(--brand);letter-spacing:.2px;color:var(--hi)}
  #side .brandbar #room-label{flex:0 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font:500 12px 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)}
  #side .brandbar #side-close{width:34px;height:34px;place-items:center;border-radius:50%;color:var(--fg-2)}
  #side .brandbar #side-close:hover{background:var(--hover);color:var(--hi)}
  #side .brandbar .count{height:30px;padding:0 11px;gap:var(--s-3);font:600 13px var(--ui);color:var(--fg-2);background:var(--group);box-shadow:none;-webkit-backdrop-filter:none;backdrop-filter:none;border-radius:var(--pill);font-variant-numeric:tabular-nums}
  #side .brandbar .count svg{width:13px;height:13px;color:var(--fg-3)}
  /* Two tabs read as one segmented control, not two loose buttons. */
  #side .head{display:flex;align-items:center;gap:3px;margin:0 var(--s-8) var(--s-7);padding:3px;border-radius:var(--pill);background:var(--group);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:600 13px 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; coral at 13px carries only
     3.9:1 against white, so the selected tab wears the pill instead. */
  #side .head button.on{background:var(--solid);color:var(--on-solid)}
  #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:400 15px 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:22px}
  .grp{display:flex;align-items:center;gap:var(--s-4);height:22px;margin:0 0 var(--s-3);padding:0 var(--s-4);
    font:600 11px var(--ui);letter-spacing:.1em;text-transform:uppercase;color:var(--fg-3)}
  .grp svg{width:13px;height:13px;color:var(--fg-3)}
  .rm{display:flex;align-items:center;gap:11px;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)}
  .rm.on{background:var(--group);color:var(--hi)}
  .rm .chev{display:none}
  .rm .ico{flex:none;width:22px;text-align:center;font-size:16px;line-height:1;filter:saturate(.9)}
  .rm .nm{flex:1;font:600 14px 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:5px;font:600 12px var(--ui);color:var(--fg-2);font-variant-numeric:tabular-nums}
  .rm .ct i{font-style:normal;font-weight:500;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)}
  .rm .tagx{flex:none;font:600 10px var(--ui);letter-spacing:.08em;text-transform:uppercase;color:var(--fg-3);
    padding:3px 7px;border-radius:var(--pill);background:var(--group)}
  /* 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:10px;box-shadow:0 0 0 2px var(--panel)}
  .ppl .stack .avatar + .avatar{margin-left:-9px}
  .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) 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:500 13px 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:10px}
  .pp .you{color:var(--fg-3);font-weight:400} .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)}
  #addroom,#venuesetup,#h-help{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:600 14px var(--ui);text-transform:none}
  #addroom:hover,#venuesetup:hover{background:var(--hover)}
  #venuesetup{color:var(--fg-3)}
  /* chat */
  #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:44px}
  .msg .copy{position:absolute;right:0;top:0;height:24px;padding:0 var(--s-4);border-radius:var(--r-md);font:600 11px var(--ui);color:var(--fg-3);background:var(--field);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:400 14px/1.45 var(--ui);color:var(--fg-2);display:grid;gap:var(--s-1);min-width:0;overflow-wrap:anywhere} .msg b{font-weight:600;color:var(--hi)} .msg.sys{color:var(--fg-4);font-style:italic}
  #chatform{display:flex;gap:var(--s-4);padding:var(--s-4)}
  #chatform input{flex:1;background:var(--field);border:0;box-shadow:var(--ring);color:var(--hi);border-radius:var(--r);font:400 15px var(--ui);padding:0 var(--s-7);height:44px}
  #chatform button{height:44px;padding:0 var(--s-9);border-radius:var(--pill);background:var(--solid);color:var(--on-solid);font:600 14px var(--ui)}
  /* schedule */
  .sched .ev:hover{background:var(--hover)}
  .sched .ev .t{color:var(--fg-3);font-weight:600} .sched .ev .r{color:var(--fg-4);font-size:12px}
  .sched .ev a{color:#A7A7FF;text-decoration:none}

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

  /* menus + modals */
  .menu{position:absolute;z-index:9;min-width:240px;background:var(--glass-solid);border-radius:var(--r);padding:var(--s-3);display:grid;box-shadow:var(--ring),0 16px 40px -12px var(--shade-60)}
  .menu button{text-align:left;height:40px;padding:0 var(--s-6);border-radius:var(--r-md);font:500 14px 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:11px;color:var(--fg-4)}
  .menu .sep{height:1px;background:var(--line);margin:var(--s-3) var(--s-4)}
  #addmenu{left:50%;bottom:72px;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:600 12px var(--ui);letter-spacing:.6px;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:40;display:grid;place-items:center;background:rgba(23,24,27,.7);-webkit-backdrop-filter:blur(6px);backdrop-filter:blur(6px)}
  .modal .card{width:min(480px,92vw);background:var(--paper);color:var(--ink-3);border-radius:var(--r);box-shadow:var(--ring),0 24px 60px -20px var(--shade-70);padding:28px;display:grid;gap:var(--s-8)}
  .modal h2{margin:0;font:400 22px/1.25 var(--brand);letter-spacing:.6px}
  .modal p{margin:0;font:400 16px/1.5 var(--display);color:var(--ink-4)}
  .modal .row{display:flex;gap:var(--s-5);justify-content:flex-end}
  /* v60. White on --accent (#E84548) is 3.91:1 and white on the old #F25E60
     hover was 3.21 — both under AA, on the one button in every destructive
     modal whose label you most need to read. --coral-solid is the brand's own
     darker red and clears it at 5.32; the hover darkens to 6.46 rather than
     lightening, which is the only direction with room left in it. */
  .modal .btn.danger{background:var(--coral-solid);color:#fff}
  .modal .btn.danger:hover{background:#B4262A}
  .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:400 14px 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:60} #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:14px}
  #admin-add{display:flex;align-items:end;gap:var(--s-6)} #admin-add label{flex:1;min-width:0}
  #admin-results{display:grid;gap:var(--s-4)}
  .admin-entry{display:flex;align-items:center;gap:var(--s-6);padding:var(--s-6);background:var(--glass);border-radius:var(--r-lg)}
  .admin-entry .details{flex:1;min-width:0;overflow-wrap:anywhere}
  .admin-entry small{display:block;color:var(--fg-3)}
  .admin-entry select{width:auto;max-width:140px}
  #admin input[type=checkbox]{accent-color:var(--accent)}
  @media (max-width:540px){#admin-add{align-items:stretch;flex-direction:column}.admin-entry{flex-wrap:wrap}}
  @media (max-width:900px){
    :root{--side:0px} #side{display:none}
    #gate{grid-template-rows:56px 1fr 44px} #gate .gmid{padding:var(--s-6)}
    #gate .card{width:100%;grid-template-columns:1fr;gap:var(--s-10);padding:var(--s-10)}
    #gate .gtop .chip:not(.icon){display:none} #gate .gtop .spacer{display:none} #gate .gtop{justify-content:flex-end}
    .gate-right{padding-top:0} .gate-right h1{font-size:22px}
    #gate .gbot{padding:0 var(--s-8);font-size:15px} #gate .gbot small{display:none}
  }

  input[type=number],input[type=time]{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:#26272c;color:white}
  #tool-modal{z-index:70}
  /* The card is the frame and does not scroll; only the middle row does, so
     the title stays put at the top and the actions stay put at the bottom
     however long the panel gets. */
  #tool-modal .card{width:min(760px,95vw);max-height:92vh;max-height:92dvh;overflow:hidden;padding:0;gap:0}
  /* A profile is one column of short fields. 760 px is the width the room
     editors need, not this. */
  #tool-modal.narrow .card{width:min(440px,94vw)}
  #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:26px 28px var(--s-8)}
  #tool-content{display:grid;gap:var(--s-7);padding:0 28px 24px;overflow:auto;min-height:0;overscroll-behavior:contain}
  .tool-foot{display:flex;align-items:center;gap:var(--s-5);flex-wrap:wrap;padding:var(--s-8) 28px;
    background:var(--paper-2);box-shadow:inset 0 1px 0 var(--on-dark-10)}
  #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:14px}
  /* 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:14px;line-height: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. */
  #tool-error{color:#ffb4b5;font-size:14px;padding:0 28px 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,rgba(23,24,27,.62) 0,rgba(23,24,27,.28) 44%,transparent 72%)}
  #tool-modal.sheet .card{position:relative;width:min(600px,100vw);max-width:100vw;height:100%;max-height:none;
    margin-left:auto;border-radius:0;box-shadow:-1px 0 0 var(--on-dark-14),-32px 0 80px -24px rgba(0,0,0,.75);
    animation:sheet-in var(--t-panel) var(--ease-arrive)}
  @keyframes sheet-in{from{transform:translateX(24px);opacity:0}to{transform:none;opacity:1}}
  @media (prefers-reduced-motion:reduce){#tool-modal.sheet .card{animation:none}}
  #tool-modal.sheet #tool-form{max-height:none;height:100%;grid-template-rows:auto minmax(0,1fr) auto auto}
  #tool-modal.sheet .tool-head{padding:22px 24px var(--s-7)}
  /* 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}
  .editnav{display:flex;flex-direction:column;gap:var(--s-1);padding:var(--s-3) var(--s-6) var(--s-8) var(--s-9);width:172px;
    box-shadow:inset -1px 0 0 rgba(255,255,255,.09)}
  .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:9px var(--s-5);border-radius:7px;text-align:left;color:var(--fg-3);background:none;
    font:600 13px 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:-10px;top:50%;width:3px;height:18px;
    margin-top:-9px;border-radius:var(--r-xs);background:var(--accent)}
  .editnav .i{width:18px;height:18px}
  .editpane{display:none;gap:var(--s-7);padding:var(--s-7) 24px 26px var(--s-10);overflow:auto;min-height:0;overscroll-behavior:contain;align-content:start}
  .editpane[data-open]{display:grid}
  .editpane > h3{margin:0;font:600 12px var(--ui);letter-spacing:.1em;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)}
  .editpane .rule{height:1px;background:var(--on-dark-10);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:400 12.5px/1.5 var(--ui);color:var(--fg-3);text-align:left;border-top:0;padding-top:0;margin:-4px 0 0}
  /* Under 760px the sheet takes the screen and the rail lies down as a tab row:
     there is no floor left to watch beside it. */
  @media(max-width:760px){
    #tool-modal.sheet::before{background:rgba(23,24,27,.72)}
    #tool-modal.sheet .card{width:100vw}
    #tool-modal.sheet #tool-content{grid-template-columns:minmax(0,1fr);grid-template-rows:auto minmax(0,1fr)}
    .editnav{flex-direction:row;width:auto;gap:var(--s-2);padding:0 var(--s-7) var(--s-5);overflow:auto;
      box-shadow:inset 0 -1px 0 rgba(255,255,255,.09)}
    .editnav button{grid-template-columns:18px;gap:0;padding:0 var(--s-7);min-height:44px;justify-items:center;flex:none}
    .editnav button span{display:none}
    .editnav button[aria-selected="true"]::after{left:50%;top:auto;bottom:-6px;margin:0 0 0 -10px;width:20px;height:3px}
    .editpane{padding:var(--s-8) var(--s-9) 24px}
  }
  /* 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:11px 0;font:500 14px var(--ui);color:var(--fg)}
  .sw small{display:block;font:400 12px var(--ui);color:var(--fg-3);margin-top:var(--s-1)}
  .sw input{appearance:none;flex:none;width:44px;height:26px;border-radius:999px;background:var(--group);box-shadow:var(--ring);position:relative;cursor:pointer;transition:background var(--t-base)}
  .sw input::after{content:'';position:absolute;left:3px;top:3px;width:20px;height:20px;border-radius:50%;background:#fff;transition:transform var(--t-base)}
  .sw input:checked{background:var(--accent)}
  .sw input:checked::after{transform:translateX(18px)}
  .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:7px var(--s-5);padding:9px 0;
    font:500 13px 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}
  .sld input::-webkit-slider-runnable-track{height:6px;border-radius:99px;
    background:linear-gradient(90deg,var(--accent) calc(var(--p,0) * 100%),var(--on-dark-14) 0)}
  .sld input::-moz-range-track{height:6px;border-radius:99px;background:var(--on-dark-14)}
  .sld input::-moz-range-progress{height:6px;border-radius:99px;background:var(--accent)}
  .sld input::-webkit-slider-thumb{appearance:none;width:16px;height:16px;margin-top:-5px;border-radius:50%;
    background:#fff;box-shadow:0 1px 4px var(--shade-55);transition:transform var(--t-tap)}
  .sld input::-moz-range-thumb{width:16px;height:16px;border:0;border-radius:50%;background:#fff;
    box-shadow:0 1px 4px var(--shade-55)}
  .sld input:active::-webkit-slider-thumb{transform:scale(1.15)}
  .sld input:focus-visible{outline:2px solid var(--accent);outline-offset:4px;border-radius:99px}
  .seg{display:grid;gap:7px;padding:9px 0;font:500 13px var(--ui);color:var(--fg-2)}
  .seg .opts{display:flex;gap:var(--s-2);padding:3px;border-radius:var(--pill);background:var(--group)}
  .seg button{flex:1;padding:7px var(--s-5);border-radius:var(--pill);font:600 12px var(--ui);color:var(--fg-3);background:none}
  .seg button[aria-pressed="true"]{background:var(--accent);color:#fff}
  /* 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:11px 0;border-bottom:1px solid rgba(255,255,255,.07)}
  .permrow:last-child{border-bottom:0}
  .permrow .q{min-width:0;font:600 14px var(--ui);color:var(--fg-2)}
  .permrow .q small{display:block;margin-top:var(--s-1);font:400 12px var(--ui);color:var(--fg-3)}
  .permrow .opts{display:flex;gap:3px;padding:3px;border-radius:var(--pill);background:var(--group);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:#fff}
  .permrow .opts .i{width:17px;height:17px}
  .spawn{display:grid;gap:var(--s-4);padding:9px 0;font:500 13px 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:-13px 0 0 -13px;border-radius:50%;background:var(--accent);border:3px solid #fff;box-shadow:0 2px 8px rgba(0,0,0,.5);pointer-events:none}
  .bgpick{display:grid;grid-template-columns:repeat(auto-fill,minmax(104px,1fr));gap:var(--s-4);padding:var(--s-3) 0}
  .bgpick button{padding:0;border-radius:var(--r);overflow:hidden;box-shadow:var(--ring);aspect-ratio:16/9;background:var(--group)}
  .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:5px;top:5px;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:#fff;background:rgba(18,19,22,.82);box-shadow:0 0 0 1px rgba(255,255,255,.24);
    font:600 11px 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 9px}
  .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)}
  .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:600 12px var(--ui);letter-spacing:.1px}
  /* 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:5px;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:11px;letter-spacing:0}
  .editpane .sld,.editpane .sw,.editpane .seg,.editpane .spawn{padding:0}
  /* 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:500 11px 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:5px;top:5px;display:grid;place-items:center;width:24px;height:24px;
    border-radius:var(--r-md);background:rgba(23,24,27,.7);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}
  .tool-actions .btn,.setup-row .btn{height:40px;padding:0 var(--s-8);font-size:13px}
  .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:12px;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 -6px;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:0 0 0 3px var(--paper),0 0 0 6px var(--accent)}
  .profile-avatars button[aria-pressed=true]::after{content:"✓";position:absolute;right:-3px;bottom:-3px;width:22px;height:22px;border-radius:50%;
    background:var(--accent);color:#fff;font:700 13px/22px var(--ui);text-align:center;box-shadow:0 0 0 3px var(--paper)}
  .profile-picked{margin:var(--s-5) 0 0;font:500 13px 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:#101114;border-radius:var(--r);overflow:hidden;display:flex;flex-direction:column;box-shadow:var(--ring),0 16px 40px -16px var(--shade-60)}
  .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:9px var(--s-7);font:600 12px var(--ui);letter-spacing:.01em;color:#fff;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 8px 8px;padding:3px var(--s-4);border-radius:999px;font:500 11px var(--ui);color:#fff;background:rgba(16,17,20,.72);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:-16px;display:block;border-radius:var(--r-4xl);pointer-events:none;z-index:2;
    box-shadow:0 0 0 2px var(--accent),inset 0 0 0 2px var(--accent)}
  .editing-pins .pin .mv i{position:absolute;background:rgba(232,69,72,.5);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:16px;bottom:16px;left:0;width:16px}
  .editing-pins .pin .mv i:nth-child(4){top:16px;bottom:16px;right:0;width:16px}
  .editing-pins .pin.dragging .mv i{cursor:grabbing;background:rgba(232,69,72,.72)}
  .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:-16px;transform:translate(-50%,-100%);
    display:flex;align-items:center;gap:var(--s-3);height:26px;padding:0 11px;border-radius:var(--pill);
    background:var(--accent);color:#fff;font:600 11px var(--ui);letter-spacing:.04em;white-space:nowrap;
    pointer-events:none;z-index: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:0 0 0 2px rgba(255,255,255,.6),0 22px 48px -14px rgba(0,0,0,.85)}
  .editing-pins .pin.dragging{opacity:.85;cursor:grabbing}
  #arrangebar{position:absolute;left:50%;bottom:96px;transform:translateX(-50%);z-index:9;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:600 13px var(--ui)}
  #arrangebar button{height:34px;padding:0 var(--s-8);border-radius:var(--pill);background:var(--solid);color:var(--on-solid);font:600 13px var(--ui)}
  #arrangebar .plain{background:transparent;color:var(--hi);box-shadow:var(--ring)}
  /* Three icons beside the + . Clicking one puts the thing on the floor; there is
     no form between wanting a note and having one. */
  #addfly{position:absolute;z-index:10;display:flex;gap:var(--s-1);padding:5px;border-radius:var(--pill)}
  #addfly button{width:42px;height:42px;border-radius:50%;display:grid;place-items:center;color:var(--fg-2);
    transition:background var(--t-base),color var(--t-base)}
  #addfly button:hover{background:var(--coral-solid);color:#fff}
  #addfly .i{width:19px;height:19px}

  /* 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:5}
  .pin .grip::after{content:"";position:absolute;right:7px;bottom:7px;width:15px;height:15px;
    border-right:3px solid #fff;border-bottom:3px solid #fff;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:8px;right:8px;bottom:8px;display:none;height:32px;padding:0 var(--s-5);z-index:4;
    border:0;border-radius:var(--r);font:500 12px var(--ui);color:#fff;background:rgba(0,0,0,.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:30px;
    box-shadow:inset 0 0 0 1.5px #46B87A;
    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 .34s}
  @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:#101114;color:var(--fg-3);font:600 13px 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:#101114;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 .34s 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:rgba(12,13,16,.84);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 .24s}
  /* 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 40s linear infinite;font:600 13px var(--ui);letter-spacing:.14em;text-transform:uppercase;fill:#fff}
  .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,54px) * .707 - 15px);
    top:calc(50% - var(--amb-r,54px) * .707 - 15px)}
  /* 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,54px) - 16px);
    border-radius:50%;box-shadow:0 0 0 2px rgba(232,69,72,.5);
    /* 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:0 0 0 2px rgba(232,69,72,.78)}
  #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:4}
  #world.editing-pins .pin.video.ambience .mvtag{top:calc(50% - var(--amb-r,54px) - 14px)}
  /* 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:#fff;color:var(--ink);
    display:grid;place-items:center;box-shadow:0 6px 18px -6px var(--shade-70)}
  .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 26s 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:0 0 0 1px rgba(255,255,255,.6);opacity:0;
    animation:amb-ping 3.4s var(--ease-move) infinite}
  .pin.video.ambience .amb-ring:nth-child(2){animation-delay:-1.13s}
  .pin.video.ambience .amb-ring:nth-child(3){animation-delay:-2.27s}
  @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:11;width:236px;padding:var(--s-6);border-radius:var(--r-3xl);
    background:var(--glass-solid);box-shadow:var(--ring),0 18px 44px -14px rgba(0,0,0,.62);
    -webkit-backdrop-filter:blur(14px);backdrop-filter:blur(14px);display:grid;gap:var(--s-5)}
  #pinmenu .mrow{display:grid;gap:var(--s-3)}
  #pinmenu .mrow > span{font:600 11px var(--ui);letter-spacing:.08em;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:9px;display:flex;align-items:center;
    gap:9px;text-align:left;font:500 12px var(--ui);line-height: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:#fff;box-shadow:none}
  #pinmenu .opts .i{width:16px;height:16px}
  #pinmenu .mname{height:34px;padding:0 11px;border:0;border-radius:var(--r);width:100%;
    font:500 12px 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 11px;border:0;border-radius:var(--r);width:100%;
    font:500 12px 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:7px}
  #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}
  #pinmenu .drop{height:34px;border-radius:var(--pill);font:600 12px var(--ui);color:var(--coral-icon);
    background:var(--group);display:grid;place-items:center}
  #pinmenu .drop:hover{background:var(--leave);color:#fff}

  /* 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:#fff;color:var(--ink);display:flex;align-items:center;gap:var(--s-6);padding:0 var(--s-8);
    box-shadow:var(--ring),0 14px 34px -14px var(--shade-60);text-decoration:none}
  .pin.link:hover{background:#F4F4F5}
  .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);color:#fff}
  .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:400 16px/1 var(--ui)}
  .pin.link .lktxt{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;
    font:600 15px var(--ui);color:var(--ink)}
  .pin.link .host{font:400 12px var(--ui);color:rgba(32,33,37,.55);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:#F1F1F2;color:var(--ink);
    box-shadow:inset 0 0 0 1px rgba(0,0,0,.14)}
  .pin.link.blank .addr::placeholder{color:rgba(32,33,37,.5)}
  .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. */
  #reachring{position:absolute;z-index:0;border-radius:50%;pointer-events:none;
    background:radial-gradient(circle,rgba(232,69,72,.20) 0%,rgba(232,69,72,.06) 60%,rgba(232,69,72,0) 100%);
    box-shadow:inset 0 0 0 3px rgba(255,255,255,.55)}
  /* Where people land, on the floor itself rather than on a thumbnail of it. */
  #spawnmark{position:absolute;z-index:4;width:64px;height:64px;margin:-32px 0 0 -32px;border-radius:50%;
    display:none;place-items:center;cursor:grab;touch-action:none;
    background:rgba(232,69,72,.28);box-shadow:0 0 0 3px #fff,0 0 0 6px rgba(0,0,0,.35);color:#fff}
  #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:600 12px var(--ui);color:#fff;text-shadow:0 1px 3px rgba(0,0,0,.85)}
  .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 rgba(255,255,255,.07)}
  .helprow:last-child{border-bottom:0}
  .helprow b{font:600 13px var(--ui);color:var(--hi)}
  .helprow span{font:400 14px/1.5 var(--ui);color:var(--fg-2);min-width:0}
  #tool-modal h3{font:600 12px var(--ui);letter-spacing:.1em;text-transform:uppercase;color:var(--fg-3)}
  .schedule-go{color:#ffaaaa;text-decoration:underline;padding-top:var(--s-3)}
  #side-close{display:none}#room-label{font-size:13px;color:var(--fg-3)}
  #mic-meter{width:100%;height:16px;accent-color:var(--accent)}#audio-message{font-size:13px;color:var(--fg-3)}
  #pop{max-height:80vh;overflow:auto}
  @media(max-width:900px){
    #mobile-side{display:grid}#viewdock{top:68px}#side.mobile-open{display:grid;position:fixed;right:0;top:0;bottom:0;width:min(360px,94vw);z-index:25;box-shadow:-10px 0 40px #0008}
    /* 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:30}
    #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:12px}
    /* Under 900 the top-right corner is the drawer button, with the view
       cluster below it. The toast clears both rather than fighting them. */
    #toast{top:128px;right:12px;max-width:calc(100% - 24px)}
    #topleft{left:12px;top:12px;max-width:calc(100% - 76px)}
    #roomchip{padding:9px var(--s-8)}#roomchip .rn{font-size:18px}
  }

  /* ------------------------------------------------------------------ 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:12px;top:12px;right:70px;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:8px;top:auto;bottom:74px;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:12px;right:auto;top:auto;bottom:84px;transform:none;grid-auto-flow:column;gap:var(--s-1)}

    /* 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:linear-gradient(90deg,#000 calc(100% - 26px),transparent);
      mask-image:linear-gradient(90deg,#000 calc(100% - 26px),transparent)}
    #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 3px}
    /* v31. A member's Broadcast is disabled, so on a phone it is not drawn: 44 px
       of a row that has none to spare. Staff keep it. */
    #dock #d-cast:disabled{display:none}
    /* 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. */
    #toast{top:124px;right:12px;max-width:calc(100% - 24px)}
    #hint{bottom:78px}
    #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:66px}
    #arrangebar{bottom:84px;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:8px;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){
    body.side-hidden #app{grid-template-columns:1fr 0}
    body.side-hidden #side{display:none}
    /* 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. */
    #side-open{display:grid;place-items:center;position:fixed;right:14px;top:14px;z-index:26;
      width:44px;height:44px;border-radius:var(--pill);color:var(--hi);background:var(--glass);box-shadow:var(--ring);
      -webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);transition:background var(--t-base),color var(--t-base)}
    /* Room for it at the end of the sidebar's own bar. */
    #side .brandbar{padding-right:56px}
    #side-open:hover{background:var(--hover)}
    #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:rotate(180deg)}
  }
.admin-group{border:1px solid #ffffff1f;border-radius:var(--r-lg);padding:var(--s-6);margin:var(--s-6) 0}.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)}
/* Room order & groups: a room moves by its handle. The buttons beside it do the
   same thing from the keyboard, so nothing here is the only way to move a room. */
.dragdot{flex:none;cursor:grab;color:var(--fg-4);font:600 15px var(--ui);line-height:1;padding:var(--s-1) var(--s-2);border-radius:var(--r-md);user-select:none}
.dragdot:hover{color:var(--hi);background:var(--hover)}
.dragdot:active{cursor:grabbing}
.setup-row.dragging{opacity:.4}
.setup-row.dropbefore{box-shadow:inset 0 3px 0 var(--accent)}
.setup-row.dropafter{box-shadow:inset 0 -3px 0 var(--accent)}
.admin-group.dragging{opacity:.4}
.admin-group.dropbefore{box-shadow:inset 0 3px 0 var(--accent)}
.admin-group.dropafter{box-shadow:inset 0 -3px 0 var(--accent)}
.droptail{height:10px;border-radius:var(--r-md);margin-top:var(--s-1)}
.droptail.empty{height:auto;padding:var(--s-5) var(--s-6);border:1px dashed #ffffff33;border-radius:var(--r-lg);font:500 12px var(--ui);color:var(--fg-4)}
.droptail.on{background:var(--accent)}
.droptail.empty.on{background:none;border-color:var(--accent);color:var(--hi)}
