/* temple.css — components from design/system.md. Ships at Stage 8. */
*,*::before,*::after{box-sizing:border-box}
html{scroll-behavior:smooth}
body{margin:0;font-family:var(--font-sans);font-size:1.125rem;line-height:1.6;
  color:var(--color-text);background:var(--color-bg);-webkit-font-smoothing:antialiased}
h1,h2,h3{font-family:var(--font-serif);font-weight:500;line-height:1.25;margin:0 0 var(--space-2)}
h1{font-size:clamp(2.25rem,5vw + 1rem,3.75rem);line-height:1.2}
h2{font-size:clamp(1.5rem,2.5vw + .75rem,2.4rem)}
h3{font-size:clamp(1.25rem,1.5vw + .75rem,1.6rem)}
p{margin:0 0 var(--space-2)}
a{color:var(--color-accent)}
small,.small{font-size:.875rem;line-height:1.5;font-weight:500}
img,video{max-width:100%;height:auto}
:focus-visible{outline:2px solid var(--color-accent);outline-offset:2px}
main{display:block}

/* shell */
/* the etheric bar: a floating veil of cloud that melts into the scene, no edge, no line */
.shell-bar{position:fixed;top:0;left:0;right:0;z-index:40;display:flex;align-items:center;
  justify-content:space-between;gap:var(--space-2);padding:0 var(--space-2);height:60px;
  background:transparent;border:0}
/* photo clouds drifting + gently breathing (swell/shrink) behind the frosted bar */
.shell-bar::before{content:"";position:absolute;inset:0 0 -34px;z-index:-1;pointer-events:none;
  backdrop-filter:blur(14px) saturate(1.06) brightness(1.03);
  -webkit-backdrop-filter:blur(14px) saturate(1.06) brightness(1.03);
  background:url(../assets/cloud3.webp?v=1786426043) 0 60% / 64% 190% repeat-x,
    url(../assets/cloud1.webp?v=1784710734) 28% 28% / 52% 170% repeat-x,
    linear-gradient(180deg, rgb(255 253 248 / .5), rgb(255 253 248 / .28) 55%, rgb(255 253 248 / 0));
  -webkit-mask-image:linear-gradient(180deg,#000 0,#000 60%,transparent);
  mask-image:linear-gradient(180deg,#000 0,#000 60%,transparent);
  animation:cloudmorph 40s ease-in-out infinite alternate}
@keyframes cloudmorph{
  from{background-position:0 60%, 28% 28%, 0 0; background-size:64% 190%, 52% 170%, 100% 100%}
  to{  background-position:16% 54%, 18% 34%, 0 0; background-size:74% 212%, 60% 188%, 100% 100%}}
@media (prefers-reduced-motion: reduce){.shell-bar::before{animation:none}}
.shell-bar::after{content:"";position:absolute;inset:0 0 auto;height:1px;pointer-events:none;
  background:linear-gradient(90deg,transparent 4%,rgb(255 255 255 / .8),transparent 96%)}
.shell-bar.transparent{position:fixed}
/* ⚠ THE BAR GETS ITS OWN LAYER, AND IT MAY NOT BE A `transform` (2026-08-25).
   Director: "in the courses room, when I go into a learn more or register, the top
   tool bar gets displaced like half way down the screen." Only on iOS, and only on
   the pages that SCROLL: a room is one fixed 100svh scene with nothing to scroll, a
   course page is a long document, which is why the rooms never show it. The bar is
   `position:fixed` and its ::before carries `-webkit-backdrop-filter`, and that pair
   on a scrolling page is the long-standing iOS compositing fault where the fixed
   layer keeps painting at the position it was composited at instead of following the
   viewport. Giving the element its own layer up front is the cure.
   `transform:translateZ(0)` is the usual way to ask for one and it CANNOT be used
   here: `.mobile-menu` is a `position:fixed` child of this header (inset:var(--bar-h)
   0 0), and a transformed ancestor becomes the containing block for a fixed
   descendant, so the menu would size itself to the 60px bar instead of the screen.
   `backface-visibility:hidden` asks for the same layer and establishes no containing
   block, so the menu is untouched. Verified in Chromium and in WebKit that the bar's
   box is unchanged (top 0, height 60) and the menu still fills the screen; the fault
   itself is a compositor one that neither headless engine reproduces, so the proof
   is the Director's own phone. */
/* WITHDRAWN 2026-08-25 (red team). This was added earlier today to CURE the drifting
   bar by giving it its own layer. A fixed element that is NOT promoted is repositioned
   by WebKit's scrolling coordinator every frame and cannot desync; a promoted one is
   handed to the compositor with a fixed-position constraint, and that is exactly the
   class of layer that gets stranded at a stale offset when the visual viewport resizes.
   It is at least as likely to have been sustaining the fault as fixing it, and the real
   causes are now addressed above, so the promotion goes.
   .shell-bar{backface-visibility:hidden;-webkit-backface-visibility:hidden} */
/* the bar's actions: glassy gold-rimmed pills of light */
.shell-actions .btn{border-radius:999px;min-height:38px;padding:.35rem 1.15rem;font-size:.82rem;
  letter-spacing:.05em;color:#5a4a2a;border:1px solid rgb(217 182 98 / .45);
  background:linear-gradient(180deg, rgb(255 255 255 / .85), rgb(255 250 236 / .5));
  backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px);
  box-shadow:inset 0 1px 0 rgb(255 255 255 / .9), 0 2px 12px rgb(150 110 40 / .10);
  transition:box-shadow .4s var(--ease-veil),border-color .4s ease}
.shell-actions .btn:hover,.shell-actions .btn:focus-visible{border-color:#d9b662;
  box-shadow:inset 0 1px 0 rgb(255 255 255 / .95), 0 0 24px rgb(231 207 141 / .55)}
.shell-actions .btn-primary{color:#3a3040;border-color:rgb(217 182 98 / .8);
  background:linear-gradient(180deg,#f3e0b0,#dfc077)}
.wordmark{font-family:var(--font-serif);font-size:1.05rem;color:var(--color-text);text-decoration:none;
  display:flex;align-items:center;min-height:44px}
.shell-room{font-size:.875rem;letter-spacing:.14em;text-transform:uppercase;color:var(--color-text-muted)}
.shell-actions{display:flex;gap:var(--space-1);align-items:center}
.wrap{max-width:44rem;margin:0 auto;padding:0 var(--space-2)}
.wrap-wide{max-width:64rem;margin:0 auto;padding:0 var(--space-2)}

/* buttons */
.btn{display:inline-flex;align-items:center;justify-content:center;gap:.5em;min-height:44px;
  padding:var(--space-1) var(--space-3);border-radius:var(--radius-md);border:1px solid transparent;
  font-family:var(--font-sans);font-size:.9rem;font-weight:500;letter-spacing:.02em;cursor:pointer;
  text-decoration:none;transition:box-shadow var(--t-move) var(--ease-veil),transform var(--t-touch)}
.btn:active{transform:scale(.98)}
.btn{white-space:nowrap}
.btn-primary{background:linear-gradient(180deg,var(--gold),var(--color-primary));color:var(--on-primary);
  border-color:var(--color-primary)}
/* hover is a pointer's word. On a phone an unguarded :hover latches on tap and stays
   latched until you touch something else, so it is fenced behind (hover:hover); the
   press feedback these buttons already have is `.btn:active{transform:scale(.98)}`. */
@media (hover:hover){.btn-primary:hover{box-shadow:var(--shadow-lifted);filter:brightness(1.05)}}
.btn-secondary{background:transparent;border-color:var(--color-border);color:var(--color-text)}
@media (hover:hover){.btn-secondary:hover{border-color:var(--color-accent)}}
.btn-destructive{background:var(--color-error);color:#fff}

/* ceremony */
.ceremony{position:relative;min-height:100svh;overflow:hidden;
  background:linear-gradient(rgb(255 255 255 / .28), rgb(255 253 248 / .10) 40%, rgb(255 255 255 / .20)),
  url(../assets/temple-entrance.webp?v=1784710734) center bottom/cover no-repeat}
/* the living hero: seamless wind loop over the still (which stays as poster + fallback) */
.ceremony video.hero-loop{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;
  object-position:center bottom;z-index:0;pointer-events:none}
@media (prefers-reduced-motion: reduce){.ceremony video.hero-loop{display:none}}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .ceremony{background:linear-gradient(rgb(255 255 255 / .28), rgb(255 253 248 / .10) 40%, rgb(255 255 255 / .20)),
    url(../assets/temple-entrance-portrait.webp?v=1784710734) center bottom/cover no-repeat}
}
.ceremony::after{content:"";position:absolute;inset:0;pointer-events:none;z-index:1;
  background:radial-gradient(ellipse 70% 55% at 50% 44%, rgb(255 255 255 / .10), rgb(255 253 245 / .05) 45%, rgb(255 255 255 / 0) 70%)}
.ceremony > *{position:relative;z-index:2}
/* legibility without darkness: the marble's DETAIL softens behind the text
   (masked backdrop blur, fading out with no edge); the light itself stays */
.ceremony .hero-white::before{content:"";position:absolute;inset:-18% -28%;z-index:-1;pointer-events:none;
  backdrop-filter:blur(12px) brightness(1.04) saturate(1.05);
  -webkit-backdrop-filter:blur(12px) brightness(1.04) saturate(1.05);
  -webkit-mask-image:radial-gradient(ellipse 58% 54% at 50% 46%, #000 34%, transparent 74%);
  mask-image:radial-gradient(ellipse 58% 54% at 50% 46%, #000 34%, transparent 74%)}
.ceremony .hero-white .kicker{color:rgb(255 252 244 / .95);
  text-shadow:0 1px 2px rgb(110 90 55 / .32), 0 1px 10px rgb(110 90 55 / .18)}
.ceremony .hero-white h1.v1-title{color:#fff;letter-spacing:1px;text-wrap:balance;font-weight:500;
  font-size:clamp(2.4rem,4.5vw + 1rem,5rem);line-height:1.15;
  text-shadow:0 1px 2px rgb(110 90 55 / .32), 0 2px 18px rgb(110 90 55 / .16), 0 0 40px rgba(255,255,255,.35)}
.v1-enter{display:inline-flex;align-items:center;justify-content:center;font-family:var(--font-serif);
  background:rgb(255 253 246 / .82);color:#5a4a2a;padding:17px 54px;font-size:1.02rem;
  letter-spacing:.22em;text-indent:.22em;text-decoration:none;border-radius:3px;
  border:1px solid rgb(217 182 98 / .75);
  backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  box-shadow:inset 0 0 0 3px rgb(255 255 255 / .45), 0 4px 24px rgb(160 120 40 / .14),
    0 0 30px rgb(255 244 210 / .35);
  transition:all .5s var(--ease-veil)}
.v1-enter:hover,.v1-enter:focus-visible{background:linear-gradient(180deg,#efd9a4,#d9b662);
  color:#3a3040;border-color:#d9b662;
  box-shadow:0 0 46px rgb(231 207 141 / .85), inset 0 1px 0 rgb(255 255 255 / .55)}
.ceremony .hero-white .lead{color:rgb(255 252 244 / .98);
  text-shadow:0 1px 2px rgb(110 90 55 / .32), 0 1px 10px rgb(110 90 55 / .18)}
.btn-white{background:rgb(255 255 255 / .96);color:var(--color-text);border-color:rgb(255 255 255 / .96);
  letter-spacing:.14em}
.btn-white:hover{box-shadow:0 0 40px rgb(255 246 214 / .8)}
/* ponytail: --t-breath, not a number of its own. This was 420ms, less than half
   the fade every other crossing on the site uses, which is why the way out of the
   dove film felt clipped next to them (Director 2026-08-27: "the end of video fade
   needs to be slower and more in feeling and match all the other faded"). The cues
   in temple.js that ride it moved with it: handOff now fires 1.4s before the film
   ends so the gold has its full breath, and the reveal waits 1000ms for it. */
#gold-flash{position:fixed;inset:0;z-index:65;pointer-events:none;opacity:0;
  background:radial-gradient(ellipse 110% 100% at 50% 45%, #ffffff, #ffffff 46%, #fffef9 66%, #fdf8ea 82%, #f6e9cd 96%, #e2c894 120%);
  transition:opacity var(--t-breath) var(--ease-veil)}
#gold-flash.on{opacity:1}
/* arrival from the light: gold is present at first paint, holds a breath, dissolves */
html.from-light::after{content:"";position:fixed;inset:0;z-index:70;pointer-events:none;
  background:radial-gradient(ellipse 110% 100% at 50% 45%, #ffffff, #ffffff 46%, #fffef9 66%, #fdf8ea 82%, #f6e9cd 96%, #e2c894 120%);
  animation:lightdissolve 1400ms var(--ease-veil) forwards}
.porch{position:relative;text-align:center;padding:calc(56px + var(--space-2)) var(--space-2) var(--space-2);max-width:40rem}
.porch .kicker{letter-spacing:.22em;text-transform:uppercase;font-size:.875rem;color:var(--color-text-muted)}
.porch h1{margin-top:var(--space-2)}
.porch .lead{font-size:1.15rem;color:var(--color-text-muted)}
.skip-intro{position:absolute;top:calc(56px + var(--space-1));right:var(--space-2);z-index:50;
  min-height:44px;min-width:44px;display:none;align-items:center;padding:0 var(--space-2);
  background:rgb(255 255 255 / .7);border:1px solid var(--color-border);border-radius:var(--radius-sm);
  color:var(--color-text);text-decoration:none;font-size:.875rem;backdrop-filter:blur(4px)}
/* Skip was VISIBLE AND UNCLICKABLE for the whole film. `.ceremony > *{z-index:2}` is
   more specific than `.skip-intro{z-index:50}`, so both Skip and the film flattened
   to layer 2 and the film, being later in the DOM, sat on top of the button and ate
   every click. Lift Skip past .veil-video (60) with a selector that outranks it. */
.ceremony > #skip-intro{z-index:61}
/* ⚠ 160ms, NOT --t-breath (900ms). Director, 2026-08-11: "there is a moment on the main
   landing page on mobile, the dove on the transition isnt the same its not smooth, its
   wing position changes."
   Measured at the tap: the hero loop was at t=3.05 of its own 11.7s and the entry film
   starts at t=0, and BOTH ran at full opacity through a 900ms cross-dissolve. They are
   two different renders of the SAME entrance, so for nearly a second the building
   ghosted through itself — and the entry dove arrives huge and close inside the first
   0.3s of that, so it materialised half-transparent over a second copy of the facade
   with its wings reading against the wrong frame. That is the wing "changing".
   They share an opening frame, so the honest join is a CUT, not a dissolve. 160ms is
   short enough that nothing overlaps visibly and long enough not to read as a jump.
   js/temple.js reveals it on `playing` rather than on the click, so the white background
   below can never flash before the first frame has decoded. */
.veil-video{position:fixed;inset:0;z-index:60;width:100%;height:100%;object-fit:cover;
  object-position:center bottom;background:#fff;
  opacity:0;pointer-events:none;transition:opacity .16s linear}
.veil-video.playing{opacity:1;pointer-events:auto}
.fade-in{animation:fadein var(--t-breath) var(--ease-veil)}
/* seamless arrival: the room was already on screen behind the card — no blink, and the bar breathes in late */
html.seamless-arrive body.fade-in{animation:none}
html.seamless-arrive .shell-bar{opacity:0;animation:barbreathe 1.4s var(--ease-veil) 1.1s forwards}
@keyframes barbreathe{to{opacity:1}}
@keyframes fadein{from{opacity:0}}
/* THE DOVE PLAYS FOR EVERYONE (Director, 2026-08-23: "dont bypass the dove
   animation on the landing page, go landing page > dove animation > lobby on
   laptop and mobile"). It used to be `display:none!important` here, so anyone
   with Reduce Motion turned on crossed the threshold with no ceremony at all.
   It is a film of a bird arriving, not a parallax or a spin, and Skip is shown
   from the first moment under this preference (js/temple.js) so the way past it
   is always one tap. The hero LOOP behind the porch stays off under reduce:
   that one runs for ever, and a perpetual motion is the thing the preference is
   actually asking about. */
@media (prefers-reduced-motion: reduce){
  .fade-in{animation:none}
}

/* doors & lobby */

/* cards */
.card{background:var(--color-surface);border:1px solid var(--color-border);border-radius:var(--radius-md);
  box-shadow:var(--shadow-veil);padding:var(--space-3);margin-bottom:var(--space-2)}
.card h3{margin-bottom:var(--space-1)}
.stack{display:grid;gap:var(--space-2);padding:var(--space-3) 0}
.row{display:flex;justify-content:space-between;gap:var(--space-2);padding:var(--space-1) 0;
  border-bottom:1px solid var(--color-border)}
.row:last-child{border-bottom:none}
.pill{display:inline-block;padding:.15em .8em;border-radius:999px;font-size:.875rem;font-weight:500}
.pill--active{background:var(--color-success);color:#fff}
.pill--off{background:var(--color-border);color:var(--color-text-muted)}

/* forms, signing the book */
.book-panel{max-width:26rem;margin:8vh auto;background:var(--color-surface);border-radius:var(--radius-md);
  box-shadow:var(--shadow-sanctum);padding:calc(var(--space-3)*1.2);border:1px solid var(--color-border)}
/* contact.html's form, and it is the only `.book-panel` on the site — grep it.
   The input rules further down this file are all `.auth-panel .field input`, and this
   form is not an auth panel, so its four inputs fell back to the browser default:
   153x21 at 13.33px, with every label wrapped onto the previous input's line, sitting
   above a full-width bordered textarea. The mobile pass (2026-08-10) fixed exactly
   these four lines but wrote them inside its own media block, marked "desktop needs
   its own pass", and desktop kept the broken form. Director, 2026-08-11: "make sure
   the entire website is consistant desktop and mobile" — so this is that pass, and the
   rules simply come out of the media query. One form, one appearance, every width.
   Matched to the TEXTAREA beside them rather than to some new size, because that
   textarea carries its border, padding and `font:inherit` in an INLINE style attribute,
   which beats any rule written here. Same reason the type is `font:inherit` (18px) and
   not 16: it has to agree with the box below it, and anything at or above 16px stops
   mobile Safari zooming the viewport on focus. */
.book-panel .field{display:block;margin-bottom:.9rem}
.book-panel .field span{display:block;font-size:.85rem;font-weight:500;margin-bottom:.3rem;
  color:var(--color-text)}
.book-panel .field input{width:100%;min-height:44px;font:inherit;padding:.6rem;
  color:var(--color-text);background:var(--color-bg);
  border:1px solid var(--color-border);border-radius:var(--radius-sm)}
.book-panel .field input:focus-visible{outline:2px solid #8a6f2e;outline-offset:1px;
  border-color:#8a6f2e}
.book-panel .field textarea{min-height:44px}
/* the Book: a 3D open spread in a lit room */
.book-room{position:relative;min-height:100svh;display:grid;place-items:center;padding:4vh var(--space-2);overflow:clip;
  background:#dfeefa}
/* the V1 sky, built exactly as version one builds it:
   home-bg under a seamless banner-clouds marquee (2 copies, 48s loop) under a white gradient */
.v1-sky{position:fixed;inset:0;pointer-events:none}
.v1-sky .bg{position:absolute;inset:0;background:url(../assets/home-bg.webp?v=1784710734) center/cover no-repeat}
.v1-sky .cloud-track{position:absolute;inset:0 0 auto;height:88%;opacity:.85;overflow:hidden}
.v1-sky .cloud-list{position:absolute;top:0;left:0;height:100%;display:flex;flex:none;
  /* ponytail: one number. 48s read as fast because this sky is a STILL photograph
     with only the clouds moving over it, so the drift is the whole motion and it
     showed (Director 2026-08-27: "I wanted to slow down the clouds"). */
  will-change:transform;animation:marquee 96s linear infinite reverse}
.v1-sky .cloud-list img{height:100%;width:auto;aspect-ratio:140/19;display:block;flex:none}
@keyframes marquee{from{transform:translateX(-50%)}to{transform:translateX(0%)}}
.v1-sky .grad{position:absolute;inset:auto 0 0;height:50%;
  background:linear-gradient(180deg,rgb(255 255 255 / 0) 0%,rgb(255 255 255 / .5) 60%,rgb(255 255 255 / .92) 100%)}
/* the temple interior: same hall as the lobby scene, so signing in dissolves you into it */
.v1-sky .interior{position:absolute;inset:0;background-image:url(../assets/temple-interior.webp?v=1785136000);
  background-size:cover;background-position:center}
@media (prefers-reduced-motion: reduce){.v1-sky .cloud-list{animation:none}}
/* signing dissolves the card away inside the hall */
.auth-panel.dissolve{opacity:0;transform:scale(.96);filter:blur(8px);
  transition:opacity .44s var(--ease-veil),transform .44s var(--ease-veil),filter .44s var(--ease-veil)}
.auth-brand.dissolve{opacity:0;transition:opacity .44s var(--ease-veil)}

/* V1-style tabs in the member bar */
/* flex:0 0 auto, 2026-08-04. The logo was the only shrinkable item in this row,
   so the row balanced itself by eating the brand: measured at 24px, the lockup
   rendered 192px wide at 1280, 105 at 1180, 36 at 1100 and ZERO at 1024, where
   the School had no mark on screen at all. It never shrinks now; the row gives
   somewhere else, in the two bands below. */
.brand-logo{display:inline-flex;align-items:center;min-height:44px;flex:0 0 auto}
/* 30px, up from 24 (Director, 2026-08-10: "make the logo a little bigger").
   The bar is 60px, so this still leaves 15px of air above and below, and
   .brand-logo's 44px min-height is untouched. Anything much past this and
   the lockup starts crowding the bar's lower edge, where its frosted veil
   hangs 34px further down. */
.brand-logo img{height:30px;width:auto;display:block}
.shell-tabs{display:flex;gap:clamp(.5rem,2.2vw,1.8rem);overflow-x:auto;scrollbar-width:none}
.shell-tabs a{font-size:.78rem;letter-spacing:.16em;text-transform:uppercase;color:var(--color-text);
  text-decoration:none;padding:.3rem .1rem;border-bottom:2px solid transparent;min-height:44px;
  display:inline-flex;align-items:center;white-space:nowrap}
.shell-tabs a:hover{color:var(--color-accent)}
.shell-tabs a.active{border-bottom-color:#d9b662;color:var(--color-text)}
.shell-bar.transparent .shell-tabs a{text-shadow:0 1px 8px rgb(255 255 255 / .9)}
/* Heart of Space, 2026-08-04 (SSHB: "add Heart of Space as a separate portal up
   top in menu"). It is the ONLY tab that leaves the Temple, so it carries a small
   outward mark. Identical to every other tab in every other respect. */
.tab-out{display:inline-flex;align-items:center;gap:.4rem}
.tab-out svg{width:.62em;height:.62em;flex:0 0 auto;opacity:.5;transform:translateY(-.18em);
  transition:opacity .2s var(--ease-veil)}
.tab-out:hover svg,.tab-out:focus-visible svg{opacity:1}
@media (prefers-reduced-motion:reduce){.tab-out svg{transition:none}}
/* desktop: a quiet way home, resting in the bottom-right of every room */
.lobby-return{position:fixed;right:1.4rem;bottom:1.2rem;z-index:40;font-family:var(--font-serif);
  font-size:.8rem;letter-spacing:.22em;text-transform:uppercase;text-decoration:none;
  color:rgb(90 74 42 / .62);background:rgb(255 253 246 / .55);border:1px solid rgb(217 182 98 / .35);
  border-radius:999px;padding:.55rem 1.1rem;backdrop-filter:blur(6px);-webkit-backdrop-filter:blur(6px);
  transition:color .5s var(--ease-veil),background .5s var(--ease-veil),border-color .5s var(--ease-veil),box-shadow .5s var(--ease-veil)}
.lobby-return:hover,.lobby-return:focus-visible{color:#5a4a2a;background:rgb(255 253 246 / .9);
  border-color:rgb(217 182 98 / .75);box-shadow:0 0 30px rgb(255 244 210 / .5)}
/* phone: no room for the pill bottom-right (the panorama's swipe plate lives there),
   so it becomes a small chevron under the bar, top-left, clear of the menu button
   (top-right). z-index 20: over the swipe plate (7), under the bar and menu (39/40). */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  /* 2026-08-19 (audit): the chevron under the bar covered page headings on the
     non-panorama pages (Meditation, Account, the courses), so it lives IN the bar now,
     left of the brand, the way an iOS back control does. z-index 41: over the bar (40). */
  .lobby-return{position:fixed;right:auto;bottom:auto;left:8px;z-index:41;
    top:calc(env(safe-area-inset-top,0px) + 8px);
    min-height:44px;min-width:44px;display:inline-flex;align-items:center;justify-content:center;
    padding:0 .9rem;font-size:0;letter-spacing:0;pointer-events:auto}
  .lobby-return span{font-size:1.4rem;line-height:1}   /* the &lsaquo; the link already carries */
  body:has(.lobby-return) .shell-bar .brand-logo{margin-left:50px}}

/* THE BAR'S THREE WIDTHS, 2026-08-04, all three arithmetic rather than taste.
   The full row wants 32 (padding) + 209 (the lockup at 24px) + 16 + 835 (seven
   tabs, measured) + 16 + 193 (the account pills) = 1301px. It has had seven tabs
   only since Heart of Space was added this morning, which is what tipped it.
     >=1320px  the whole lockup and every tab, with air.
     1025-1319 the mark alone (the study's own small form, its icon row is
               16/32/64px) and a tighter tab gap, so no tab is lost.
     <=1024px  the glass sheet. Below this the seven tabs cannot fit beside even
               the bare mark, so hiding them behind a control the member can see
               beats scrolling them off an edge with nothing to say so.
   The last number is the one to move if the Director wants tabs further down. */
@media (max-width:1319px) and (min-width:1025px){
  .shell-bar .brand-logo img{content:url(../assets/logo-mark.svg?v=1786426043)}
  .shell-tabs{gap:.75rem}
  .shell-tabs a{letter-spacing:.12em}
}

/* mobile: the bar slims to logo + hamburger; the rooms live in a glass sheet */
.menu-btn,.mobile-menu{display:none}
@media (max-width:1024px){
  /* ONE NAME FOR THE BAR'S HEIGHT (2026-08-10). Before this there were three
     numbers for the same clearance in this file — 60px on the sheet, 70px on
     `.room-panel` and `.room-plates`, 56px in an inline style on two pages —
     so four people writing four mobile screens each cleared a different bar and
     nothing lined up. Every mobile top clearance below is now
     `calc(var(--bar-h) + Nsvh)`, and this is the only place the 60 is written.
     It carries the notch itself, so nothing downstream has to remember to.
     Declared here and not on a bare :root on purpose: it means nothing above
     1024, where the bar is a different object with tabs in it. */
  :root{--bar-h:calc(60px + env(safe-area-inset-top))}
  /* the phone's own edges, which the desktop does not have. Dead until a page
     ships viewport-fit=cover; harmless, and correct the moment one does. */
  .shell-bar{flex-wrap:nowrap;height:var(--bar-h);
    padding:env(safe-area-inset-top) calc(var(--space-2) + env(safe-area-inset-right))
            0 calc(var(--space-2) + env(safe-area-inset-left))}
  .shell-tabs,.shell-actions{display:none}
  /* about.html, contact.html and privacy.html hardcode their own bar with no
     .menu-btn and no .mobile-menu anywhere in the DOM, so the line above was
     hiding their ONLY way back into the School: measured, the Enter button
     rendered 0x0 on all three. Give the actions back exactly where the sheet
     does not exist. One rule instead of ~90 lines of duplicated menu markup on
     each page, and it cannot reach a page that has a hamburger. */
  .shell-bar:not(:has(.menu-btn)) .shell-actions{display:flex}
  /* ...and at 44, not the 38 the desktop pill wears. Handing back a button that
     is under the tap floor is not handing it back. */
  .shell-bar:not(:has(.menu-btn)) .shell-actions .btn{min-height:44px}
  /* A PHONE HAS NO HOVER, IT HAS A PRESS. Chrome paints a blue wash on every tap
     without the first line, and an unfenced :hover LATCHES after a tap and stays
     latched until you touch something else. Most of this file's hovers are already
     fenced behind (hover:hover); these three were not, and they are the three you
     actually meet on a phone: the music button glows gold on every room, the
     restored Enter button above, and a glossary letter keeps a gold field after
     you jump. Answered here rather than by opening a (hover:none) query, which
     would be a fourth definition of "mobile" in a pass that exists to remove one. */
  body{-webkit-tap-highlight-color:transparent;-webkit-text-size-adjust:100%}
  .music-toggle:hover:not(:focus-visible){box-shadow:none}
  .shell-bar .shell-actions .btn:hover:not(:focus-visible){border-color:rgb(217 182 98 / .45);
    box-shadow:inset 0 1px 0 rgb(255 255 255 / .9), 0 2px 12px rgb(150 110 40 / .10)}
  .gl-rail a:hover:not(:focus-visible){background:none;color:#6f5a26}
  .shell-bar .menu-btn{display:flex;flex-direction:column;gap:5px;justify-content:center;align-items:center;
    width:44px;height:44px;flex:none;border:1px solid rgb(217 182 98 / .45);border-radius:50%;cursor:pointer;
    background:linear-gradient(180deg, rgb(255 255 255 / .85), rgb(255 250 236 / .5));
    backdrop-filter:blur(8px);-webkit-backdrop-filter:blur(8px)}
  .menu-btn span{display:block;width:18px;height:1.5px;background:#5a4a2a;border-radius:1px;
    transition:transform .35s var(--ease-veil),opacity .25s}
  .menu-btn[aria-expanded="true"] span:nth-child(1){transform:translateY(6.5px) rotate(45deg)}
  .menu-btn[aria-expanded="true"] span:nth-child(2){opacity:0}
  .menu-btn[aria-expanded="true"] span:nth-child(3){transform:translateY(-6.5px) rotate(-45deg)}
  .mobile-menu{position:fixed;inset:var(--bar-h) 0 0;z-index:39;display:flex;flex-direction:column;
    align-items:center;justify-content:center;gap:.9rem;overscroll-behavior:contain;
    background:rgb(255 253 246 / .55);backdrop-filter:blur(30px) saturate(1.5);
    -webkit-backdrop-filter:blur(30px) saturate(1.5)}
  /* THE SHEET FLOATS OVER A PAGE THAT IS STILL SCROLLING, and on the two course
     documents that page is nearly 16,000px tall: a swipe on the glass scrolled the
     essay behind it. `overscroll-behavior:contain` above does NOT fix that on its
     own and it was measured proving it — on a normal page the sheet is not a scroll
     container at all (scrollHeight == clientHeight), so there is no chain to
     contain. This is the line that freezes the page, and it is still not JavaScript.
     Keyed off the BUTTON, not off `.mobile-menu:not([hidden])`: the button's
     aria-expanded is only ever true after a real press, so a menu parked inside
     some other hidden mount can never freeze the page by accident. */
  html:has(.menu-btn[aria-expanded="true"]){overflow:hidden}
  .mobile-menu>a{font-family:var(--font-serif);font-size:1.3rem;letter-spacing:.14em;text-transform:uppercase;
    color:var(--color-text);text-decoration:none;padding:.4rem 1rem;min-height:44px;display:flex;align-items:center}
  .mobile-menu-actions{display:flex;gap:1rem;margin-top:1.8rem}
  .mobile-menu-actions .btn{min-width:8.2rem;min-height:44px;display:inline-flex;
    align-items:center;justify-content:center;font-size:.9rem;letter-spacing:.05em}
  .mobile-menu[hidden]{display:none}
}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  /* a whisper at the foot of the first door: there is more below */
  /* The home-indicator inset goes on THIS declaration and not on a new
     `.deck-cue{bottom:...}` at the end of the file, and that is not tidiness.
     `.deck-cue--pan` below re-sets `bottom` at the same specificity (0,1,0), so a
     later rule for `.deck-cue` wins over it on source order alone — the panorama's
     cue was measured dropping from 89px to 29px and landing on top of the STEP
     INSIDE plate. Written here it sits before its own override and cannot collide. */
  .deck-cue{position:fixed;left:50%;transform:translateX(-50%);
    bottom:calc(3.5svh + env(safe-area-inset-bottom));z-index:5;
    display:flex;flex-direction:column;align-items:center;gap:.1rem;pointer-events:none;
    color:#4a3a1c;opacity:.9;text-shadow:0 1px 8px rgb(255 255 255 / .9),0 0 18px rgb(255 255 255 / .7);
    transition:opacity .8s ease}
  .deck-cue span{font-size:.6rem;letter-spacing:.34em;text-transform:uppercase}
  .deck-cue .arr{font-size:1.1rem;animation:hintbob 2.6s ease-in-out infinite}
  @keyframes hintbob{0%,100%{transform:translateY(0)}50%{transform:translateY(5px)}}
  .deck-cue.off{opacity:0}
}
@media (min-width:821px) and (min-height:501px){.deck-cue{display:none}}

/* ---- the temple scene (lobby): image stage + live doors ---- */
.temple-scene{position:relative;min-height:100svh;overflow:clip;background:#f6efdf}
body:has(.temple-scene){background:#f6efdf}
body:has(.book-room){background:#dfeefa}
/* every scene backdrop is FIXED and always covers the whole viewport (lvh/lvw so
   no white strip when a mobile toolbar collapses); content scrolls over it */
.scene-frame{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);z-index:0;
  aspect-ratio:1561/1008;min-width:100vw;min-height:100vh;
  min-width:100lvw;min-height:100lvh}
.scene-zoom{position:absolute;inset:0;transition:transform 1.6s cubic-bezier(.6,.05,.35,1)}
.scene-zoom.push{transform:scale(2.8)}
/* THE DRAWN DOVE'S CSS IS GONE TOO, 2026-08-11. Director: "You can't have the animated
   bird just flying through a door. I'm not sure and I don't like that." The .dove-host /
   .dove-fly rules and the dove-cross and dove-flap keyframes were deleted with the SVG
   that used them (js/temple.js). Nothing else referenced them — grepped. They are in git
   before this commit; do not rebuild a stand-in bird from memory. A film or nothing. */
.scene-zoom .stage{position:absolute;inset:0;width:100%;height:100%;object-fit:fill;user-select:none;pointer-events:none}
/* the living layer wakes over the still once it can play; never a hard swap */
.stage-live{opacity:0;transition:opacity 2s var(--ease-veil)}
.stage-live.on{opacity:1}
/* the room mostly rests as a still; now and again a film wakes over it and hands back */
.stage-ambient{opacity:0;transition:opacity .5s var(--ease-veil);pointer-events:none;z-index:1}
.stage-ambient.on{opacity:1}
@media (prefers-reduced-motion: reduce){.stage-ambient{display:none}}
.stage-ajar{opacity:0;transition:opacity .7s var(--ease-veil)}
.temple-scene.opening .stage-ajar.on{opacity:1}
.arch{position:absolute;display:block;text-decoration:none;cursor:pointer;outline-offset:4px;
  border-radius:46% 46% 6px 6px / 30% 30% 6px 6px}
.arch .halo{position:absolute;inset:-14% -20%;border-radius:inherit;opacity:0;transition:opacity .5s ease;
  background:radial-gradient(ellipse 62% 58% at 50% 62%, rgb(255 240 198 / .6), rgb(255 240 198 / 0) 72%);
  filter:blur(5px);pointer-events:none}
.arch:focus-visible .halo{opacity:1}
@media (hover:hover){.arch:hover .halo{opacity:1}}
.arch:active .halo{opacity:1}   /* a phone has no hover; the press lights the door instead */
.arch .plate{position:absolute;left:50%;top:calc(100% + .5rem);transform:translateX(-50%);white-space:nowrap;
  font-family:var(--font-serif);font-size:clamp(.72rem,1vw,.95rem);letter-spacing:.2em;text-transform:uppercase;
  color:#565073;text-shadow:0 1px 10px rgb(255 255 255 / .95),0 0 24px rgb(255 255 255 / .8);
  opacity:.85;transition:opacity .3s;text-align:center}
.arch--soon{cursor:default}
.arch--soon .halo{background:radial-gradient(ellipse 62% 58% at 50% 62%, rgb(205 218 240 / .45), transparent 72%)}
.arch--soon .plate small{display:block;font-size:.78em;letter-spacing:.3em;color:#565073;text-transform:uppercase}
.scene-greeting{position:absolute;top:5%;left:0;right:0;text-align:center;pointer-events:none;
  font-family:var(--font-serif);font-weight:500;font-size:clamp(1.2rem,1.6vw + .6rem,1.9rem);letter-spacing:.14em;
  text-transform:uppercase;color:#fff;text-shadow:0 2px 8px rgba(0,0,0,.22),0 0 30px rgba(255,255,255,.3)}
.scene-flash{position:absolute;inset:0;z-index:6;pointer-events:none;opacity:0;transition:opacity .9s ease;
  background:radial-gradient(ellipse 85% 75% at 50% 55%, #ffffff, #fcfcf9 35%, #faf9e7 60%, #f0e2c0 85%, #d9b662 120%)}
.temple-scene.flash .scene-flash{opacity:1}
/* The door film, 2026-08-04. It has to sit EXACTLY on the still it grew out of or the
   cut into it is a jump. Measured, not eyeballed: the still is 1561x1008 and the film
   came back 864x496, and comparing frame 0 against the still says it is a centre crop in
   HEIGHT at full width (mean error 2.19/255) and not a squash (11.11). 1561/1.7419 = 896,
   so the film covers 896 of the still's 1008 rows, 56 off the top and 56 off the bottom:
   56/1008 = 5.5556% and 896/1008 = 88.8889%. `fill` because the stage image under it is
   `fill` too, so both take the same horizontal stretch and the pillars stay put.
   ⚠ Re-measure these two numbers if a film is ever regenerated at a different ratio. */
.door-film{position:absolute;left:0;width:100%;top:5.5556%;height:88.8889%;
  object-fit:fill;z-index:5;pointer-events:none;opacity:0;transition:opacity .18s linear}
.door-film.on{opacity:1}
/* THE PHONE'S OWN FILM, 2026-08-11. Director: "It's meant to be a video on the mobile
   not the little Dove animation like the desktop version it has to be the same but made
   for the mobile version." Each one is generated from the slice of the room the phone
   shows of that door (work/v4-door-dove/crop-portrait.py) rather than from the whole
   wall, so the dove's entrance happens inside the frame instead of off the side of it.

   ⚠ TWO WRONG ANSWERS CAME FIRST, AND THEY SHARE ONE ROOT CAUSE: THE ENGINE RETURNS
     720x1280 WHATEVER CROP IT IS GIVEN. Take 1's crops were cut at the phone's own
     0.462, so it reached 9:16 by CENTRE-CROPPING THE HEIGHT — frame 0 against the crop
     scored 2.0-3.4/255 for a height-crop and 25-53/255 for a width-pad, so this is
     measured, not surmised. A film that is the crop minus its top and bottom cannot both
     fill the phone and stay in register with the still:
       `inset:0` + `object-fit:cover` -> filled the screen, doorway jumped 22% bigger
       full width + natural height     -> in register, but left bands of static still
     The Director saw the second within minutes: "theres like a stactic image on the
     right hand side like a thin strip", then "and the bottom, its like the video doesnt
     fill the screen."
     THE FIX IS UPSTREAM, not here: crop-portrait.py cuts at 9:16 now, so the engine
     trims nothing, the film IS the whole crop, and it can be laid over the artwork in
     register AND still cover the phone — a 9:16 slice of this artwork is wider than the
     phone's window, so the excess is clipped rather than missing.

   ⚠ SIZED IN JS, AND DERIVED RATHER THAN TYPED. The still is drawn `object-fit:fill`
     into a box that is not its own aspect, so it is stretched by different amounts in x
     and y, and the film must take the SAME stretch or it will not register. playDoorFilm
     works the crop's share of the still's width out from the video's own aspect and the
     stage image's natural size, so a re-rolled film at any ratio keeps working with
     nothing to edit here. Same discipline as the landscape film's measured seating above.

   ⚠ .door-film-stage does the clipping, NOT the viewport: a fixed element wider than the
     window can put a horizontal scrollbar on the page. It is `fixed` and appended to
     <body>, so no scroller, transform or overflow between here and the scene can drag it
     sideways when the panorama re-snaps mid-stride.
   ⚠ z-index 5 keeps it UNDER `.scene-flash` (6), so the light still rises over the end of
     the film exactly as it does on the desktop.
   ⚠ `max-width:none` IS LOAD BEARING, AND LEAVING IT OUT IS THE "WARP". The reset at the
     very top of this file says `img,video{max-width:100%;height:auto}`. This film is
     DELIBERATELY wider than the window — a 9:16 slice of the artwork is 474.7px against a
     390px phone, and the excess is meant to be clipped by .door-film-stage. Without
     max-width:none the reset clamped it to 390 and `object-fit:fill` squashed the picture
     horizontally by 18%, so the room went narrow the instant the film appeared. Director:
     "the videos is now warp a little bit at the start sort of like go wide and then come
     back only on mobile". Measured: inline width 474.7px, computed width 390px.
     `max-height:none` was already here for the same reason on the other axis; its twin
     was simply missed.
   ⚠ And `width:auto` so the intrinsic aspect is right from the very first painted frame.
     seat() sets an exact width, but if it runs on `loadedmetadata` rather than
     immediately, the frames before it inherit `width:100%` from `.door-film` above —
     which is the same 390px squash, briefly. auto lands on 474.7 by itself here, so
     nothing moves when seat() confirms it. */
.door-film-stage{position:fixed;inset:0;overflow:hidden;z-index:5;pointer-events:none}
/* ⚠ `width:auto` WAS THE HOLE, AND IT IS CLOSED (2026-08-25). While the width was auto
   the box was whatever the engine guessed until something gave it a size, and iOS
   guesses the default intrinsic 300x150 -- a 2:1 box where a 9:16 one belongs, which is
   the warp. seat() in js/temple.js normally sets an exact width in pixels before the
   first frame, but any moment it could not (and before today it measured the still
   behind the door, so there were such moments) fell straight back to the guess.
   The width is declared here instead. It is not a magic number: the film's width is the
   frame's HEIGHT times the film's aspect -- the artwork's own dimensions cancel out of
   that sum, see the working in seat() -- and the frame is the full height of the screen.
   So 9/16 of the screen height IS the width, always, and seat() confirms it to the
   decimal rather than being the only thing that knows it.
   ⚠ AND THE HEIGHT COMES FROM THE SAME UNIT AS THE WIDTH, WHICH MATTERS MORE THAN
   EITHER BEING EXACT. This is `object-fit:fill`, so the picture is stretched into
   whatever box it is given: a width and a height that disagree do not letterbox, they
   DISTORT, which is the very fault being fixed. `height:100%` is the stage's height,
   and on iOS a fixed element's height is the LARGE viewport while `svh` is the small
   one, so with the URL bar hidden those two were different numbers and the picture was
   squeezed between them. Both are svh now, so they can never disagree; if svh is a few
   pixels short of the screen the film is a few pixels short of the screen, undistorted,
   and seat() puts it exactly right the moment it measures. Undistorted and slightly
   small beats exact and stretched.
   Re-cut the portrait films at another ratio and this number and DECLARED in temple.js
   are the two places that change. */
.door-film--full{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  width:calc(100svh * 9 / 16);height:100svh;max-width:none;max-height:none;object-fit:fill}
@media (prefers-reduced-motion:reduce){.door-film{display:none}}
/* generic room: content floats on a soft column over the full-bleed scene */
.room-panel{position:relative;z-index:4;width:min(92vw,44rem);margin:0 auto;
  padding:calc(56px + 6svh) 0 12svh}
/* ⚠ THE DESKTOP SPHERE WALL WAS HERE AND IS DELETED (2026-08-10).
   About 24 lines: `.room-panel` as a horizontal snap scroller with perspective:1300px,
   `.pane` flattened to a bare 3D cell, and `.pane-3d` carrying the glass, the mask and
   the inner scroll. It was written for `@media (min-width:701px)` and it could not
   paint a single pixel at any width, because `.exhibit-scene .room-panel{display:none}`
   (further down this file) hides every `.room-panel` on the site from 821px up, and
   EVERY page that has one is an `.exhibit-scene`: alchymie, courses, library, sessions
   and zoom. Measured before deleting, not reasoned about — display:none on all five at
   1025 and at 1512, box 0x0. So the wall only ever rendered in the dead 701-820 band,
   where it also broke the tablet: a room showed the desktop scroller with its hotspots
   already hidden, i.e. nothing to click and nothing to read.
   Renaming it to 821 would have made it unreachable at every width instead of just
   most of them. Deleting it removes the fifth thing a builder has to know about
   `.room-panel`, and the phone's card rail (end of this file) now owns that element
   below 820 with no desktop rule left to fight.
   Its JavaScript went with it in the same change: the `.pane-3d` wrapper, the wheel
   pager, the pointer-drag flick and the per-frame `sphere()` scroll listener are all
   gone from js/temple.js, which no longer contains the string `pane-3d` at all. The
   wall and its engine were removed together; there is nothing left of it to hunt for. */
.room-panel .pane{background:rgb(255 253 246 / .3);backdrop-filter:blur(22px) saturate(1.5);
  -webkit-backdrop-filter:blur(22px) saturate(1.5);
  border:1px solid rgb(255 255 255 / .5);border-radius:10px;
  box-shadow:0 16px 44px rgb(120 90 30 / .16);padding:clamp(1.4rem,3vw,2.4rem);
  margin-bottom:1.2rem}
.room-panel h1{font-size:clamp(1.5rem,2vw + .6rem,2.1rem);margin:0 0 .5rem}
.room-panel h2{font-size:clamp(1.1rem,1.2vw + .5rem,1.4rem);margin:0 0 .6rem}
.room-panel .pane .small{color:#55506b}
/* the meditation chamber: cards live on the flat wall panel, scroll up and dissolve */
.scene-frame--med{aspect-ratio:3/2}
/* the projector: the room crossfades to its dimmed twin and the film plays 16:9
   on the gold-framed wall panel, as if projected there */
.stage-dim{opacity:0;transition:opacity 2.4s var(--ease-veil)}
.room-scene.dimmed .stage-dim{opacity:1}
/* deck and film share the SAME box on the wall panel, in % of the IMAGE frame,
   so both stay glued to the panel at any window size; cards bow out, picture fades in */
/* the deck bows out while the film plays, and returns with the light */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .room-scene .scene-frame{position:fixed;left:0;top:0;transform:none;min-width:0;width:100vw;
    aspect-ratio:auto;min-height:0;height:100lvh}
  .room-scene .stage{object-fit:cover;object-position:center 30%}
  .room-panel{padding-top:calc(var(--bar-h) + 3svh)}
}
/* mobile: the rotunda becomes a stack of door cards; slide, tap, the door opens and you go in */
.door-deck,.pan-plate{display:none}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  /* THE SCROLLER IS EXACTLY THE VISIBLE VIEWPORT — svh, the SMALL viewport, the one
     with the browser toolbar still showing. Sized in lvh it would stand taller than
     the phone whenever the toolbar was down, and the page itself would scroll
     vertically: you would still be scrolling, which is the whole thing we removed.
     Nothing is done to html/body here on purpose — sign-the-book and enter mount this
     same scene with the sign-in card over it, and that card must still scroll. */
  .temple-scene:has(.door-deck){height:100svh;min-height:0;overflow-x:auto;overflow-y:hidden;
    scroll-snap-type:x mandatory;overscroll-behavior:contain;touch-action:pan-x;
    scrollbar-width:none;
    /* ⚠ FLEX ONLY SO THE ARTWORK CENTRES VERTICALLY WHEN IT IS TALLER THAN THE PHONE,
       which is the landscape case below. One in-flow child (.scene-frame, which was
       already carrying a vestigial `flex:none`), so this changes nothing in portrait:
       there the artwork is exactly 100lvh and centring it is a no-op. Measured. */
    display:flex;align-items:center}
  .temple-scene:has(.door-deck)::-webkit-scrollbar{display:none}
  /* the ARTWORK is sized in lvh, so it still covers when the toolbar slides away. It
     stands taller than the scroller and the scroller clips it: cover, never a bare
     strip, and never a page that scrolls. Width follows height, so the room runs
     past both edges of the phone.
     ⚠ ...UNTIL THE PHONE IS TURNED, and then height leads to a room NARROWER than the
     screen. A plain `width:calc(100lvh * 1561/1008)` is 1307px against a 390px portrait
     phone (right: the room runs past both edges) but only 604px against an 844px
     landscape one, which measured as 240px of bare cream page beside the artwork with
     the doors bunched to the left. So each dimension takes whichever of the two
     constraints is larger: fill the height, OR fill the width, whichever demands more.
       portrait  390x844: height max(844, 252) = 844, width max(390, 1307) = 1307
       landscape 844x390: height max(390, 545) = 545, width max(844,  604) =  844
     Portrait is arithmetically untouched — the same two numbers it had before. In
     landscape the room now covers edge to edge and hangs 77px past the top and bottom,
     which is the same overhang the desktop composition has always had, and the flex
     centring on the scroller above is what keeps that overhang even. */
  .temple-scene:has(.door-deck) .scene-frame{display:block;position:relative;left:auto;top:auto;
    transform:none;height:max(100lvh, 100vw * 1008 / 1561);
    width:max(100vw, 100lvh * 1561 / 1008);min-width:0;min-height:0;
    aspect-ratio:auto;flex:none}
  /* every doorway is a resting place: a swipe never stops on blank wall */
  .temple-scene:has(.door-deck) .arch{scroll-snap-align:center;scroll-snap-stop:always}
  /* the door you are standing in front of: lit, and standing open */
  .temple-scene:has(.door-deck) .arch.locked .halo{opacity:1}
  .temple-scene:has(.door-deck) .stage-ajar.ajar{opacity:1}
  /* the flash belongs to the phone, not to the panorama it sits inside */
  .temple-scene:has(.door-deck) .scene-flash{position:fixed}
  /* the deck this replaces, and the text list it makes unnecessary */
  .temple-scene:has(.door-deck) .door-deck{display:none!important}
  /* the invitation at your feet. The door already carries its name carved in marble,
     so the plate does not say it twice — it only offers the way in, and guarantees a
     44px target whatever the artwork does. The name still goes to screen readers. */
  .pan-plate{position:fixed;left:50%;transform:translateX(-50%);bottom:3.2svh;z-index:7;
    display:flex;align-items:center;justify-content:center;min-height:44px;
    padding:.5rem 1.6rem;text-decoration:none;border-radius:999px;
    background:rgb(255 253 246 / .5);border:1px solid rgb(217 182 98 / .42);
    backdrop-filter:blur(10px);-webkit-backdrop-filter:blur(10px);
    box-shadow:0 2px 20px rgb(255 244 210 / .5);
    transition:opacity .45s var(--ease-veil)}
  .pan-plate .pan-name{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%)}
  .pan-plate .pan-step{font-family:var(--font-serif);font-size:.72rem;letter-spacing:.3em;
    text-transform:uppercase;color:#5a4a2a;line-height:1}
  .temple-scene.opening .pan-plate{opacity:0}
  /* said once, at the start: this room turns. It rests above the plate, never on it */
  .deck-cue--pan{bottom:calc(3.2svh + 62px)}
  .deck-cue--pan .arr{animation:hintsway 2.8s ease-in-out infinite}
  @keyframes hintsway{0%,100%{transform:translateX(-7px)}50%{transform:translateX(7px)}}
  .door-deck{display:block;position:relative;z-index:2;height:100svh;overflow-y:auto;
    scroll-snap-type:y mandatory;padding:0;scrollbar-width:none}
  .door-deck::-webkit-scrollbar{display:none}
  .door-card{display:block;position:relative;scroll-snap-align:start;scroll-snap-stop:always;
    width:100vw;height:100svh;margin:0;border-radius:0;border:0;box-shadow:none;overflow:hidden;
    cursor:pointer;transition:transform 1.1s var(--ease-veil),opacity 1.1s var(--ease-veil)}
  .door-card img{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}
  .door-card img.open{opacity:0;transition:opacity .7s var(--ease-veil)}
  .door-card.opening img.open{opacity:1}
  .door-card.opening{transform:scale(1.45);z-index:6}
  .door-card--soon{cursor:default}
  .door-card--soon::after{content:attr(data-soon);position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
    padding:.45em 1.1em;border-radius:999px;background:rgb(250 246 236 / .82);color:#6b6483;
    font-family:var(--font-serif);font-size:.78rem;letter-spacing:.3em;text-transform:uppercase;white-space:nowrap}
  .door-deck.opening .door-card:not(.opening){opacity:0}
  .temple-scene:has(.door-deck) .scene-doorlist{display:none!important}
}
@media (max-width:820px) and (prefers-reduced-motion:reduce),(max-height:500px) and (orientation:landscape) and (prefers-reduced-motion:reduce){
  /* someone who has asked for less movement should not be handed a room that only
     opens by flinging it. They get the deck back: one door per screen, no momentum. */
  /* ⚠ The TRANSITIONS these two elements carry are killed at the very END of this
     file, not here. `.scene-zoom` is redeclared at "the crossing" block further down
     and a later rule of equal specificity wins, so a `transition` written at this
     line is dead on arrival — which is exactly how the existing
     `@media (prefers-reduced-motion: reduce){.scene-zoom{transition:none}}` a few
     rules below became a no-op nobody noticed. Search "reduced motion, part two". */
  .temple-scene:has(.door-deck){height:auto;min-height:100svh;overflow-x:visible;scroll-snap-type:none}
  .temple-scene:has(.door-deck) .scene-frame{display:none}
  .temple-scene:has(.door-deck) .door-deck{display:block!important}
  .pan-plate,.deck-cue--pan{display:none!important}
}
.scene-doorlist{display:none}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .arch .plate{display:none}
  .scene-doorlist{position:absolute;left:0;right:0;bottom:0;z-index:5;display:flex;flex-wrap:wrap;gap:.5rem;
    justify-content:center;padding:1rem;background:linear-gradient(transparent, rgb(255 255 255 / .92) 40%)}
  .scene-doorlist a,.scene-doorlist span{font-family:var(--font-serif);font-size:.85rem;letter-spacing:.12em;
    text-transform:uppercase;padding:.6rem 1rem;min-height:44px;display:inline-flex;align-items:center}
  .scene-doorlist a{color:var(--color-text);background:rgb(255 255 255 / .9);border:1px solid var(--color-border);
    border-radius:var(--radius-sm);text-decoration:none}
  .scene-doorlist span{color:var(--color-text-muted)}
}
@media (prefers-reduced-motion: reduce){
  .scene-zoom{transition:none}
  .stage-ajar{transition:opacity .2s linear}
}
.field .err{display:none;color:var(--color-error);font-size:.8rem;margin-top:.3rem;text-transform:none;letter-spacing:0}
.field.invalid input{border-bottom-color:var(--color-error)!important}
.field.invalid .err{display:block}

.auth-panel{position:relative;max-width:26rem;width:100%;margin:0 auto;background:rgb(255 255 255 / .92);
  border:1px solid var(--color-border);border-radius:var(--radius-md);box-shadow:var(--shadow-sanctum);
  padding:clamp(2rem,4vw,3rem);backdrop-filter:blur(4px)}
/* the split threshold: ONE card; the School on the left, the Book on the right */
.auth-card{display:grid;grid-template-columns:minmax(0,1.12fr) minmax(21rem,24rem);
  width:100%;max-width:62rem;margin:0 auto;background:rgb(255 251 241 / .80);
  border:1px solid rgb(255 255 255 / .55);border-radius:var(--radius-md);
  box-shadow:var(--shadow-sanctum);backdrop-filter:blur(26px) saturate(1.5);
  -webkit-backdrop-filter:blur(26px) saturate(1.5);overflow:hidden;
  transition:opacity .44s var(--ease-veil),transform .44s var(--ease-veil),filter .44s var(--ease-veil)}
.auth-card.dissolve{opacity:0;transform:scale(.96);filter:blur(8px)}
.auth-card .card-primer{position:relative;padding:clamp(1.8rem,3.2vw,2.8rem);
  border-right:1px solid rgb(217 182 98 / .4);
  background:linear-gradient(165deg, rgb(250 246 236 / .55), rgb(255 255 255 / .3))}
.auth-card .card-primer::before{content:"";position:absolute;inset:0;pointer-events:none;
  background:url(../assets/temple-entrance.webp?v=1784710734) center bottom/cover no-repeat;opacity:.07}
.auth-card .card-primer>*{position:relative}
.card-primer .lead-line{font-family:var(--font-serif);font-size:clamp(1rem,.6vw + .85rem,1.18rem);
  line-height:1.65;margin:0 0 1.15rem;color:var(--color-text)}
.card-primer h3{font-family:var(--font-serif);font-weight:500;font-size:1.02rem;
  color:#7a6a3a;margin:1.05rem 0 .3rem}
.card-primer p{margin:.2rem 0;font-size:.9rem;line-height:1.62;color:var(--color-text)}
.auth-card .auth-panel{max-width:none;background:transparent;border:0;border-radius:0;
  box-shadow:none;backdrop-filter:none;padding:clamp(1.8rem,3.2vw,2.8rem);align-self:center}
.auth-face{animation:facein .45s var(--ease-veil)}
@keyframes facein{from{opacity:0;transform:translateY(5px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion: reduce){.auth-face{animation:none}}
@media (max-width:920px){
  .auth-card{grid-template-columns:1fr;max-width:28rem}
  .auth-card .card-primer{order:2;border-right:0;border-top:1px solid rgb(217 182 98 / .4)}
  .card-primer .lead-line br{display:none}
}
/* the V1 header (h3.loadervisual-text): white serif, letter-spacing 1px, dark drop + white glow.
   Sized to sit in the top band as one line; the card centers in the viewport independently. */
/* The masthead of the card, not a banner floating above it. It used to be pinned
   at top:4svh while the card was vertically centred, so on anything under 1080px
   tall the card rose into it. Inside the card it sits in normal flow and cannot
   collide with anything, and it is still the only place the School is named on
   this screen. On mobile the primer stacks first, so it is still read first. */
.auth-brand{display:block;text-decoration:none;
  font-family:var(--font-serif);font-weight:500;line-height:1.18;
  font-size:clamp(1.35rem,1.5vw + .75rem,1.95rem);
  color:var(--color-text);letter-spacing:.5px;
  margin:0 0 1.15rem;padding:0 0 .9rem;
  border-bottom:1px solid rgb(217 182 98 / .45)}
.auth-brand:hover{color:#7a6a3a}
.auth-brand:focus-visible{outline:2px solid var(--color-primary);outline-offset:3px;border-radius:3px}
/* The brand is pinned at top:4svh while the card is vertically centred, so on a
   SHORT viewport the card rises into the title. Measured: it overlaps at every
   height below 1080px, including 1440x900 and 1366x768, which is most laptops.
   Narrow screens already dropped it into normal flow above the card, so short
   screens use the very same treatment rather than a second mechanism. */
@media (max-width:920px){
  .book-room{align-content:center;grid-template-rows:auto auto}
}
@media (max-height:760px){
  .book-room{padding-top:2vh;padding-bottom:2vh}
}
.auth-panel h1{font-size:clamp(1.6rem,2vw + .8rem,2rem);text-align:center;margin-bottom:var(--space-1)}
.auth-panel .sub{text-align:center;color:var(--color-text-muted);font-size:.98rem;margin-bottom:var(--space-3)}
.auth-panel .field{margin-bottom:var(--space-2)}
.auth-panel .field span{display:block;font-size:.85rem;font-weight:500;margin-bottom:.3rem;color:var(--color-text)}
.auth-panel .field .err{display:none;color:var(--color-error);font-weight:400;margin-top:.3rem}
.auth-panel .field.invalid .err{display:block}
.auth-panel .field.invalid input{border-color:var(--color-error)}
.auth-panel .field input{width:100%;min-height:44px;padding:var(--space-1) var(--space-2);font:inherit;
  color:var(--color-text);background:var(--color-bg);border:1px solid var(--color-border);border-radius:var(--radius-sm)}
/* --color-accent is #5f63c0, a periwinkle indigo used nowhere else on this
   screen, so clicking into the first field grew a purple ring on a gold temple.
   Bronze is the same gesture in the palette the card is actually made of, and at
   #8a6f2e it clears 4.5:1 on the cream field, which the pale --color-primary
   would not. (2026-07-29) */
.auth-panel .field input:focus-visible{outline:2px solid #8a6f2e;outline-offset:1px;border-color:#8a6f2e}
.auth-panel .btn-primary{width:100%;margin-top:var(--space-1)}
.auth-panel .under-note{text-align:center;font-size:.875rem;color:var(--color-text-muted);margin:var(--space-2) 0 0}

/* states */
/* 2026-08-19: the 3px side tab became a hairline; a notice is a surface with a
   coloured edge, not a tab. */
.notice{border:1px solid var(--color-border);border-left-width:1px;background:var(--color-surface);
  border-radius:var(--radius-sm);padding:var(--space-2);margin:var(--space-2) 0}
.notice--error{border-left-color:var(--color-error)}
.veil-shimmer{border-radius:var(--radius-md);background:var(--color-surface);
  animation:shimmer var(--t-breath) ease-in-out infinite alternate;min-height:5.5rem;margin-bottom:var(--space-2)}
@keyframes shimmer{from{opacity:.55}to{opacity:1}}
.empty{color:var(--color-text-muted);text-align:center;padding:calc(var(--space-3)*2) var(--space-2)}
/* ⚠ THE PAGE MAY NOT WAIT FOR THE SCRIPT TO BE SHOWN AT ALL (2026-08-25, red team).
   Every page wraps its whole body in `[data-state-panel="populated"]`, and this rule
   used to hide ALL of them until js/temple.js ran and added `.on`. temple.js is 247KB
   and the last thing in the body, so iOS painted a document that was a 60px header and
   nothing else, composited the fixed bar against a page ~60px tall, and then one
   classList.toggle later the document became 18,000px. That is the largest layout shift
   a page can have -- 0% to 100% of it -- landing after the compositor has already
   decided where the fixed bar goes, and it is why the fault tracked page length exactly:
   worst on Foundational Practice (~18,000px), mild on Essene (~1,700px).
   And it bought nothing. `state` is 'populated' on every visit; the other three panels
   exist for a QA switcher that only works on localhost (js/temple.js, `IS_LOCAL &&
   qs.get('state')`). So the real page is shown at first paint, and only the switcher,
   which announces itself with `html.qa-state`, can take it away again. */
[data-state-panel]{display:none}
[data-state-panel="populated"],[data-state-panel].on{display:block}
html.qa-state [data-state-panel="populated"]:not(.on){display:none}

/* paywall */
.paywall{border:1px solid var(--gold);}
.paywall .doctrine{color:var(--color-text-muted);font-size:.875rem}

/* --- SaaS: auth gating, welcome, keys, locks --- */
/* protected pages stay veiled until the session is confirmed, so members' rooms
   never flash before the gate resolves */
body[data-protected="1"]:not(.authed) main{visibility:hidden}
/* ponytail: the white flash between signing in and the welcome was NOT a paint
   delay, it was this. main is hidden until boot() finishes its /api/auth/me call,
   and --color-bg is #ffffff, so the browser held pure white for the whole length
   of that request. One rule: while a seamless arrival is waiting, paint the room's
   own colour instead of white. Sampled from temple-interior-v2.webp, so the gap
   reads as the room arriving rather than as a flash. */
html.seamless-arrive, html.seamless-arrive body{background:#cfb799}
/* the onboarding screen: a floating card that greets a member once, just after
   they sign in (styled like the threshold card, over a dimmed lobby) */
/* The welcome. It used to be a narrow 34rem box of bordered text rows arriving
   after a white flash and a re-fade. It is now built in the same language as the
   threshold card it follows: one wide glass panel, words on the left, the four
   doors on the right as the actual door artwork, so the welcome shows the Temple
   rather than listing it. */
.onboard{position:fixed;inset:0;z-index:60;display:grid;place-items:safe center;overflow-y:auto;
  padding:1.2rem;background:rgb(30 26 18 / .34);
  backdrop-filter:blur(7px);-webkit-backdrop-filter:blur(7px);
  opacity:0;transition:opacity .3s var(--ease-veil)}
.onboard.on{opacity:1}
.onboard[hidden]{display:none}

.onboard-card{display:grid;grid-template-columns:minmax(0,1.05fr) minmax(23rem,26rem);
  width:100%;max-width:60rem;background:rgb(255 255 255 / .82);
  border:1px solid rgb(255 255 255 / .6);border-radius:var(--radius-md);
  box-shadow:var(--shadow-sanctum);overflow:hidden;
  backdrop-filter:blur(24px) saturate(1.45);-webkit-backdrop-filter:blur(24px) saturate(1.45);
  transform:translateY(10px) scale(.99);transition:transform .34s var(--ease-veil)}
.onboard.on .onboard-card{transform:none}

/* align-self:center shrank this panel to its text inside a row whose height is
   set by the four room cards beside it, so its cream gradient STOPPED where the
   words stopped: measured 120px of bare white above and 121px below at 1440x900.
   The two halves of the card stopped looking like one card, and it read as a
   rendering fault on the first screen a new member ever sees. The panel fills the
   row now and centres its own contents instead. (2026-07-29) */
.onboard-say{padding:clamp(1.7rem,3vw,2.6rem);
  display:flex;flex-direction:column;justify-content:center;
  background:linear-gradient(165deg, rgb(250 246 236 / .5), rgb(255 255 255 / .25))}
.onboard-kicker{font-size:.76rem;letter-spacing:.2em;text-transform:uppercase;color:#9a854a;margin:0 0 .5rem}
.onboard-card h2{font-family:var(--font-serif);font-weight:500;
  font-size:clamp(1.7rem,2.2vw + .5rem,2.3rem);line-height:1.15;margin:0 0 .9rem}
.onboard-lead{color:var(--color-text);line-height:1.68;font-size:1rem;margin:0 0 .85rem;max-width:34rem}
.onboard-lead-2{color:var(--color-text-muted)}
/* align-self, because .onboard-say is a flex column now and a stretched child
   would make this button the full width of the panel. */
.onboard-go{margin-top:.5rem;align-self:flex-start}

.onboard-doors{padding:clamp(1.4rem,2.4vw,2rem);border-left:1px solid rgb(217 182 98 / .4);
  background:rgb(255 255 255 / .3)}
.onboard-doorlead{font-size:.76rem;letter-spacing:.16em;text-transform:uppercase;
  color:var(--color-text-muted);margin:0 0 .9rem;text-align:center}
.onboard-rooms{display:grid;grid-template-columns:1fr 1fr;gap:.8rem;margin:0;padding:0;list-style:none}
.onboard-room{display:grid;gap:.15rem;text-decoration:none;color:var(--color-text);
  padding:.55rem .55rem .7rem;border:1px solid var(--color-border);border-radius:var(--radius-md);
  background:rgb(255 255 255 / .62);text-align:center;
  transition:border-color var(--t-move) var(--ease-veil),transform var(--t-move) var(--ease-veil),
             box-shadow var(--t-move) var(--ease-veil)}
.onboard-room img{width:100%;aspect-ratio:3 / 4;object-fit:cover;border-radius:var(--radius-sm);
  margin-bottom:.45rem;display:block}
.onboard-room .or-name{font-family:var(--font-serif);font-weight:500;font-size:1rem;line-height:1.2}
.onboard-room .or-line{font-size:.78rem;line-height:1.35;color:var(--color-text-muted)}
.onboard-room:hover,.onboard-room:focus-visible{border-color:var(--color-primary);
  transform:translateY(-2px);box-shadow:0 10px 26px rgb(120 90 30 / .16)}

/* ponytail: mobile is a later pass. This one line only stops it breaking. */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){ .onboard-card{grid-template-columns:1fr} .onboard-doors{border-left:0} }
@media (prefers-reduced-motion: reduce){.onboard,.onboard-card,.onboard-room{transition:none}}
/* sign-in / register feedback */
.auth-error{color:var(--color-error);text-align:center;font-size:.9rem;margin:0 0 var(--space-2);
  background:rgb(160 69 69 / .08);border:1px solid rgb(160 69 69 / .28);border-radius:var(--radius-sm);padding:.5rem .7rem}
/* The one saturated blue on a screen of cream, marble and gold: this checkbox was
   left at the browser default while .onboard-again four rules below already took
   accent-color. It sat directly above the primary button, 13x13, so it was both
   the loudest wrong colour and a tap target a third of the 44px floor. The label
   is the target now, not the box. (2026-07-29) */
.remember{display:flex;align-items:center;justify-content:center;gap:.55rem;font-size:.85rem;
  color:var(--color-text-muted);margin:0 0 var(--space-2);min-height:44px;cursor:pointer}
.remember input{width:18px;height:18px;min-height:0;margin:0;flex:none;
  accent-color:var(--color-primary);cursor:pointer}
/* the account page — 2026-08-04, rebuilt as a console: the identity chip and the nav
   rail on the left, one panel on the right. Operate discipline in the Temple's own
   materials, not a second visual world.
   ⚠ THE TABS ARE :target, NOT JAVASCRIPT. Every "active" state below is something CSS
   can already see, so no script sets a class and the twelve account.html#keys links
   already in the site keep working. If you add a panel you add exactly two things:
   a <section id="x" class="acct-panel"> and its id to the selector list marked ACTIVE. */
.acct{max-width:74rem;margin:0 auto;padding:var(--space-2) var(--space-2) var(--space-3);
  display:grid;grid-template-columns:18rem 1fr;column-gap:var(--space-3);align-items:start}
/* fixed, not clamped: this is product UI read at one distance, not a hero */
.acct-title{grid-column:1/-1;font-size:2.25rem;margin:var(--space-2) 0 var(--space-3)}

.acct-rail{position:sticky;top:76px;display:flex;flex-direction:column;gap:var(--space-2)}
.acct-me{background:var(--color-surface);border:1px solid var(--color-border);
  border-radius:var(--radius-md);box-shadow:var(--shadow-veil);padding:var(--space-3)}
/* the chip's three parts are the originals, unchanged: the gold disc, the eyebrow-cased
   label and the serif key name. They stack now instead of sitting in a row. */
/* 2026-08-04 (Director): the gold disc is gone. The mark for a key is a key, so this
   holds Thoth's Keys instead of a glyph on an orb. Kept at the disc's own 3.3rem so
   nothing below it moves, and the ring was cropped off the drawing for UI use: at
   53px the ring crowded the keys until they read as a smudge. */
.account-key-badge{width:3.3rem;height:3.3rem;display:grid;place-items:center;
  margin-bottom:.9rem}
.account-key-badge img{width:100%;height:100%;object-fit:contain;display:block}
/* the same mark on every Key card, so a Key looks like a key wherever you meet one */
.key-emblem{width:2.5rem;height:2.5rem;object-fit:contain;display:block;
  margin:0 auto .5rem;opacity:.92}
.account-key-label{font-size:.76rem;letter-spacing:.15em;text-transform:uppercase;
  color:var(--color-text-muted);margin:0}
.account-key-name{font-family:var(--font-serif);font-size:1.35rem;margin:.2rem 0 0;
  display:flex;align-items:center;gap:.55rem;flex-wrap:wrap}
.acct-who{margin:.75rem 0 0;font-size:.9rem;line-height:1.5;color:var(--color-text-muted);
  overflow-wrap:anywhere}
.acct-who strong{display:block;font-size:1rem;font-weight:500;color:var(--color-text)}
.acct-renew{margin:.6rem 0 0;color:var(--color-text-muted)}

.acct-nav{display:flex;flex-direction:column;gap:2px}
.acct-nav a{display:flex;align-items:center;justify-content:space-between;gap:.6rem;
  min-height:44px;padding:.6rem .9rem;border-radius:var(--radius-md);
  border:1px solid transparent;background:transparent;color:var(--color-text);
  text-decoration:none;font-size:.95rem;font-weight:500;
  transition:background var(--t-touch) ease,border-color var(--t-touch) ease}
@media (hover:hover){.acct-nav a:hover{background:var(--color-surface);border-color:var(--color-border)}}
.acct-chev{display:none;width:7px;height:12px;flex:none;color:var(--color-text-muted)}
/* ACTIVE — the selected tab wears the same gold-rimmed light as the bar's own pills.
   The last selector is the desktop default (no hash yet); it is scoped to the wide
   layout below, because on a phone the root shows the LIST and nothing is selected. */
.acct:has(#keys:target) [href="#keys"],
.acct:has(#billing:target) [href="#billing"],
.acct:has(#settings:target) [href="#settings"],
.acct:has(#help:target) [href="#help"]{
  background:linear-gradient(180deg,#fffdf6,rgb(255 250 236 / .75));
  border-color:rgb(217 182 98 / .75);color:#5a4a2a;
  box-shadow:inset 0 1px 0 rgb(255 255 255 / .9),0 2px 10px rgb(150 110 40 / .10)}

.acct-signout{appearance:none;width:100%;min-height:44px;padding:.6rem .9rem;
  border-radius:var(--radius-md);border:1px solid var(--color-border);background:transparent;
  color:var(--color-error);font-family:var(--font-sans);font-size:.95rem;font-weight:500;
  cursor:pointer;transition:border-color var(--t-touch) ease,background var(--t-touch) ease}
@media (hover:hover){.acct-signout:hover{border-color:var(--color-error);background:rgb(160 69 69 / .05)}}
.acct-signout:active{border-color:var(--color-error);background:rgb(160 69 69 / .05)}
.acct-signout-note{margin:.55rem 0 0;font-size:.8rem;line-height:1.5;color:var(--color-text-muted)}

.acct-panel{display:none;scroll-margin-top:76px}
.acct-panel:target{display:block}
/* a short arrival so switching reads as a move rather than a blink. 160ms is the
   product range: the panel is already there, this delays nothing. */
.acct-panel:target{animation:acctpanel 160ms var(--ease-veil)}
@keyframes acctpanel{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
@media (prefers-reduced-motion: reduce){.acct-panel:target{animation:none}}
.acct-panel h2{font-size:1.5rem;margin:0 0 var(--space-1)}
.acct-panel .card:last-child{margin-bottom:0}
/* settings prose stops at a readable measure. Only the Keys grid wants the whole
   column; a 1160px line of body copy is a worse read than a 570px one. */
#billing .card,#settings .card,#help .card{max-width:36rem}
.acct-lede{color:var(--color-text-muted);margin:0 0 var(--space-3);max-width:38rem}
.acct-note{color:var(--color-text-muted);margin:0 0 .8rem}
.acct-back{display:none}
.acct-rows .row:first-child{padding-top:0}
.acct-rows .row span:last-child{text-align:right;font-weight:500;color:var(--color-text)}
.acct-empty{margin:0;color:var(--color-text-muted)}

@media (min-width:821px) and (min-height:501px){
  .acct:not(:has(:target)) #keys{display:block}
  .acct:not(:has(:target)) [href="#keys"]{
    background:linear-gradient(180deg,#fffdf6,rgb(255 250 236 / .75));
    border-color:rgb(217 182 98 / .75);color:#5a4a2a;
    box-shadow:inset 0 1px 0 rgb(255 255 255 / .9),0 2px 10px rgb(150 110 40 / .10)}
}
/* the phone is an iOS settings list: the chip, then rows that push to a panel and
   come back. Same :target mechanism, no second markup. */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .acct{display:block;padding-bottom:var(--space-3)}
  .acct-title{font-size:1.9rem;margin:var(--space-1) 0 var(--space-2)}
  .acct-rail{position:static}
  .acct-chev{display:block}
  .acct:has(:target) .acct-rail,
  .acct:has(:target) .acct-title{display:none}
  .acct-nav{background:var(--color-surface);border:1px solid var(--color-border);
    border-radius:var(--radius-md);box-shadow:var(--shadow-veil);gap:0;overflow:hidden}
  .acct-nav a{min-height:52px;padding:.85rem var(--space-2);border-radius:0;
    border:0;border-bottom:1px solid var(--color-border)}
  .acct-nav a:last-child{border-bottom:0}
  .acct-nav a:hover{background:transparent}
  .acct-nav a:active{background:rgb(217 182 98 / .14)}
  /* the temple's gold-brown, not the global link periwinkle: this is the only
     accent-coloured thing on the page and it sat foreign against the ivory.
     Measures 4.95:1 on white, and the chevron carries the affordance. */
  .acct-back{display:inline-flex;align-items:center;gap:.45rem;min-height:44px;
    margin:0 0 .2rem -.35rem;padding:.3rem .35rem;color:#8a6b3a;
    text-decoration:none;font-size:.95rem;font-weight:500}
  .acct-back svg{width:7px;height:12px;flex:none}
}
/* the keys (membership) */
/* auto-fit, not a fixed 4 at 820px: the grid now sits in the account console's panel
   column, which is narrower than the page and changes width with the rail. It still
   reaches four across when there is room, and it no longer leaves an empty fourth
   column when only three keys are listed. */
/* ===== THE KEYS: ONE PANEL, DIVIDED ROWS ================================
   Director 2026-08-06, on the version before this one: "the layout, the yellow,
   like all of it just irks me. I think it could be better, more professional,
   more like a real account. I could just see the boxes and the words are all
   over the place and it doesn't really gel."

   Three faults, and all three were structural rather than decorative:

   1. EVERY SURFACE WAS THE SAME CREAM. The page, the rail, the nav, and each Key
      were all --color-surface, so nothing sat on anything and the eye had no
      hierarchy to hold. The rail keeps the cream; content is now white. That is
      the second neutral layer a real account UI uses to separate navigation from
      the thing being navigated.
   2. BOXES INSIDE BOXES. Three bordered, shadowed cards floating on a page made
      of bordered, shadowed cards. They are now ONE panel with hairline dividers,
      which is what a real account looks like: a list, not a gallery.
   3. THE WORDS WERE ALL OVER THE PLACE. Three columns each aligned to something
      different, and a state chip floating in the right column at whatever height
      the row happened to make. Everything now hangs off ONE top line: the mark,
      the name, and the action all start at the same y, and the chip rides the
      name it describes.

   The gold is spent only on the row you actually hold. */
.keys-grid{display:block;max-width:47rem;margin:0;
  background:var(--color-bg);border:1px solid var(--color-border);
  border-radius:var(--radius-md);box-shadow:var(--shadow-veil);overflow:hidden}
.key-card{background:none;border:0;border-radius:0;box-shadow:none;margin:0;
  padding:1.4rem var(--space-3);
  display:grid;grid-template-columns:2.6rem minmax(0,1fr) minmax(0,13rem);
  gap:0 1.15rem;align-items:start;text-align:left}
.key-card + .key-card{border-top:1px solid var(--color-border)}
/* The one you hold: a quiet tint and a gold edge on the leading margin. No ring,
   no glow. It is a fact about one row, not an advertisement. */
.key-card.current{background:#fdfaf1;box-shadow:inset 3px 0 0 var(--color-primary)}
.key-body{min-width:0}
.key-card h2{margin:0;font-size:1.35rem;line-height:1.25;
  display:flex;align-items:center;gap:.6rem;flex-wrap:wrap}
.key-chip{font-size:.75rem;padding:.1em .7em;flex:none}
.key-price{font-family:var(--font-sans);font-size:.95rem;color:var(--color-text-muted);
  margin:.2rem 0 0}
.key-blurb{color:var(--color-text-muted);margin:.5rem 0 0}
.key-unlocks{list-style:none;padding:0;margin:.6rem 0 0;font-size:.9rem;color:var(--color-text)}
.key-unlocks li{padding:.18rem 0 .18rem 1.3rem;position:relative}
/* A DRAWN MARK, NOT A GLYPH. This was U+2726, a dingbat standing in for an icon
   while every other mark on the site is drawn SVG at a consistent stroke. */
.key-unlocks li::before{content:"";position:absolute;left:0;top:.5em;
  width:.7rem;height:.7rem;transform:translateY(-50%);
  background:no-repeat center/contain
    url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="none" stroke="%23b9a06a" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="5.5" cy="5.5" r="3.2"/><path d="M7.9 7.9L14 14M11.4 11.4l-1.6 1.6M13 13l-1.4 1.4"/></svg>')}
/* THE METAL IS SAID BY THE MARK. One emblem file still, tinted three ways, so a
   Bronze Key no longer wears the same gold as a Gold Key on the same screen. */
.key-emblem{width:2.6rem;height:2.6rem;object-fit:contain;display:block;margin:0;
  opacity:.95;align-self:start}
.key-card--bronze .key-emblem{filter:sepia(.5) saturate(1.6) hue-rotate(-16deg) brightness(.78)}
.key-card--silver .key-emblem{filter:grayscale(1) brightness(1.06)}
.key-card--soon .key-emblem{opacity:.35;filter:grayscale(1)}
/* A Key that is not here yet has no unlocks to list. Its one line is a STATE, so
   it must not wear the key mark that means "this opens for you" on every other
   row, and its name must not compete with the Keys that do. */
.key-card--soon .key-unlocks{margin-top:.35rem}
.key-card--soon .key-unlocks li{padding-left:0;color:var(--color-text-muted);
  font-size:.72rem;letter-spacing:.16em;text-transform:uppercase}
.key-card--soon .key-unlocks li::before{display:none}
.key-card--soon h2{color:var(--color-text-muted)}
/* The action column starts on the same line as the name it belongs to, so the
   buttons stack down the page instead of drifting with each row's height. */
.key-action{display:flex;flex-direction:column;gap:.5rem;align-items:stretch;
  justify-self:end;align-self:start;width:100%}
.key-action:empty{display:none}
.key-soon{color:var(--color-text-muted);margin:.45rem 0 0;text-align:center}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .keys-grid{border-radius:var(--radius-md)}
  .key-card{grid-template-columns:2.2rem minmax(0,1fr);gap:0 .9rem;padding:1.15rem var(--space-2)}
  .key-emblem{width:2.2rem;height:2.2rem}
  .key-action{grid-column:1 / -1;margin-top:.9rem;justify-self:stretch}
}
.key-devcancel{margin-top:.5rem;font-size:.82rem;padding:.3rem .7rem}
.keys-demo-note{text-align:center;color:#9a854a;font-size:.85rem;letter-spacing:.02em;
  margin:0 auto var(--space-2);max-width:40rem;background:rgb(217 182 98 / .1);
  border:1px solid rgb(217 182 98 / .3);border-radius:var(--radius-sm);padding:.5rem .8rem}
/* the meditation sample lock is REMOVED (Director, 2026-07-29): it sat at the foot
   of the chamber and fell off the bottom of the screen. The playlist's locked rows
   carry the offer now. */

/* dialog */
dialog{border:none;border-radius:var(--radius-md);box-shadow:var(--shadow-sanctum);
  padding:var(--space-3);max-width:24rem;background:var(--color-bg);color:var(--color-text)}
dialog::backdrop{background:rgb(38 33 31 / .5)}




/* section reveals: enhance an already-visible default */
html.js .reveal{opacity:1;transform:none;transition:opacity 900ms var(--ease-veil),transform 900ms var(--ease-veil)}
html.js .reveal:not(.seen){opacity:.001;transform:translateY(14px)}
@media (prefers-reduced-motion: reduce){html.js .reveal:not(.seen){opacity:1;transform:none}}

/* footer + demo bar */
.porch-footer{padding:calc(var(--space-3)*2) var(--space-2);text-align:center;color:var(--color-text-muted);
  font-size:.875rem}
.porch-footer a{margin:0 var(--space-1)}


/* ---- Cinema on the wall (controls beneath; grows to centred cinema, then fullscreen) ---- */
.wall-projection{display:none}   /* replaced by .cinema */
.cinema{display:flex;flex-direction:column;z-index:6;opacity:1;pointer-events:auto;
  transition:opacity 1.1s var(--ease-veil)}
.cinema.mode-wall{position:absolute;left:31.2%;top:24.5%;width:37.6%}
.room-scene.dimmed .cinema.mode-wall{opacity:1;pointer-events:auto}
.cinema.mode-big,.cinema:fullscreen{opacity:1;pointer-events:auto}
.cinema-screen{position:relative;width:100%;aspect-ratio:16/9;background:#05040a;overflow:hidden;
  border-radius:7px;box-shadow:0 0 0 1px rgb(255 255 255 /.1),0 24px 60px rgb(0 0 0 /.6),0 0 70px rgb(120 96 210 /.28)}
.cinema-video{width:100%;height:100%;object-fit:contain;background:#05040a;display:block}
.cinema-video.is-audio{visibility:hidden}
.cinema-art{position:absolute;inset:0;background-size:cover;background-position:center;display:none}
.cinema-art.show{display:block}
.cinema-art::before{content:"";position:absolute;inset:0;background:radial-gradient(circle at 50% 42%,rgb(0 0 0 /.05),rgb(0 0 0 /.6))}
.cinema-art .aura{position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);z-index:1;width:76px;height:76px;
  border-radius:50%;background:rgb(255 255 255 /.16);display:grid;place-items:center;color:#fff;font-size:1.5rem;
  animation:aura-pulse 3.6s ease-in-out infinite}
@keyframes aura-pulse{0%,100%{box-shadow:0 0 0 0 rgb(255 255 255 /.16)}50%{box-shadow:0 0 0 24px rgb(255 255 255 /0)}}
.cinema-title{position:absolute;left:0;right:0;bottom:0;padding:.5rem .7rem;color:#fff;font-size:.82rem;
  background:linear-gradient(transparent,rgb(0 0 0 /.55));pointer-events:none}
.cinema-title:empty{display:none}
.cinema-bar{display:flex;align-items:center;gap:.5rem;margin-top:.5rem;padding:.45rem .6rem;color:#fff;
  background:rgb(20 16 30 /.72);-webkit-backdrop-filter:blur(12px);backdrop-filter:blur(12px);
  border:1px solid rgb(255 255 255 /.16);border-radius:11px}
.cinema-bar button{background:none;border:0;color:#fff;cursor:pointer;font-size:.95rem;line-height:1;padding:.3rem .4rem;border-radius:7px;transition:background .15s}
@media (hover:hover){.cinema-bar button:hover{background:rgb(255 255 255 /.18)}}
.cinema-bar button:active{background:rgb(255 255 255 /.18)}
.cinema-seek{flex:1;height:4px;accent-color:#fff;cursor:pointer;min-width:40px}
.cinema-time{font-size:.72rem;font-variant-numeric:tabular-nums;white-space:nowrap;color:rgb(255 255 255 /.85)}
.cinema.mode-big{position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:min(940px,88vw);z-index:65}
.cinema.mode-big .cinema-screen{box-shadow:0 0 0 1px rgb(255 255 255 /.12),0 30px 90px rgb(0 0 0 /.7),0 0 0 100vmax rgb(6 4 10 /.86)}
.cinema:fullscreen{width:100vw;height:100vh;left:0;top:0;transform:none;background:#000;justify-content:center;padding:0}
.cinema:fullscreen .cinema-screen{aspect-ratio:auto;flex:1;min-height:0;border-radius:0;box-shadow:none}
.cinema:fullscreen .cinema-bar{position:fixed;left:50%;bottom:3vh;transform:translateX(-50%);width:min(940px,92vw)}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .cinema.mode-wall{left:7vw;width:86vw;top:calc(var(--bar-h) + 4svh)}
  .cinema.mode-big{width:94vw}
}

/* ---- Liquid-glass playlist ---- */
.playlist.glass{position:fixed;left:max(3.4vw,22px);top:50%;transform:translateY(-50%);z-index:8;
  width:min(300px,82vw);max-height:80svh;overflow-y:auto;padding:.85rem;border-radius:20px;
  background:rgb(255 255 255 /.07);-webkit-backdrop-filter:blur(24px) saturate(1.5);backdrop-filter:blur(24px) saturate(1.5);
  border:1px solid rgb(255 255 255 /.22);
  box-shadow:0 22px 60px rgb(0 0 0 /.4),inset 0 1px 0 rgb(255 255 255 /.3),inset 0 0 40px rgb(255 255 255 /.04);
  scrollbar-width:thin}
.playlist.glass::-webkit-scrollbar{width:6px}
.playlist.glass::-webkit-scrollbar-thumb{background:rgb(255 255 255 /.25);border-radius:3px}
.pl-head{margin:.1rem .2rem .7rem;color:#fff;font-size:1rem;letter-spacing:.01em;text-shadow:0 1px 10px rgb(0 0 0 /.45)}
.pl-group{margin-bottom:.7rem}
.pl-tier{font-size:.64rem;text-transform:uppercase;letter-spacing:.14em;margin:.35rem .1rem;color:#fff;
  display:inline-block;padding:.16rem .55rem;border-radius:999px;background:rgb(255 255 255 /.14);border:1px solid rgb(255 255 255 /.2)}
.pl-tier--gold{color:#ffe7a6;background:rgb(255 208 120 /.16);border-color:rgb(255 208 120 /.34)}
.pl-tier--silver{color:#eef2f8;background:rgb(215 225 245 /.14);border-color:rgb(215 225 245 /.28)}
.pl-item{font-family:inherit;display:flex;align-items:center;gap:.6rem;width:100%;text-align:left;margin:.28rem 0;padding:.5rem .55rem;
  border-radius:13px;border:1px solid transparent;background:rgb(255 255 255 /.06);color:#fff;cursor:pointer;
  transition:background .15s,border-color .15s,transform .1s}
.pl-item:hover:not(.locked){background:rgb(255 255 255 /.17);border-color:rgb(255 255 255 /.24)}
.pl-item:active:not(.locked){transform:scale(.99)}
.pl-item.active{background:rgb(255 255 255 /.24);border-color:rgb(255 255 255 /.42)}
.pl-item.locked{opacity:.55;cursor:not-allowed}
.pl-play{width:26px;height:26px;flex:none;display:grid;place-items:center;border-radius:50%;
  background:rgb(255 255 255 /.2);font-size:.68rem}
.pl-meta{display:flex;flex-direction:column;min-width:0}
.pl-title{font-size:.85rem;line-height:1.25;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
.pl-sub{font-size:.66rem;color:rgb(255 255 255 /.72)}

/* ===== meditation room: stronger glass, closer panels, brighter tabs, right info panel ===== */
/* Desktop only. Unscoped, this re-supplied `left` and beat the mobile rule above,
   over-constraining the box so translateX(50%) pushed half the panel off-screen. */
@media (min-width:821px) and (min-height:501px){ .playlist.glass{left:max(5vw,30px)} }
.video-info.glass{position:fixed;right:max(5vw,30px);top:50%;transform:translateY(-50%);z-index:8;
  width:min(300px,82vw);max-height:80svh;overflow-y:auto;padding:.95rem 1rem;border-radius:20px;scrollbar-width:thin}
.video-info.glass::-webkit-scrollbar{width:6px}
.video-info.glass::-webkit-scrollbar-thumb{background:rgb(255 255 255 /.28);border-radius:3px}
.vi-head{margin:.1rem 0 .7rem;color:#fff;font-size:.66rem;text-transform:uppercase;letter-spacing:.14em;opacity:.85}
.vi-title{color:#fff;font-size:1.05rem;font-weight:600;line-height:1.3;margin-bottom:.55rem;text-shadow:0 1px 10px rgb(0 0 0 /.4)}
.vi-tier{margin-bottom:.75rem}
.vi-desc{color:rgb(255 255 255 /.9);font-size:.85rem;line-height:1.6;margin:0}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){.video-info.glass{display:none}}

/* video selection tabs — more visible */
.pl-title{font-weight:600}


/* Live Gatherings: the room heading, whatever key you hold. */
.circle-head{font-size:1.15rem;margin:0 0 .8rem}

/* 2026-08-12: the gathering cards, Kirra's reset. Each card leads with the
   region and the visitor's own hour (serif, like the map names) and the shared
   title drops to a gold kicker; the two dated lines under it are the region's
   wall time and the [your time] line. The stack is a flex column at every
   width now, so the mobile block below 821px only stacks the row. */
#zoom-live .gathering .g-what{display:flex;flex-direction:column;gap:.15rem}
#zoom-live .gathering .g-when{font-family:var(--font-serif);font-size:1.05rem;
  letter-spacing:.03em;color:var(--color-text)}
#zoom-live .gathering .g-title{font-weight:400;font-size:.72rem;letter-spacing:.22em;
  text-transform:uppercase;color:#8a7a4e}
#zoom-live .gathering .g-wall,#zoom-live .gathering .g-yours{color:var(--color-text-muted)}

/* ===========================================================================
   COURSES (2026-07-27)
   The Learn More pages carry long teaching documents, so they read as prose in
   a column rather than as cards. Everything is scoped under .course-doc so the
   generic element selectors cannot leak into the rest of the Temple.
   =========================================================================== */
.course-doc{max-width:44rem;margin:0 auto;padding:calc(56px + 6svh) 0 10svh;color:var(--color-text);
  position:relative;z-index:1}

/* THE COURSE ROOM, 2026-08-19 (Director: "that content page could be a lot better ...
   tie it into the feeling of the website without all the images ... maybe you could have
   its statue in the background ... for every course").
   The page stops being a document on white and becomes the room the four statues stand
   in: the temple's own cream ground, daylight falling from above, and THIS course's
   statue standing in the right gutter, cut from the Courses room render itself
   (art-source/courses/statues) so it is the same marble, the same light, the same
   object a member just walked up to. No other picture is added.
   The statue is `position:fixed`: it stands still while the teaching scrolls past it,
   which is what makes it read as a room rather than as an illustration in a page. It is
   never under the text at reading widths: the column is 44rem and the statue lives in
   the gutter beside it; below 1180px it fades as the gutter closes. */
body:has(.course-doc){background:#f6efdf}
body:has(.course-doc)::before{content:"";position:fixed;inset:0;pointer-events:none;z-index:0;
  background:radial-gradient(120% 70% at 50% -10%, rgb(255 252 244 / .95), rgb(255 250 238 / 0) 60%),
             linear-gradient(180deg, rgb(255 253 247 / .8), rgb(246 239 223 / 0) 34%)}
/* the statue is a RELIEF, not a photograph: art-source/courses/statues/*-relief.png keep
   only the modelling (luminance minus its own local average), so the marble wall it was
   cut from disappears and what is left reads as carving on this page's cream ground. */
.course-doc::before{content:"";position:fixed;z-index:0;pointer-events:none;
  right:max(0.5rem, calc(50vw - 31rem));bottom:0;width:20rem;height:min(82vh, 48rem);
  background-image:var(--statue);background-repeat:no-repeat;background-position:bottom right;
  background-size:contain;opacity:0;
  /* the side nearest the column dissolves, so nothing ever reads over the text edge */
  -webkit-mask-image:linear-gradient(90deg, rgb(0 0 0 / 0) 0, rgb(0 0 0 / 1) 34%);
  mask-image:linear-gradient(90deg, rgb(0 0 0 / 0) 0, rgb(0 0 0 / 1) 34%);
  animation:statue-rise 1.6s var(--ease-veil) .25s forwards}
@keyframes statue-rise{from{opacity:0;transform:translateY(14px)}to{opacity:.52;transform:none}}
@media (prefers-reduced-motion: reduce){
  .course-doc::before{animation:none;opacity:.52}}
@media (max-width:1180px){.course-doc::before{opacity:0;animation:statue-rise-faint 1.6s var(--ease-veil) .25s forwards;
  width:15rem;right:-2rem}}
@keyframes statue-rise-faint{from{opacity:0}to{opacity:.17}}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .course-doc::before{width:12rem;right:-3rem;height:56vh}}

/* the title block: the course's name carries the page, and a gold hairline closes it,
   the same hairline the rooms use under a plaque. */
.course-doc .kicker{font-size:.72rem;letter-spacing:.22em;text-transform:uppercase;
  color:#8a7a4e;margin:0 0 .7rem}
.course-doc h1{margin:0 0 .8rem;font-size:clamp(1.9rem,1.2rem + 2.4vw,2.7rem);line-height:1.14;
  letter-spacing:-.005em;text-wrap:balance}
.course-doc>.lede{font-size:1.14rem;line-height:1.62;color:#4b4460;max-width:34rem;
  padding-bottom:1.6rem;border-bottom:1px solid rgb(184 145 47 / .35)}
.course-doc h2{font-family:var(--font-serif);font-weight:500;font-size:1.42rem;line-height:1.25;
  margin:2.9rem 0 .6rem;text-wrap:balance}
.course-doc h3{font-family:var(--font-serif);font-weight:500;font-size:1.06rem;margin:1.8rem 0 .35rem}
.course-doc p{line-height:1.78;margin:0 0 1.05rem;max-width:36rem}
.course-doc .lede{font-size:1.05rem}
.course-doc ul{margin:0 0 1.3rem;padding-left:1.1rem;line-height:1.74}
.course-doc li{margin:0 0 .35rem}
.course-doc .teaching{font-style:italic;color:var(--color-text-muted)}
.course-doc .collection{margin:1.3rem 0}
.course-doc .collection h3{margin-top:1.3rem}
/* THE ENERGY ITSELF, 2026-08-19 (Director: "for all the energies in the courses ...
   go online on the Shopify and get them the images ... it would be good to show
   representation"). Each named Energy carries its own product photograph from the
   Energy Alchymie Happiness store (Storefront API, art-source/courses/energies), the
   white studio ground knocked out so the wand or disc sits on this page's cream rather
   than in a card. It is a specimen plate beside the teaching, never a shop tile, and it
   links to that Energy in the store. An Energy the store does not carry has no plate
   and the paragraph simply runs full width: an honest gap, never a stand-in. */
.course-doc .energy{margin:0 0 1.6rem;position:relative;min-height:4.4rem}
.course-doc .energy:has(.energy-plate){padding-left:6.2rem;min-height:6rem}
.course-doc .energy strong{display:block;margin-bottom:.15rem}
.course-doc .energy-plate{position:absolute;left:0;top:.1rem;width:4.4rem;height:5.9rem;
  display:grid;place-items:center;border-radius:2px;
  background:radial-gradient(60% 60% at 50% 55%, rgb(255 252 244 / .9), rgb(255 252 244 / 0) 72%);
  transition:transform .4s var(--ease-veil),filter .4s var(--ease-veil)}
.course-doc .energy-plate img{width:100%;height:100%;object-fit:contain;display:block}
@media (hover:hover){.course-doc .energy-plate:hover{transform:scale(1.06);filter:saturate(1.08)}}
.course-doc .energy-plate:focus-visible{outline:2px solid #8a6f2e;outline-offset:3px}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .course-doc .energy:has(.energy-plate){padding-left:0}
  .course-doc .energy-plate{position:static;width:3.4rem;height:4.6rem;margin:0 0 .55rem}}
.course-doc .practical{margin-top:2.8rem}

/* WHAT THE COURSE IS, AT A GLANCE. Kirra's own week list, her words untouched; it just
   stops being a bulleted list and becomes rows a reader can scan, each week separated by
   the same gold hairline the temple uses. Works for three weeks and for six. */
.course-doc .outline{list-style:none;margin:1.6rem 0 0;padding:0}
.course-doc .outline li{position:relative;margin:0;padding:.85rem 0 .85rem 1.5rem;
  border-bottom:1px solid rgb(184 145 47 / .22);line-height:1.62}
.course-doc .outline li:first-child{border-top:1px solid rgb(184 145 47 / .22)}
.course-doc .outline li::before{content:"";position:absolute;left:.25rem;top:1.45rem;
  width:6px;height:6px;transform:rotate(45deg);background:rgb(184 145 47 / .75)}
.course-doc .outline strong{font-family:var(--font-serif);font-weight:500}

/* A WEEK IS A NEW ROOM IN THE DOCUMENT. The hairline it used to carry read as a table
   divider; a gold rule that stops short, with the week's own name in serif under it,
   reads as a threshold. */
.course-doc .week{border-top:0;padding-top:2.2rem;margin-top:2.6rem;position:relative}
.course-doc .week::before{content:"";position:absolute;top:0;left:0;width:4.5rem;height:1px;
  background:linear-gradient(90deg, rgb(184 145 47 / .85), rgb(184 145 47 / 0))}
.course-doc .week:first-of-type{margin-top:2.2rem}
.course-doc .week .kicker{color:#8a7a4e;margin-bottom:.35rem}

/* The booking card (2026-08-13). It is a plain .card, so it inherits the Temple's
   surface, border and shadow and nothing new is invented. The only thing it needs
   is a width: the prose column is 44rem and a PayPal iframe told to fill that runs
   a pay button the whole width of a paragraph. 24rem holds the dates line on two
   lines and shrinks to the viewport on a phone, where the card is already inside
   the wrap's gutter, so there is nothing to overflow. */
.course-doc .book{max-width:26rem;margin-top:2rem;position:relative;overflow:hidden;
  background:rgb(255 252 245 / .92);border-color:rgb(184 145 47 / .38)}
.course-doc .book::before{content:"";position:absolute;top:0;left:0;right:0;height:2px;
  background:linear-gradient(90deg, rgb(184 145 47 / .9), rgb(184 145 47 / .15))}
.course-doc .book .dates{margin:0 0 1rem;font-family:var(--font-serif);font-size:1.06rem;
  line-height:1.6;color:var(--color-text)}
.course-doc .book .book-said{margin:0;max-width:none}
.paypal-book-slot{max-width:20rem}
/* The demo and the dormant buttons are ours, so they fill the slot rather than
   sitting at label width next to a full-width PayPal button. */
.paypal-book-slot>.btn{width:100%}
/* The Collections add-on picker (2026-08-17). Hidden until the backend lists a
   priced add-on for the course, so with none priced the card is exactly as above.
   Rows are 44px tall for the thumb; a press state instead of hover on touch. */
.course-doc .book .addons{margin:0 0 1rem;padding:0;border:0;min-width:0}
.course-doc .book .addons[hidden]{display:none}
.course-doc .book .addons legend{padding:0;margin:0 0 .35rem}
.addon{display:flex;align-items:center;gap:.7rem;min-height:44px;padding:.3rem 0;
  border-top:1px solid rgb(140 120 70 / .18);cursor:pointer;line-height:1.35}
.addon input{width:22px;height:22px;min-height:0;margin:0;flex:none;accent-color:#8a6f2e}
.addon input:focus-visible{outline:2px solid #8a6f2e;outline-offset:2px}
.addon-name{flex:1}
.addon-price,.addons-total strong{font-family:var(--font-serif);white-space:nowrap}
.addon-price{color:var(--color-text-muted)}
.course-doc .book .addons-total{display:flex;justify-content:space-between;align-items:baseline;gap:1rem;
  margin:0;padding-top:.6rem;border-top:1px solid rgb(140 120 70 / .28);max-width:none}
@media (hover:none){.addon:active{background:rgb(140 120 70 / .06)}}

/* The tell-me-when-it-opens capture. Hidden until "click here", per Kirra. */
.notify form{margin-top:.9rem;display:grid;gap:.6rem;max-width:22rem}
.notify form[hidden]{display:none}
.notify .field span{display:block;font-size:.8rem;margin-bottom:.25rem}
/* The global .field .err rule is display:none; the line above would unhide it,
   exactly as it does on the auth panel. Re-assert it, same fix as there. */
.notify .field .err{display:none}
.notify .field.invalid .err{display:block}
.notify input[type=email]{font:inherit;width:100%;padding:.55rem .7rem;border-radius:3px;
  border:1px solid rgb(140 120 70 / .35);background:rgb(255 255 255 / .6);color:var(--color-text)}
.notify .hp{position:absolute;left:-9999px;width:1px;height:1px;opacity:0}
.notify .notify-status{margin:.2rem 0 0;font-size:.85rem;color:var(--color-text-muted);min-height:1.2em}
.notify.done .notify-status{color:var(--color-text)}

/* The Courses room: each course is a pane on the wall. */
.course-card .weeks{list-style:none;padding:0;margin:.6rem 0 1rem;font-size:.9rem}
.course-card .weeks li{margin:0 0 .25rem;color:var(--color-text-muted)}
.course-card .dates{font-size:.85rem;color:var(--color-text-muted);margin:0 0 .9rem}

/* The music control. z-index 66 puts it above .veil-video (60) and #gold-flash
   (65) so it stays reachable while the dove plays. */
.music-toggle{position:fixed;right:1rem;bottom:1rem;z-index:66;width:44px;height:44px;
  display:grid;place-items:center;padding:0;cursor:pointer;
  background:rgb(255 255 255 / .7);border:1px solid var(--color-border);border-radius:50%;
  color:var(--color-text);backdrop-filter:blur(4px);-webkit-backdrop-filter:blur(4px);
  transition:box-shadow var(--t-move) var(--ease-veil)}
/* .music-toggle{display:grid} outranks the browser's own [hidden]{display:none},
   so the control ignored its hidden attribute and sat on screen from first paint,
   before there was any music to mute, and stayed after the music had gone. Every
   other togglable thing in this file needs the same line. */
.music-toggle[hidden]{display:none}
.music-toggle:hover,.music-toggle:focus-visible{box-shadow:0 0 24px rgb(231 207 141 / .6)}
.music-toggle svg{width:20px;height:20px;display:block;fill:none;stroke:currentColor;
  stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.music-toggle .off-slash{opacity:0}
.music-toggle.is-off .wave{opacity:0}
.music-toggle.is-off .off-slash{opacity:1}
@media (prefers-reduced-motion: reduce){.music-toggle{transition:none}}

/* The door titles are carved into the room artwork now, so the .plate text would
   duplicate them on screen. Hide it visually but keep it for screen readers and
   for the keyboard focus ring, which still needs something to sit on. */
.plate.visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
/* the same pattern, unscoped: for headings that belong to the document outline but
   must never paint over the room artwork. */
.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;
  overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}


/* ---- the threshold, mounted over the lobby -------------------------------
   The sign-in used to be its own page with its own sky, so signing in meant a
   navigation, and no amount of fade tuning hides a page change. The card now
   sits over the rotunda that is already on screen: it dissolves, and the room
   behind it was never touched. One scene, things arriving and leaving in it. */
.threshold-over{position:fixed;inset:0;z-index:58;display:grid;place-items:safe center;
  overflow-y:auto;padding:1.2rem;
  /* Warm, not a neutral dark scrim. The card is a translucent white, so a grey
     veil behind it turned the whole thing grey: it used to read gold because it
     sat on the pale sky. This keeps the rotunda's own light coming through. */
  background:rgb(96 74 38 / .20);
  backdrop-filter:blur(6px) saturate(1.15);-webkit-backdrop-filter:blur(6px) saturate(1.15)}
.threshold-over[hidden]{display:none}
/* the card is the scene's own furniture here, so it drops the full-page sky */
.threshold-over .book-room{min-height:0;background:none;padding:0;display:block;overflow:visible}
.threshold-over .v1-sky{display:none}
/* the rotunda is visible behind the card, but not reachable past it */
body.awaiting-threshold .arch,
body.awaiting-threshold .door-card,
body.awaiting-threshold .scene-doorlist{pointer-events:none}
body.awaiting-threshold .shell-bar{opacity:0;pointer-events:none}
/* The bar used to arrive with the page load that followed signing in, so it had
   html.seamless-arrive to breathe it in. There is no page load now: the class comes
   off in place, and without this the bar snapped on in one frame. */
.shell-bar{transition:opacity .9s var(--ease-veil)}

/* "do not show this again" */
.onboard-again{display:flex;align-items:center;justify-content:flex-start;gap:.55rem;
  font-size:.85rem;color:var(--color-text-muted);margin:.9rem 0 0;
  min-height:44px;align-self:flex-start;cursor:pointer}
.onboard-again input{width:20px;height:20px;accent-color:var(--color-primary);flex:none;cursor:pointer}


/* ---- leaving: signing out returns to the landing page ---------------------
   The old exit was .fade-out, a 900ms fade to opacity 0 that was cut off at
   400ms by the navigation, landing on the browser's white. So it read as a snap
   to white rather than as leaving. Now the room settles back and the warm light
   closes over it, the fade COMPLETES, and only then does the page change. */
html.signing-out, html.signing-out body{background:#cfb799}
html.signing-out .temple-scene,
html.signing-out main{
  animation:leaveroom 560ms var(--ease-veil) forwards}
@keyframes leaveroom{
  from{opacity:1;transform:none;filter:none}
  to{opacity:0;transform:scale(1.035);filter:blur(3px)}
}
/* the light closes over the room as it goes */
html.signing-out::after{content:"";position:fixed;inset:0;z-index:80;pointer-events:none;
  background:radial-gradient(ellipse 110% 100% at 50% 45%, #fffdf7, #fdf7ea 55%, #f2e6cd 100%);
  opacity:0;animation:leavelight 560ms var(--ease-veil) forwards}
@keyframes leavelight{from{opacity:0}to{opacity:1}}
html.signing-out .shell-bar,
html.signing-out .onboard,
html.signing-out .threshold-over{opacity:0;transition:opacity 240ms var(--ease-veil)}
@media (prefers-reduced-motion: reduce){
  html.signing-out .temple-scene,html.signing-out main,html.signing-out::after{animation:none}
}
/* arriving back on the porch, the light opens rather than snapping */
html.from-leaving .porch{animation:porchopen 700ms var(--ease-veil) 60ms backwards}
@keyframes porchopen{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}

/* ===========================================================================
   MEDITATION CHAMBER — the polish round (Director, 2026-07-29)
   Three asks, all geometry: the screen fills the bay BETWEEN THE PILLARS instead
   of hiding inside the gold wall panel; both glass panels are bigger and hold ONE
   fixed size whatever is in them; the Keys panel at the foot of the room is gone.
   Numbers are read off the artwork (assets/room-meditation-bg.webp?v=1785839481, 1536x1024):
   the pillar capitals stop at x=415 and start again at x=1120, so the clear bay is
   27% to 73% of the frame. The screen is 16:9 inside it, dropped to start at 19%
   and running down to ~58%, which is the wall panel's own lower rail.
   =========================================================================== */
@media (min-width:821px) and (min-height:501px){
  .cinema.mode-wall{left:27%;top:19%;width:46%}

  /* Both panels: one width, one height, set by the wall screen (js sets --pl-h).
     Fixed means fixed — a panel does not resize because a description is long or
     because a row was added. It scrolls inside itself instead. */
  .playlist.glass,.video-info.glass{
    width:clamp(240px,22vw,360px);
    height:var(--pl-h,420px);
    max-height:none;
    padding:1rem;
    border-radius:22px}
  .playlist.glass{left:max(2.2vw,20px)}
  .video-info.glass{right:max(2.2vw,20px)}

  /* ⚠ The desktop `transform` on these two panels is NOT here. alignPlaylist() in
     js/temple.js pins them to the screen's top with an INLINE style, which beats any
     rule in this file. The inwards tilt (2026-08-04) lives on that same line. */
  .pl-head{font-size:1.1rem;margin-bottom:.45rem}
  /* three shelves now stand in this panel (Free, Silver Key, Gold Key) and all three
     have to be visible without scrolling, or the keys they are selling are below the
     fold. The rows lose their slack, not their legibility. */
  .pl-group{margin-bottom:.3rem}
  .pl-tier{margin:.18rem .1rem}
  .pl-title{font-size:.9rem}
  .vi-title{font-size:1.12rem}
  .vi-desc{font-size:.88rem;line-height:1.65}
}

/* The locked rows: you can see there ARE more recordings, you cannot see what they
   are. Kirra's rule (2026-07-27) still holds — no unbuilt recording is named — so a
   locked row shows a lock and a soft outline where the title will be, and opens the
   Keys. It carries the offer the removed foot panel used to carry. */
.pl-item--ghost{background:rgb(255 255 255 /.045);border:1px dashed rgb(255 255 255 /.3);
  cursor:pointer;opacity:1;padding:.3rem .55rem;margin:.14rem 0}
.pl-item--ghost:hover{background:rgb(255 255 255 /.11);border-color:rgb(255 255 255 /.5)}
.pl-item--ghost .pl-play{background:rgb(255 255 255 /.16);color:rgb(255 255 255 /.7)}
.pl-ghost-bar{display:block;height:.62rem;border-radius:999px;
  background:linear-gradient(90deg,rgb(255 255 255 /.3),rgb(255 255 255 /.14));
  filter:blur(.6px)}
.pl-ghost-bar--sub{height:.42rem;margin-top:.34rem;width:52%;
  background:linear-gradient(90deg,rgb(255 255 255 /.2),rgb(255 255 255 /.08))}
.pl-item--ghost .pl-meta{flex:1;min-width:0}
.pl-ghost-note{margin:.3rem .2rem 0;font-size:.68rem;letter-spacing:.06em;
  color:rgb(255 255 255 /.68);text-shadow:0 1px 6px rgb(0 0 0 /.4)}
.pl-item--ghost .pl-play{width:22px;height:22px;font-size:.6rem}
.pl-item--ghost .pl-ghost-bar{height:.55rem}
.pl-item--ghost .pl-ghost-bar--sub{height:.36rem;margin-top:.28rem}


/* ===========================================================================
   WALKING THROUGH A DOOR — shortened and smoothed (Director, 2026-07-29)
   It was 2.15s of choreography before the page even changed, then a 1.4s dissolve
   on the far side: nearly three and a half seconds to cross one doorway. Same four
   beats, roughly half the time, and they now OVERLAP — the light is already rising
   while the walk finishes, so it reads as one movement rather than four steps.
   Timings live in two places by necessity: the durations here, the cues in
   temple.js. They are tuned as one; change both or neither.
   =========================================================================== */
.stage-ajar{transition:opacity .45s var(--ease-veil)}
.scene-zoom{transition:transform 1.15s cubic-bezier(.5,.03,.3,1)}
.scene-flash{transition:opacity .62s ease-in}
html.from-light::after{animation:lightdissolve 900ms var(--ease-veil) forwards}
@keyframes lightdissolve{0%{opacity:1}8%{opacity:1}100%{opacity:0}}

/* ============================================================================
   THE TOP BAR'S DROPDOWNS
   Two ways into the same nested rooms. Walking through the doors is the temple's
   way and nothing about it changes -- the tab itself is still a plain link that
   walks you into the room. This adds the other way for people who would rather
   read a list: Teaching and Practitioner carry their nested rooms in a panel.
   Above 900px only, because that is where the bar has room for it; below that
   the bar already scrolls and, at 700px, becomes the glass sheet.
   ========================================================================== */
.tab-group{position:relative;display:inline-flex;align-items:center;gap:.15rem}
/* The panel hangs out of the bar, so the tabs may not be a scroll box where it shows. */
@media (min-width:901px){.shell-tabs{overflow:visible}}

.tab-caret{display:inline-flex;align-items:center;justify-content:center;width:20px;height:44px;
  padding:0;border:0;background:none;cursor:pointer;color:var(--color-text);opacity:.55;
  transition:opacity .3s var(--ease-veil),color .3s var(--ease-veil)}
.tab-caret svg{width:9px;height:auto;display:block;
  transition:transform .38s var(--ease-veil)}
.tab-caret:hover,.tab-caret:focus-visible{opacity:1;color:var(--color-accent)}
.shell-bar.transparent .tab-caret{filter:drop-shadow(0 1px 8px rgb(255 255 255 / .9))}
.tab-group:hover .tab-caret,.tab-group.open .tab-caret{opacity:1}
.tab-group:hover .tab-caret svg,.tab-group.open .tab-caret svg{transform:rotate(180deg)}

.tab-menu{position:absolute;top:calc(100% - .15rem);left:-.85rem;z-index:60;
  min-width:14.5rem;max-width:min(20rem,calc(100vw - 2rem));
  display:flex;flex-direction:column;padding:.55rem .45rem;
  background:rgb(255 253 246 / .82);border:1px solid rgb(217 182 98 / .38);border-radius:14px;
  box-shadow:0 18px 46px rgb(90 74 42 / .18),0 0 40px rgb(255 244 210 / .35);
  backdrop-filter:blur(20px) saturate(1.4);-webkit-backdrop-filter:blur(20px) saturate(1.4);
  opacity:0;visibility:hidden;transform:translateY(-7px);pointer-events:none;
  transition:opacity .34s var(--ease-veil),transform .34s var(--ease-veil),visibility 0s linear .34s}
/* A bridge across the gap, so the pointer can travel from tab to panel. */
.tab-menu::before{content:"";position:absolute;left:0;right:0;top:-12px;height:12px}
/* Hover and focus open it with no script at all; the script adds the caret, the
   closing delay and Escape. Both routes land on the same open state. */
.tab-group:hover .tab-menu,.tab-group:focus-within .tab-menu,.tab-group.open .tab-menu{
  opacity:1;visibility:visible;transform:none;pointer-events:auto;transition-delay:0s}

/* The panel's own links: quiet sentence case, not another row of bar tabs. */
.shell-tabs .tab-menu a{display:flex;align-items:center;gap:.5rem;min-height:38px;
  padding:.34rem .7rem;border:0;border-radius:9px;white-space:normal;
  font-family:var(--font-serif);font-size:.82rem;letter-spacing:.02em;text-transform:none;
  color:#4a3f2a;text-shadow:none;text-decoration:none;
  transition:background .28s var(--ease-veil),color .28s var(--ease-veil)}
.shell-tabs .tab-menu a:hover,.shell-tabs .tab-menu a:focus-visible{
  background:rgb(255 244 210 / .72);color:#3a3040}
.shell-tabs .tab-menu a[aria-current="page"]{background:rgb(217 182 98 / .2);color:#3a3040}
.tab-menu a em{font-style:normal;font-size:.6rem;letter-spacing:.2em;text-transform:uppercase;
  color:rgb(90 74 42 / .55);margin-left:auto;padding-left:.5rem}

/* THE FIFTH BREAKPOINT IS GONE, DELETED RATHER THAN RENAMED (2026-08-10).
   `@media (max-width:900px){.tab-caret{display:none}.tab-menu{display:none}}`
   stood here and could never fire: both elements live inside `.shell-tabs`, which
   is `display:none` from 1024 down, and the rule never applied above 900. So it
   hid two things that were already gone. Measured display:none at 1025 and 1440
   with and without it. One fewer number for the next person to hold in their head. */
@media (prefers-reduced-motion: reduce){
  .tab-menu{transition:opacity .01s,visibility 0s}
  .tab-caret svg{transition:none}
}

/* ---- the glass sheet: the same nesting, tapped open ---- */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  /* Expanded groups can outgrow the sheet, so it scrolls -- and `safe` keeps the
     first row reachable instead of clipped above the top edge when it does. */
  /* The bottom inset belongs on THIS shorthand and nowhere else: it comes after the
     1024 block in source order, so a padding-bottom written up there would be wiped
     by this one line. `safe center` is already right and is left alone. */
  .mobile-menu{overflow-y:auto;padding:1.5rem 0 calc(1.5rem + env(safe-area-inset-bottom));
    justify-content:flex-start;justify-content:safe center}
  .mobile-menu>*{flex:none}
  .m-group{display:flex;flex-direction:column;align-items:center;width:100%}
  .m-row{position:relative;display:flex;align-items:center;justify-content:center}
  .m-row>a{font-family:var(--font-serif);font-size:1.3rem;letter-spacing:.14em;text-transform:uppercase;
    color:var(--color-text);text-decoration:none;padding:.4rem;min-height:44px;
    display:flex;align-items:center}
  /* Out of the flow, so the room's name sits on the same centre line as the rooms
     above and below it instead of being shouldered left by its own caret. */
  .m-caret{position:absolute;left:100%;top:50%;transform:translateY(-50%);
    display:inline-flex;align-items:center;justify-content:center;width:44px;height:44px;
    padding:0;border:0;background:none;cursor:pointer;color:var(--color-text);opacity:.6}
  .m-caret svg{width:12px;height:auto;display:block;transition:transform .38s var(--ease-veil)}
  .m-caret[aria-expanded="true"]{opacity:1}
  .m-caret[aria-expanded="true"] svg{transform:rotate(180deg)}
  .m-sub{display:flex;flex-direction:column;align-items:center;gap:.1rem;
    margin:.15rem 0 .5rem;padding:.4rem 0;width:min(20rem,84%);
    border-top:1px solid rgb(217 182 98 / .3);border-bottom:1px solid rgb(217 182 98 / .3)}
  .m-sub[hidden]{display:none}
  .m-sub>a{font-family:var(--font-serif);font-size:.95rem;letter-spacing:.04em;
    color:rgb(63 58 85 / .85);text-decoration:none;min-height:44px;padding:.2rem 1rem;
    display:flex;align-items:center;gap:.5rem;text-align:center}
  .m-sub>a[aria-current="page"]{color:#3a3040}
  .m-sub>a em{font-style:normal;font-size:.58rem;letter-spacing:.2em;text-transform:uppercase;
    color:rgb(90 74 42 / .55)}
}

/* ===========================================================================
   THE BAR CLEARS FOR THE FILM (Director, 2026-07-29)
   The top bar carries a veil of drifting cloud so it melts into a bright temple.
   When a recording plays the room crossfades to its dimmed twin, and that cloud
   band was the one bright thing left on a darkened screen. It now dissolves with
   the room, on the same slow breath, and comes back when the film stops.
   The bar does not merely lose its backdrop: with the veil gone the wall behind
   is warm and dark, so the whole bar changes register to light-on-dark. Clearing
   the clouds without this would leave the navigation unreadable.
   =========================================================================== */
.shell-bar::before,.shell-bar::after,
.shell-bar .shell-tabs a,.shell-bar .brand-logo img,.shell-bar .tab-caret,
.shell-bar .shell-actions .btn,.shell-bar .menu-btn span{
  transition:opacity 1.8s var(--ease-veil),color 1.8s var(--ease-veil),
    background-color 1.8s var(--ease-veil),border-color 1.8s var(--ease-veil),
    filter 1.8s var(--ease-veil),text-shadow 1.8s var(--ease-veil)}
body:has(.room-scene.dimmed) .shell-bar::before,
body:has(.room-scene.dimmed) .shell-bar::after{opacity:0}
body:has(.room-scene.dimmed) .shell-bar .shell-tabs a{
  color:rgb(255 250 240 / .84);text-shadow:0 1px 12px rgb(0 0 0 / .6)}
/* the submenu carets draw in currentColor and wear a white glow for the bright bar;
   on the dark one that glow is what makes them vanish */
body:has(.room-scene.dimmed) .shell-bar .tab-caret{
  color:rgb(255 250 240 / .84);filter:drop-shadow(0 1px 8px rgb(0 0 0 / .6))}
body:has(.room-scene.dimmed) .shell-bar .shell-tabs a.active{color:#f4e3b6}
body:has(.room-scene.dimmed) .shell-bar .brand-logo img{filter:brightness(0) invert(1);opacity:.82}
body:has(.room-scene.dimmed) .shell-bar .shell-actions .btn{
  color:rgb(255 250 240 / .9);border-color:rgb(217 182 98 / .5);
  background:linear-gradient(180deg,rgb(255 255 255 / .14),rgb(255 250 236 / .06));
  box-shadow:inset 0 1px 0 rgb(255 255 255 / .22),0 2px 12px rgb(0 0 0 / .25)}
body:has(.room-scene.dimmed) .shell-bar .menu-btn span{background:rgb(255 250 240 / .88)}

/* ================= The Meditation Chamber on a phone =================
   Desktop is its own thing: a cinema hung on the wall with a glass shelf floating
   beside it. That is a LANDSCAPE composition, and portrait was being handed the same
   three objects stacked and shrunk — which is why a third of the screen was empty
   wall while the Gold Key shelf sat one pixel below the fold, unreachable.

   The phone gets its own design, built out of cards (Director, 2026-07-29). The room
   artwork behind them is a placeholder and carries nothing, so the cards are opaque
   ivory rather than glass over a picture: it also fixes the contrast, since the old
   shelf set white text on a pale gold wall.

   One practice you can start, and an honest account of what the keys hold. Nothing
   scrolls, nothing is cut off. */
.mroom{display:none}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  /* the artwork stops being information and becomes atmosphere */
  .room-scene:has(.mroom) .scene-frame::after{content:"";position:absolute;inset:0;
    background:linear-gradient(180deg, rgb(250 246 237 / .72), rgb(250 246 237 / .93) 46%, rgb(248 244 236 / .97));
    pointer-events:none}
  /* the shelf and the info panel are appended to <body>, not to the scene, so they
     have to be hidden from the body — a scene-scoped selector never reaches them */
  body:has(.mroom) .wall-deck,
  body:has(.mroom) .deck-nav,
  body:has(.mroom) .deck-hint,
  body:has(.mroom) .playlist,
  body:has(.mroom) .video-info{display:none!important}

  /* the whole screen has to close inside one viewport: raising every control to the
     44pt iOS minimum cost 7px more than there was, so the rhythm gives it back */
  .mroom{display:flex;flex-direction:column;gap:.86rem;position:relative;z-index:4;
    padding:calc(var(--bar-h) + 1.4svh) 18px 2svh;min-height:100svh}
  .mroom-head h1{margin:0;font-family:var(--font-serif);font-weight:500;
    font-size:1.62rem;letter-spacing:.02em;color:var(--color-text);text-wrap:balance}
  .mroom-head p{margin:.3rem 0 0;font-size:.88rem;line-height:1.5;color:#575072;max-width:34ch}

  /* --- the practice itself. This card IS the player: no overlay, no second screen,
         no navigation. The film plays where the poster was. --- */
  .practice{background:var(--color-surface);border:1px solid rgb(217 182 98 / .42);
    border-radius:16px;overflow:hidden;box-shadow:0 10px 34px rgb(63 58 85 / .11)}
  .practice .cinema{position:static;left:auto;top:auto;width:auto;margin:0}
  .practice .cinema-screen{border-radius:0;box-shadow:none;background:#141119}
  .practice .cinema-title{display:none}          /* the card names it, in serif, below */
  /* the control strip belongs to the temple, not to a media player: ivory, gold, ink */
  .practice .cinema-bar{margin:0;border-radius:0;border:0;border-bottom:1px solid rgb(217 182 98 / .3);
    background:rgb(255 253 246 / .96);backdrop-filter:none;-webkit-backdrop-filter:none;
    color:var(--color-text);gap:.55rem;padding:.5rem .7rem}
  .practice .cinema-bar button{color:#5a4a2a;min-width:38px;min-height:38px;font-size:1rem;
    display:grid;place-items:center;border-radius:50%}
  .practice .cinema-bar button:active{background:rgb(217 182 98 / .18)}
  @media (hover:hover){.practice .cinema-bar button:hover{background:rgb(217 182 98 / .18)}}
  .practice .cinema-seek{accent-color:#c9a94e;height:3px}
  .practice .cinema-time{color:#575072;font-size:.72rem}
  .practice-body{padding:.85rem 1rem 1.05rem}
  .practice-title{margin:0;font-family:var(--font-serif);font-weight:500;font-size:1.02rem;
    line-height:1.32;color:var(--color-text);text-wrap:pretty}
  .practice-desc{margin:.45rem 0 0;font-size:.83rem;line-height:1.6;color:#575072;
    display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden}
  .practice-desc.open{-webkit-line-clamp:unset}
  .practice-more{margin:.5rem 0 0;padding:0;border:0;background:none;color:#8a6b28;
    font-family:var(--font-sans);font-size:.74rem;letter-spacing:.14em;text-transform:uppercase;
    cursor:pointer;min-height:44px}

  /* --- what the keys hold. Two rows, not six blank pills. The count is honest and
         the titles stay unsaid (Kirra 2026-07-27: hide what they entail). --- */
  .keyrow{display:flex;align-items:center;gap:.9rem;text-decoration:none;
    padding:.85rem 1rem;border-radius:14px;min-height:60px;
    border:1px solid rgb(217 182 98 / .34);background:rgb(255 253 246 / .82);
    transition:transform var(--t-touch) var(--ease-veil),box-shadow var(--t-move) var(--ease-veil)}
  .keyrow:active{transform:scale(.988)}
  .keyrow-name{font-family:var(--font-serif);font-size:.98rem;color:var(--color-text);line-height:1.2}
  .keyrow-count{display:block;font-family:var(--font-sans);font-size:.75rem;color:#575072;margin-top:.15rem}
  .keyrow-text{flex:1;min-width:0}
  .keyrow-go{flex:none;font-size:1.25rem;color:#8a6b28;line-height:1}
  .mroom-foot{margin:.1rem 0 0;font-size:.76rem;line-height:1.5;color:#5f5980;text-align:center}
  .mroom-foot a{color:#8a6b28}

  /* the room settles rather than performs: one rise, staggered, over an already-visible
     default so nothing depends on a transition firing */
  .mroom>*{animation:mroom-rise 520ms var(--ease-veil) backwards}
  .mroom>*:nth-child(2){animation-delay:60ms}
  .mroom>*:nth-child(3){animation-delay:120ms}
  .mroom>*:nth-child(4){animation-delay:170ms}
  .mroom>*:nth-child(5){animation-delay:210ms}
  @keyframes mroom-rise{from{opacity:0;transform:translateY(10px)}to{opacity:1;transform:none}}
}
@media (max-width:820px) and (prefers-reduced-motion:reduce),(max-height:500px) and (orientation:landscape) and (prefers-reduced-motion:reduce){
  .mroom>*{animation:none}
  .practice .cinema-bar button{transition:none}
}

/* ===========================================================================
   THE BAR'S CLOUD VEIL: CLOUDS OFF, BLUR KEPT (Director, 2026-07-29)
   First order: hide the cloud in the top toolbar everywhere, "I might turn it back
   on". Then: the blur should have stayed. So the veil keeps its shape and its soft
   lower edge and loses only the two cloud photographs and their 40s breath. What is
   left is frosted glass: a light blur, a whisper of warm white so the frost reads on
   flat marble, and the same mask, so there is still no line across the artwork.
   It is one switch: DELETE THIS BLOCK and the clouds come back exactly as they were,
   drift and breath included. Nothing above it was changed, including the dimmed-room
   rule that fades this veil out for a film, which still works because the veil still
   exists.
   =========================================================================== */
.shell-bar::before{
  background:linear-gradient(180deg, rgb(255 253 248 / .2), rgb(255 253 248 / .07) 60%, rgb(255 253 248 / 0));
  backdrop-filter:blur(10px) saturate(1.04) brightness(1.02);
  -webkit-backdrop-filter:blur(10px) saturate(1.04) brightness(1.02);
  animation:none}
.shell-bar::after{display:none}   /* the hairline was the veil's edge; the frost has none */

/* A confirmation has to LOOK like one (2026-08-25). Director: "the register my interest
   field still does nothing" — it did, but it said so in one thin line of body text under
   a form whose button had just vanished, which on a phone reads as nothing at all. */
.notify-status--on{display:block;margin-top:.6rem;padding:.7rem .9rem;
  border:1px solid rgb(180 142 58 / .38);border-radius:.7rem;
  background:rgb(247 240 224 / .72);color:#3f3757;font-size:.86rem;line-height:1.45}

/* ⚠ A CARD OVER A CARD: THE JOIN POPUP COULD NOT BE TAPPED (2026-08-25).
   Director: "in the Zoom room, press Join details, and the Open Zoom and Close buttons
   do nothing. It's like the whole thing freezes."
   Frozen is exactly right, and it is not the dialog: the handlers fire perfectly when
   the button is clicked from script, so nothing is broken about it. It is that a REAL
   TAP never reaches it. `#join-dialog` is opened from inside an open region card, and
   that card's `.exhibit-veil` is a full-screen overlay at z-index 40 with pointer
   events on. Measured: WebKit reports "<div class='exhibit-veil on'> intercepts
   pointer events" on the Close button. The dialog is modal and in the top layer, which
   in principle out-ranks any z-index, and in practice here it did not.
   So the veil stops taking taps for as long as the dialog is open, and the dialog is
   given a z-index that beats it outright rather than relying on the top layer to do it.
   Both call sites are covered by this, the region card and the room's own list, without
   either of them knowing about the other. */
.join-pop{position:fixed;inset:0;z-index:90;display:grid;place-items:center;padding:1rem;
  background:rgb(30 26 16 / .5);opacity:0;transition:opacity .24s var(--ease-veil,ease)}
.join-pop.on{opacity:1}
.join-pop[hidden]{display:none}
.join-pop .join-box{width:min(92vw,26rem);padding:1.15rem 1.25rem 1rem;
  background:var(--color-surface,#faf6ee);border:1px solid rgb(217 182 98 / .5);
  border-radius:var(--radius-md,14px);box-shadow:0 24px 70px rgb(0 0 0 / .4)}
.join-pop .join-box h3{margin:0 0 .5rem}
.join-pop .join-box p{margin:0 0 .7rem}
.join-pop .join-box p:last-child{margin:0;display:flex;gap:.5rem;flex-wrap:wrap}
.join-pop .join-box .btn{min-height:44px;display:inline-flex;align-items:center}
/* AND THE CARD UNDERNEATH STANDS DOWN. The popup opens from inside an open region card,
   and that card's veil is a full-screen overlay: measured in WebKit, it was the thing
   swallowing every tap meant for Open Zoom and Close. A higher z-index alone did not
   settle it, so the veil simply stops taking taps for as long as the popup is up. */
body:has(#join-dialog:not([hidden])) .exhibit-veil{pointer-events:none}

/* ⚠ NO BACKDROP-FILTER ON A PAGE THAT SCROLLS (2026-08-25, third pass, and this one
   is a MEASURED cause rather than another guess at one).
   Director, with a photograph of it: the bar painted three quarters of the way down a
   course page, its frost included — the copy behind it ("Cost: $99...") is visibly
   blurred by it, which is what proves the element itself was composited there rather
   than merely drawn in the wrong order. Only on iOS, and only on the pages that
   scroll. That pairing is the well-known WebKit fault: a `position:fixed` element
   whose paint depends on the pixels BEHIND it (`-webkit-backdrop-filter`) has to be
   re-composited against a moving backdrop on every frame of a scroll, and iOS
   sometimes keeps it at the offset it was last composited at.
   Two earlier attempts treated the symptom — a single deferred scroll, then asking
   for a compositing layer with `backface-visibility` — and the Director reports it
   still moving, so the cause is removed instead of managed: where a page scrolls,
   the bar is PAINTED, not frosted. There is nothing left to sample and nothing to
   re-composite, so the fault cannot occur.
   WHERE. A room is one fixed 100svh scene and does not scroll, and the frost over its
   artwork is the whole point of it, so a room keeps it: the fault has never been
   reported in one. `.temple-scene` is exactly the room test (a course page has none,
   a room has one). The painted band uses the page's own cream and keeps the same soft
   lower mask, so the bar reads as it always did on these pages, where there is no
   artwork behind it for a frost to reveal anyway. */
body:not(:has(.temple-scene)) .shell-bar::before{
  backdrop-filter:none;-webkit-backdrop-filter:none;
  /* the MASK has to go with it (red team, 2026-08-25): a masked pseudo-element on a
     fixed parent still gets its own composited layer on iOS -- and this one is 34px
     TALLER than its parent box -- so taking only the backdrop-filter away left the
     stale-offset failure mode exactly where it was. The painted band fades out on its
     own gradient, so nothing is lost by dropping the mask here. */
  -webkit-mask-image:none;mask-image:none;inset:0;
  background:linear-gradient(180deg,
    rgb(250 246 237 / .98), rgb(250 246 237 / .92) 58%, rgb(250 246 237 / 0))}
/* The tabs already carry their own white glow for the transparent bar; the wordmark
   did not, because the veil used to sit behind it. On the lobby it now lands on the
   hanging vines, so it gets the same glow rather than the band back. */
.shell-bar .brand-logo img{filter:drop-shadow(0 1px 3px rgb(255 255 255 / .95))
  drop-shadow(0 0 12px rgb(255 255 255 / .85))}

@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  /* the film fills its frame. The master is 864x496, a hair wider than 16:9, so
     `contain` left dark rails down both edges of an otherwise ivory card. */
  .practice .cinema-video{object-fit:cover;background:#141119}
  /* Starting the practice is the only thing this room is for, so it is the whole
     picture, not a 38px glyph in a strip. Tap the film to start it, tap it to stop.
     The seal fades out once it is playing and comes back the moment it is not. */
  .practice-tap{position:absolute;inset:0;z-index:3;display:grid;place-items:center;
    border:0;background:none;cursor:pointer;padding:0;
    -webkit-tap-highlight-color:transparent}
  .practice-tap .pt-seal{width:74px;height:74px;border-radius:50%;display:grid;place-items:center;
    font-size:1.45rem;color:#4a3a1c;padding-left:5px;
    background:rgb(255 253 246 / .9);border:1px solid rgb(217 182 98 / .7);
    box-shadow:0 6px 26px rgb(0 0 0 / .3), 0 0 40px rgb(255 244 210 / .35);
    /* 2026-08-25, Director: "the transitions between play and pause need a fade
       transition so it's not so hard stop." The seal is the thing that appears and
       disappears at every tap, and .38s reads as a snap next to the room's own 900ms
       breath. Both now move on the same slow ease, so the seal, the frame's light and
       the transport all settle together instead of one flicking while the rest drifts. */
    transition:opacity .9s var(--ease-veil),transform .9s var(--ease-veil)}
  .practice-tap.playing .pt-seal{opacity:0;transform:scale(.82)}
  .practice-tap:active .pt-seal{transform:scale(.94)}
  /* while it plays the picture belongs to the practice, so the frame quietens */
  /* 2026-08-25: this lit the bar white while playing, which is the opposite of the clean
     card the Director asked for. The dimmed block below now owns the playing look. */
  .practice:has(.practice-tap.playing) .cinema-bar{background:transparent}
  /* the transport's own colours ride the same breath rather than stepping */
  .practice .cinema-bar,.practice .cinema-bar button{
    transition:background .9s var(--ease-veil),color .9s var(--ease-veil),
      border-color .9s var(--ease-veil)}
}
@media (max-width:820px) and (prefers-reduced-motion:reduce),(max-height:500px) and (orientation:landscape) and (prefers-reduced-motion:reduce){
  .practice-tap .pt-seal{transition:none}
}

/* A film plus three cards genuinely does not fit in 568px of height, so a short phone
   scrolls rather than gets crushed — but it should be a short scroll, not a long one.
   Everything stays reachable either way, which is the part that was broken before. */
@media (max-width:820px) and (max-height:700px),(max-height:500px) and (orientation:landscape){
  .mroom{gap:.7rem;padding:calc(var(--bar-h) + 1.6svh) 14px 2svh}
  .mroom-head h1{font-size:1.36rem}
  .mroom-head p{font-size:.82rem}
  .practice-body{padding:.7rem .85rem .85rem}
  .practice-desc{-webkit-line-clamp:2}
  .keyrow{min-height:54px;padding:.7rem .85rem}
  .practice-tap .pt-seal{width:62px;height:62px;font-size:1.2rem}
}

/* ============ Native-app polish (Director, 2026-07-29: build it like an iOS app) ============
   Not "make it responsive" — make it feel like it shipped. Safe areas, 44pt targets,
   press states instead of hover, and a real now-playing state. */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  /* the phone's own edges. Without these a bottom-anchored plate sits under the
     home indicator on every notched iPhone. */
  /* ⚠ THE +8px IS ARITHMETIC, NOT TASTE. This room is designed to close inside one
     screen and it does — until the notch is real. Measured at 390x844: with env()=0
     scrollHeight === clientHeight === 844, exactly the intent. With 47/34 insets live
     the padding grows to 118.8/54.3, scrollHeight becomes 874, and the last control
     ("Body Puja Guided Meditation") comes to rest at y=847.2, 3.2px below the fold,
     and even scrolled to the very end its bottom is 817.2 — 7.2px inside the 34px
     home-indicator band, with nowhere left to scroll. The short-phone compression at
     `and (max-height:700px)` cannot save it because 844 > 700. 8px takes max scrollTop
     from 30 to 38 and puts that bottom at 809.2 against the 810 threshold: clear by
     0.8px, with 38px of scroll still reading as a settle rather than a page. */
  .mroom{padding-bottom:calc(2.4svh + env(safe-area-inset-bottom) + 8px);
    padding-left:calc(18px + env(safe-area-inset-left));
    padding-right:calc(18px + env(safe-area-inset-right))}
  .pan-plate{bottom:calc(3.2svh + env(safe-area-inset-bottom))}
  .deck-cue--pan{bottom:calc(3.2svh + env(safe-area-inset-bottom) + 62px)}

  /* 44pt is the iOS minimum, not the 38 a web audit will let through */
  .practice .cinema-bar{padding:.4rem .6rem}
  .practice .cinema-bar button{min-width:44px;min-height:44px}
  .keyrow{min-height:64px}

  /* a phone has no hover. It has a press. */
  .mroom a,.mroom button,.practice .cinema-bar button,.pan-plate,.arch{
    -webkit-tap-highlight-color:transparent}
  .practice .cinema-bar button:hover{background:none}
  .practice .cinema-bar button:active{background:rgb(217 182 98 / .22);transform:scale(.94)}
  .practice-more:active{opacity:.55}

  /* ---- now playing: the chamber dims around the practice ----
     The room already dims upstairs on the wall; on the phone the dim was invisible,
     because the ivory scrim that turns the placeholder artwork into atmosphere sat
     ON TOP of the dimmed plate. So the whole screen stayed bright through a
     meditation. Now the scrim itself is what darkens, and everything except the
     practice recedes — the app goes into a now-playing state instead of leaving all
     its furniture lit. Nothing moves: only light changes, so there is no jump. */
  .mroom-head,.keyrow,.mroom-foot{transition:opacity 1.6s var(--ease-veil)}
  .room-scene:has(.mroom) .scene-frame::after{transition:background 2.2s var(--ease-veil)}
  .room-scene.dimmed:has(.mroom) .scene-frame::after{
    background:linear-gradient(180deg, rgb(30 23 40 / .84), rgb(19 14 27 / .94) 55%, rgb(14 10 21 / .96))}
  .room-scene.dimmed .mroom-head,
  .room-scene.dimmed .keyrow,
  .room-scene.dimmed .mroom-foot{opacity:0;pointer-events:none}

  /* ⚠ THE ROW IS NOT THE CAPSULE (2026-08-25). Director: "once you start playing the
     video you shouldn't see anything else, like the Silver and Gold Key capsules,
     buttons, outlines. It should just be a clean video card."
     The rule above faded `.keyrow`, which is the TEXT inside the capsule. `.keyset` is
     the capsule itself and it carries `border:1px solid` and a radius, so what was
     left on a darkened screen was two empty outlined boxes where the keys had been —
     exactly what he is describing. The capsule goes, not its contents. */
  /* ⚠ THE CARD IS ONE THING, AND THE ROOM IS THE OTHER (2026-08-25, revised the same
     day). First pass read "it should just be a clean video card" as: hide everything
     but the picture, the practice's own title and description included. Director, on
     seeing it: "when you press play you lose the description card to the darkness; it
     should also stay if it's part of the video."
     So the line is not around the picture, it is around the CARD. What recedes is the
     ROOM -- the key capsules, the room's heading, its footnote. What stays is the
     practice: film, transport, title, description, whole and lit, because that is the
     thing being played. */
  .keyset,.practice,.practice .cinema-bar{
    transition:opacity 1.6s var(--ease-veil), background 1.6s var(--ease-veil),
      border-color 1.6s var(--ease-veil), box-shadow 1.6s var(--ease-veil)}
  .room-scene.dimmed .keyset{opacity:0;pointer-events:none}
  .room-scene.dimmed .practice{box-shadow:0 22px 70px rgb(0 0 0 / .55), 0 0 90px rgb(255 236 190 / .1)}
  /* the header bar stops competing with the practice for the eye. It has to be
     reached from the body: the bar is a sibling BEFORE the scene, and CSS has no
     preceding-sibling combinator. */
  body:has(.room-scene.dimmed .mroom) .shell-bar{opacity:.45;transition:opacity 1.6s var(--ease-veil)}
}
@media (max-width:820px) and (prefers-reduced-motion:reduce),(max-height:500px) and (orientation:landscape) and (prefers-reduced-motion:reduce){
  .mroom-head,.keyrow,.mroom-foot,
  .room-scene:has(.mroom) .scene-frame::after{transition-duration:.2s}
  .practice .cinema-bar button:active{transform:none}
}

/* ---- the keys open where they are ---- */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .keyset{border-radius:14px;overflow:hidden;border:1px solid rgb(217 182 98 / .34)}
  .keyset--silver{border-color:rgb(150 165 195 / .42)}
  .keyset--gold{border-color:rgb(201 169 78 / .5)}
  /* the row is now a disclosure, not a destination: no border or radius of its own,
     the section carries both, so opening it reads as one object */
  .keyset .keyrow{width:100%;border:0;border-radius:0;text-align:left;cursor:pointer;
    font-family:inherit;background:rgb(255 253 246 / .82)}
  .keyset--silver .keyrow{background:linear-gradient(180deg, rgb(247 249 253 / .96), rgb(238 243 250 / .9))}
  .keyset--gold .keyrow{background:linear-gradient(180deg, rgb(255 246 224 / .96), rgb(252 240 209 / .9))}
  .keyset .keyrow-go{transition:transform .32s var(--ease-veil)}
  .keyset .keyrow[aria-expanded="true"] .keyrow-go{transform:rotate(90deg)}
  /* 0fr -> 1fr animates to the content's own height, with no magic max-height to guess */
  .keyslots{display:grid;grid-template-rows:0fr;transition:grid-template-rows .38s var(--ease-veil)}
  .keyrow[aria-expanded="true"] + .keyslots{grid-template-rows:1fr}
  .keyslots-in{overflow:hidden;min-height:0}
  .keyrow[aria-expanded="true"] + .keyslots .keyslots-in{border-top:1px solid rgb(217 182 98 / .28)}
  .keyslot{display:flex;align-items:center;gap:.8rem;padding:.72rem .95rem;min-height:56px;
    border-bottom:1px solid rgb(217 182 98 / .16);background:rgb(255 253 246 / .5)}
  .keyslot:last-child{border-bottom:0}
  .keyslot-lock{width:19px;height:19px;flex:none;color:#8a7a52}
  .keyslot-lines{flex:1;min-width:0;display:block}
  .keyslot-bar{display:block;height:.62rem;border-radius:999px;max-width:15rem;
    background:linear-gradient(90deg, rgb(63 58 85 / .13), rgb(63 58 85 / .05))}
  .keyslot-bar--sub{height:.42rem;margin-top:.4rem;width:46%}
  /* A recording that EXISTS is named and tappable (2026-08-04). It keeps the lock,
     because it is still held behind its key: the lock says "yours with the key",
     the name says "and this is what it is". Only unmade recordings stay blank. */
  .keyslot--named{width:100%;font:inherit;text-align:left;cursor:pointer;border:0;
    border-bottom:1px solid rgb(217 182 98 / .16);color:inherit;
    -webkit-tap-highlight-color:transparent}
  .keyslot--named:last-child{border-bottom:0}
  .keyslot--named:active{background:rgb(255 244 210 / .72)}
  .keyslot-name{flex:1;min-width:0;font-family:var(--font-serif);font-size:.94rem;
    line-height:1.28;color:#3a3040}
  /* The slot shows a lock or a play, never both: whether this member holds the key
     is only known once the session lands, so temple.js paints `.unlocked` then. */
  .keyslot-play{width:19px;height:19px;flex:none;display:none;place-items:center;
    color:#8a7a52;font-size:.82rem;line-height:1}
  .keyslot--named.unlocked .keyslot-lock{display:none}
  .keyslot--named.unlocked .keyslot-play{display:grid}
}
@media (max-width:820px) and (prefers-reduced-motion:reduce),(max-height:500px) and (orientation:landscape) and (prefers-reduced-motion:reduce){
  .keyslots{transition-duration:.01s}
  .keyset .keyrow-go{transition:none}
}

/* ============================================================
   THE ALCHYMIE ROOM — the room IS the interface (desktop)
   The artwork carries four exhibits. Hovering lights one; clicking
   opens a card that grows out of that exhibit. No cards on arrival.
   Mobile keeps the card deck below (Director's call 2026-07-29).
   ============================================================ */
.exhibit-scene .scene-frame{aspect-ratio:1536/1024}

/* the exhibits carry their own class, NOT .arch: .arch is claimed by the lobby's
   door choreography in temple.js, which zooms and navigates on click. These open a
   card instead, so they must not be picked up by that handler. */
.exhibit{position:absolute;display:block;text-decoration:none;cursor:pointer;
  outline-offset:4px;border-radius:14px;z-index:3}
/* --halo-bleed: how far the light spreads past the hotspot. The default suits a poster
   or a door. A small object (the session room's two desk ornaments are 11% of the frame
   wide and 7% tall) needs much more, or the glow reads as a lit rectangle sitting on the
   artwork instead of the object itself catching light. Set it inline per exhibit. */
.exhibit .halo{position:absolute;inset:var(--halo-bleed,-10%) calc(var(--halo-bleed,-10%) * 1.2);border-radius:inherit;opacity:0;
  transition:opacity .5s ease;filter:blur(5px);pointer-events:none;
  background:radial-gradient(ellipse 60% 58% at 50% 55%, rgb(255 244 214 / .72), rgb(255 240 198 / 0) 70%)}
.exhibit:hover .halo,.exhibit:focus-visible .halo{opacity:1}
.exhibit .plate{display:none}   /* the room is not labelled: the light is the label */
.exhibit--soon{cursor:pointer}
.exhibit--soon .halo{background:radial-gradient(ellipse 60% 58% at 50% 55%, rgb(205 218 240 / .5), transparent 70%)}

/* 2026-08-12 (Kirra, web edits): the Heart of Space light also SAYS where it leads.
   ONE exhibit in ONE room: the plate is un-hidden for [data-exhibit="hos"] only, which
   exists nowhere but room-alchymie.html, so "the light is the label" still holds for
   every other exhibit. It rides the exact hover/focus the halo rides (same .5s ease),
   so there is no second state to keep in step. Dressed as the Gatherings wall's
   .map-name (serif small-caps ink with the light text-shadow halo) so it belongs to
   the site rather than reading as a stuck-on tooltip. */
/* 2026-08-13: and the Courses Room asked for the same thing on all four statues
   ("when you hover over the statue it shows the course title"). Those four opt in by
   class, `.exhibit--labelled`, so the ONLY change here is the selector list -- no
   second plate design, no second hover state, nothing to keep in step. */
.exhibit[data-exhibit="hos"] .plate,.exhibit--labelled .plate,.exhibit--soon .plate,.exhibit[data-gate] .plate{display:block;position:absolute;left:50%;top:50%;
  transform:translate(-50%,-50%);width:max-content;max-width:14rem;pointer-events:none;
  font-family:var(--font-serif);font-size:clamp(.82rem,1.05vw,1rem);letter-spacing:.2em;
  text-transform:uppercase;text-align:center;line-height:1.35;color:#3f3757;
  opacity:0;transition:opacity .5s ease;
  text-shadow:0 1px 9px rgb(255 253 246 / .95),0 0 22px rgb(255 253 246 / .9)}
.exhibit[data-exhibit="hos"]:hover .plate,
.exhibit[data-exhibit="hos"]:focus-visible .plate,
.exhibit--labelled:hover .plate,
.exhibit--labelled:focus-visible .plate{opacity:1}
/* ONE "coming soon" badge. A closed door wears it OVER the door, not under it (Director
   2026-08-19); a closed exhibit (the Press, still clickable: its card is the content)
   and a keyed exhibit (the Library's Mezzanine and Sanctum) wear the same pill at rest,
   which is why it sits here, after the hover-only plate rule it has to out-order. */
.arch--soon .plate,.exhibit--soon .plate,.exhibit[data-gate] .plate{top:62%;transform:translate(-50%,-50%);opacity:1;
  padding:.55em 1.2em;border-radius:999px;background:rgb(250 246 236 / .84);
  box-shadow:0 2px 14px rgb(60 45 25 / .12);text-shadow:none;color:#565073}

/* THE COMING-SOON PILL GOES THE MOMENT YOU PICK A ROOM. Director, 2026-08-27: "the
   tutorials coming soon capsule stays locked on the screen when the video plays to go
   into a room. It should disappear as soon as you click on a room."
   The pill is the one .plate that is opacity:1 at rest rather than on hover, so unlike
   every other label it had nothing to take it away, and it sat over the door film for
   the whole crossing. `.opening` is already put on the scene in the click handler, in
   the same turn as the click, so this needs no new state and no JS: it is simply the
   rule that was missing. Every pill in the scene goes, not only Tutorials' one - the
   Press exhibit and the Library's keyed hotspots wear the same badge and would sit over
   their own films for the same reason. 140ms so it is gone before the film is revealed
   (.door-film transitions in over 180ms) rather than dissolving across it at the
   plate's resting .5s. */
.temple-scene.opening .arch--soon .plate,
.temple-scene.opening .exhibit--soon .plate,
.temple-scene.opening .exhibit[data-gate] .plate{opacity:0;transition:opacity .14s linear}

/* THE ROOM DOES NOT LIGHT ITSELF. Director 2026-07-29, and the deletion it
   caused is the point: an unprompted arrival sweep could only ever be seen by
   a member who had ALREADY ticked "do not show this again", because anyone who
   has not gets the walkthrough instead. So the only person it ever played to
   was the one person who had said they were done being shown things. It is
   gone: the `--wake` stagger, the `exhibit-hint` keyframes and the rule that
   ran them. A halo now lights for exactly one reason, which is that something
   is over it -- your pointer, or the walkthrough's.

   The standing .scene-hint line ("Move over the room. Whatever lights will
   open.") went at the same time, same reason: the room was explaining itself. */

/* ---- the walkthrough: how to move through this room -----------------------
   Shown every arrival until they tick the box. There is NO tutorial mode
   and no diagram of the room: a ghost point of light crosses the real room and
   lights real exhibits. .is-demo is the only new state, and it drives the same
   halo hover already drives, so there is no second room to keep in step.
   2026-07-29 */
.exhibit.is-demo .halo{opacity:1}
.guide{position:absolute;inset:0;z-index:6;pointer-events:none}
.guide[hidden]{display:none}
/* the ghost carries the travel; the dot and the ring carry the click, so the
   press never fights the transform that is moving it across the room */
.guide-ghost{position:absolute;left:0;top:0;width:0;height:0;opacity:0;
  transition:opacity .45s ease, transform 0ms cubic-bezier(.76,0,.24,1)}
.guide-ghost.in{opacity:1}
.guide-dot,.guide-ring{position:absolute;left:-13px;top:-13px;width:26px;height:26px;border-radius:50%}
/* the room is bright ivory: a pale gold light would vanish into the marble, so
   the pointer is the Temple's own ink with a gold rim. Reads at every point of
   the walk, including across the lit tables. */
.guide-dot{background:radial-gradient(circle at 38% 34%, rgb(101 94 133 / .96) 0%, rgb(63 58 85 / .94) 60%);
  box-shadow:0 0 0 2px rgb(217 182 98 / .92), 0 8px 20px rgb(63 58 85 / .42)}
.guide-ring{border:2px solid rgb(63 58 85 / .5);opacity:0}
.guide-ghost.press .guide-dot{animation:guide-press .52s var(--ease-veil)}
.guide-ghost.press .guide-ring{animation:guide-ping .72s var(--ease-veil)}
@keyframes guide-press{0%{transform:scale(1)}28%{transform:scale(.58)}100%{transform:scale(1)}}
@keyframes guide-ping{0%{opacity:.9;transform:scale(.55)}100%{opacity:0;transform:scale(3.4)}}

/* the plaque at the foot of the room: the same glass as the exhibit card, so it
   belongs to the building. Not a modal, nothing is trapped, the room stays live. */
.guide-say{position:absolute;left:50%;bottom:5svh;z-index:7;transform:translateX(-50%);
  display:flex;align-items:center;justify-content:center;gap:.9rem 1.4rem;flex-wrap:wrap;
  width:max-content;max-width:min(44rem,92vw);padding:.8rem 1.3rem;
  background:rgb(255 251 241 / .93);border:1px solid rgb(255 255 255 / .55);
  border-radius:var(--radius-md);box-shadow:var(--shadow-lifted);
  backdrop-filter:blur(18px) saturate(1.4);-webkit-backdrop-filter:blur(18px) saturate(1.4);
  opacity:0;transition:opacity .5s var(--ease-veil)}
.guide-say.on{opacity:1}
.guide-say[hidden]{display:none}
.guide-say p{margin:0;font-family:var(--font-serif);font-size:.95rem;line-height:1.5;
  color:var(--color-text);text-wrap:pretty}
.guide-say-row{display:flex;align-items:center;gap:1.1rem}
.guide-say .onboard-again{margin:0;white-space:nowrap}
@media (prefers-reduced-motion: reduce){
  .guide-dot,.guide-ring{animation:none}
  .guide-say{transition:none}
}

/* the card: same glass, same split, same width as the sign-in card, so the
   room and the threshold feel like one building */
.exhibit-veil{position:fixed;inset:0;z-index:40;display:grid;place-items:center;
  padding:clamp(1rem,4vw,3rem);background:rgb(30 26 16 / 0);
  transition:background .42s var(--ease-veil);pointer-events:none}
.exhibit-veil.on{background:rgb(30 26 16 / .34);pointer-events:auto;
  backdrop-filter:blur(2px);-webkit-backdrop-filter:blur(2px)}
.exhibit-card{display:grid;grid-template-columns:minmax(0,1.1fr) minmax(25rem,30rem);
  width:100%;max-width:78rem;background:rgb(255 251 241 / .92);
  border:1px solid rgb(255 255 255 / .55);border-radius:var(--radius-md);
  box-shadow:var(--shadow-sanctum);backdrop-filter:blur(26px) saturate(1.5);
  -webkit-backdrop-filter:blur(26px) saturate(1.5);overflow:hidden;
  /* --dx/--dy are set per exhibit so the card grows out of the thing you clicked */
  transform:translate(var(--dx,0px),var(--dy,0px)) scale(.10);opacity:0;
  /* This is the LEAVING transition (the state the card returns to). It was the same
     .34s ease-out the arrival uses, and on a strong ease-out that empties the card to
     7% opacity by 150ms — a third of the way home. You saw it vanish rather than go
     back. Leaving now fades linearly over the transform's own length, so the card is
     still there to watch as it lands on the exhibit. 2026-08-04. */
  transition:transform .52s cubic-bezier(.22,1,.36,1),opacity .5s linear}
/* and this is the ARRIVING transition, deliberately the quicker fade: the card has to
   be visible while it is still small and still on the exhibit, or the growth is over
   before anyone can see where it came from. */
.exhibit-veil.on .exhibit-card{transform:translate(0,0) scale(1);opacity:1;
  transition:transform .52s cubic-bezier(.22,1,.36,1),opacity .34s var(--ease-veil)}
@media (prefers-reduced-motion: reduce){
  .exhibit-card{transition-duration:.01s;transform:none;opacity:0}
  .exhibit-veil.on .exhibit-card{transform:none}
}
.exhibit-card .shot{position:relative;margin:0;min-height:32rem;background:#efe7d5;
  border-right:1px solid rgb(217 182 98 / .4);overflow:hidden}
.exhibit-card .shot img{display:block;width:100%;height:100%;object-fit:cover;object-position:top center}
.exhibit-card .shot.is-soon{display:grid;place-items:center;padding:2rem;
  background:linear-gradient(165deg,rgb(250 246 236 / .8),rgb(238 240 248 / .6))}
.exhibit-card .shot.is-soon p{font-family:var(--font-serif);letter-spacing:.3em;text-transform:uppercase;
  font-size:.8rem;color:#9b95b3;margin:0;text-align:center}
.exhibit-body{padding:clamp(2rem,3.4vw,3rem);align-self:center}
.exhibit-body .kicker{font-family:var(--font-serif);font-size:.72rem;letter-spacing:.3em;
  text-transform:uppercase;color:#9a8a54;margin:0 0 .5rem}
.exhibit-body h2{font-family:var(--font-serif);font-weight:500;font-size:clamp(1.6rem,2vw,2.1rem);
  margin:0 0 .8rem;color:var(--color-text)}
.exhibit-body p{margin:0 0 .95rem;font-size:.97rem;line-height:1.7;color:var(--color-text)}
.exhibit-actions{display:flex;gap:.7rem;flex-wrap:wrap;margin-top:1.4rem}
.exhibit-actions .small{flex-basis:100%;margin:.2rem 0 0;font-size:.78rem;color:#8a8272}
/* Kirra's full course copy (12 Aug) made the Abundance card taller than the screen:
   the veil centres the card and nothing capped its height, so the actions sat below
   the fold with no way to reach them. Desktop only - the phone sheet further down
   already owns its own scrolling. The cap binds only when the copy exceeds it, so
   every short card keeps its exact height. 2026-08-12. */
@media (min-width:921px){
  .exhibit-body{max-height:min(84vh,52rem);overflow-y:auto;overscroll-behavior:contain}
}
/* the long course outlines read as raw browser bullets inside the exhibit card;
   same quiet list the .course-card panes already use. 2026-08-12. */
.exhibit-body .weeks{list-style:none;padding:0;margin:.2rem 0 .95rem}
.exhibit-body .weeks li{margin:0 0 .45rem;font-size:.92rem;line-height:1.65;color:var(--color-text-muted)}
.exhibit-body .dates{font-size:.88rem;color:var(--color-text-muted)}

/* 2026-08-13 (Kirra, WEB EDITS 13/8): "uniform boxes ... with the dates highlighted".
   The dates and the price are the two lines anybody actually scans a course box for,
   and they were set as the smallest, faintest text in it. One rule for all three
   places the same sentence appears -- the phone's rail card, the desktop pop-out and
   the Learn More page -- so the three can never drift apart again.
   #8a6b28 is the gold ink already used elsewhere in this file, NOT --color-primary:
   the raw gold measures 1.9:1 on the cream card and is unreadable as body text. This
   measures 4.9:1 on the .88 card ground, above the 4.5:1 body-text floor. */
.exhibit-body .dates,.course-card .dates,.course-doc .dates{font-family:var(--font-serif);
  font-size:1.02rem;line-height:1.55;color:#8a6b28;margin:0 0 1rem}

@media (max-width:920px){
  .exhibit-card{grid-template-columns:1fr;max-width:30rem}
  .exhibit-card .shot{order:2;border-right:0;border-top:1px solid rgb(217 182 98 / .4);aspect-ratio:16/10}
}
/* ===========================================================================
   THE EXHIBIT CARD ON A PHONE, 2026-08-11. Director: "when I'm inside and I
   click on the bubbler the card is way too big for the screen. It's like you've
   designed it for desktop."
   It WAS designed for desktop, and until this morning that was harmless because
   the phone never showed a hotspot, so this card was unreachable there. Turning
   the rooms into panoramas made every one of them reachable and exposed a card
   nobody had ever seen at 390px.
   ⚠ THE MEASURED CULPRIT IS `min-height:32rem` ON `.shot` — 512px of picture on
     an 844px screen before a word of text. The `max-width:920px` block above
     switches to one column and sets `aspect-ratio:16/10`, but aspect-ratio
     cannot shrink a box below its own min-height, so the rule above it silently
     won. `max-width:30rem` is 480px against a 390px phone as well.
   It becomes a SHEET: full width, off the bottom edge, as tall as it needs up to
   the fold, and scrolling inside itself. That is the native shape for this on a
   phone and it is the same shape the Library's glossary card already uses lower
   down this file — this generalises it to every exhibit rather than adding a
   second idea. */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .exhibit-veil{padding:0;place-items:end stretch}
  .exhibit-card{max-width:100%;width:100%;
    max-height:92svh;grid-template-rows:auto minmax(0,1fr);
    border-radius:18px 18px 0 0;border-bottom:0;
    /* it rises from the bottom edge rather than growing out of a hotspot: the
       hotspot is under your thumb and the card covers it either way */
    transform:translateY(14%);}
  .exhibit-veil.on .exhibit-card{transform:translateY(0)}
  .exhibit-card .shot{order:0;min-height:0;aspect-ratio:16/9;
    border-top:0;border-bottom:1px solid rgb(217 182 98 / .4)}
  /* ⚠ NOT EVERY `.shot` IS A PHOTOGRAPH. The Sessions cards put a PLATE there — price,
     practitioners, and the booking button — and forcing 16/9 on that squashes a panel of
     text into a letterbox, then floats it ABOVE the heading, so the card opened with a
     price and a call to action before it had said what the session was. A picture leads;
     a plate follows. Told apart by what is inside it rather than by a room name, so a
     new card gets the right one without a new rule. */
  .exhibit-card .shot:not(:has(img)){aspect-ratio:auto;order:2;
    border-bottom:0;border-top:1px solid rgb(217 182 98 / .4)}
  .exhibit-card:has(.shot:not(:has(img))){grid-template-rows:minmax(0,1fr) auto}
  /* the words scroll, the picture stays put — a phone sheet you can actually read */
  .exhibit-body{padding:1.15rem 1.15rem calc(1.15rem + env(safe-area-inset-bottom));
    align-self:stretch;overflow-y:auto;overscroll-behavior:contain;
    -webkit-overflow-scrolling:touch}
  .exhibit-body h2{font-size:1.35rem}
  .exhibit-body p{font-size:.95rem;line-height:1.6}
  /* 44px targets, and the primary action reads first */
  .exhibit-actions{margin-top:1.1rem;gap:.55rem}
  .exhibit-actions .btn{flex:1 1 100%;min-height:44px;display:inline-flex;
    align-items:center;justify-content:center}
  /* a card with no picture is all body, so it must not be forced into two rows */
  .exhibit-card:not(:has(.shot)){grid-template-rows:minmax(0,1fr)}
}
/* ===========================================================================
   THE ROOMS TURN TOO, 2026-08-11. Director, on the Courses room:
     "we are meant to have that one statue stayed put on the screen at all times
      and we could actually scroll like we're in the lobby so then you can touch
      the statue and then get the card about the course"
   and then, generalising it himself:
     "that actually goes for all the screens ... take the library. It should show
      the books and then we should be able to swipe left and we see the pictures.
      Then we swipe left. We see the staircase ... same in the session room start
      with the board on the wall swipe left you see the Stargate swipe left you
      get the chair swipe left you get the astrology"

   Until now the phone hid the hotspots and showed a rail of cards instead, on the
   argument that hover does not exist on a phone. That argument was about HOVER and
   it took the whole room with it: a member on a phone never saw the room at all,
   only text over a blurred backdrop. The lobby had already answered this — the
   scene becomes a horizontal scroller, the artwork stands at full height, every
   doorway is a snap point, and the plate at your feet is the way in. A statue, a
   bookcase or a Stargate is the same problem as a door.
   So there is ONE mechanism now, not two, and it is the browser's own scroll-snap
   in both cases. `:has(.door-deck)` picks the door rooms; this picks the rest.

   ⚠ ROOM-ZOOM IS IN TOO, since 2026-08-11. It was excluded on the reading that its map
     regions "scroll a list rather than open a card". That was wrong: runZoom BUILDS a
     card per region at runtime — the gatherings for that part of the world, their hours
     in the visitor's own clock, and the Join / Claim free / See the keys buttons. So a
     region is an object like any other. The Director: "in the zoom room we focus on a
     country and then we swipe and we get a new country, but you just put the cards up."
     ⚠ Its regions carry `hidden` until runZoom has been to the backend, so the panorama
     has to work from the VISIBLE ones and re-open on the first of them once they appear
     — see the MutationObserver in js/temple.js. A room that locked onto a hidden region
     would rest on empty ocean.
   ⚠ These scenes are `aspect-ratio:1536/1024`, NOT the lobby's 1561/1008. Same
     arithmetic as the door panorama, its own two numbers: fill the height OR fill
     the width, whichever demands more, so the room runs past both edges in portrait
     and still covers when the phone is turned.
   ⚠ Specificity is deliberate. `.room-scene .scene-frame` a few hundred lines up
     pins the frame `position:fixed;width:100vw` as a backdrop, and `.room-scene
     .stage` sets `object-fit:cover`. Both are (0,2,0); every override here is
     (0,3,0) via `:not()`, so it wins without an !important.
   ⚠ AND THE SCROLLER IS THE WRAPPER, NOT `.temple-scene` — this is the whole trick, and
     the first attempt got it wrong in a way that looked plausible and measured
     obviously broken. THE HOTSPOTS ARE CHILDREN OF `.temple-scene`, not of the artwork:
     `<div class="temple-scene"><div class="scene-frame"><div class="scene-zoom"><img>` and
     then the `.exhibit` anchors as SIBLINGS of the frame. So their `left:15.6%` resolves
     against `.temple-scene`. Turn `.temple-scene` itself into the 390px-wide scroller and
     every hotspot bunches into the leftmost 390px of a 1266px room: measured, the four
     Courses statues landed at scene-x 84/149/237/308 where the artwork puts them at
     197/470/745/970, so all four snap points sat on top of the first statue and swiping
     went nowhere.
     Moving them into `.scene-zoom` (where the door rooms keep their arches) is NOT the
     fix: on desktop `.temple-scene` is 1440x900 while `.scene-frame` is 1440x960 at
     y=-30, so all 19 hotspots across four rooms are tuned against the shorter box and
     would shift ~3% of the height. That is a re-tune of every room to fix a phone.
     So `.temple-scene` becomes the WIDE element — the artwork's own box, which is what
     the percentages have always wanted — and its wrapper does the scrolling. */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  [data-state-panel="populated"]:has(> .exhibit-scene){
    height:100svh;overflow-x:auto;overflow-y:hidden;
    scroll-snap-type:x mandatory;overscroll-behavior:contain;touch-action:pan-x;
    scrollbar-width:none;display:flex;align-items:center}
  [data-state-panel="populated"]:has(> .exhibit-scene)::-webkit-scrollbar{display:none}
  /* the scene IS the artwork's box now: same two constraints as the door panorama, its
     own aspect. Fill the height, OR fill the width, whichever demands more. */
  .exhibit-scene{position:relative;flex:none;
    height:max(100lvh, 100vw * 1024 / 1536);
    width:max(100vw, 100lvh * 1536 / 1024);
    min-height:0;min-width:0;padding:0}
  .exhibit-scene .scene-frame{position:absolute;inset:0;left:0;top:0;
    width:100%;height:100%;transform:none;aspect-ratio:auto;min-width:0;min-height:0}
  .exhibit-scene .stage{object-fit:fill;object-position:center}
  /* every object is a resting place, and the one you are standing at is lit */
  .exhibit-scene .exhibit{display:block;
    scroll-snap-align:center;scroll-snap-stop:always}
  .exhibit-scene .exhibit.locked .halo{opacity:1}
  /* the flash belongs to the phone, not to the panorama it sits inside */
  .exhibit-scene .scene-flash{position:fixed}
  /* The rail of cards this replaces. Nothing is lost: every card it held is the same
     card the object opens, from the same template.
     ⚠ `.temple-scene.exhibit-scene`, NOT `.exhibit-scene`. The card rail's own rule
     `.room-panel:has(.pane){display:flex}` sits ~800 lines LATER in this file at the
     same (0,2,0) specificity, so a bare `.exhibit-scene .room-panel` LOSES to it on
     source order and every room drew its cards back over the artwork. It only worked
     before because the selector still carried `:not(.map-scene)`, which was quietly
     doing the specificity work; widening the gate to the map room dropped it and the
     rail came back. Compounding on `.temple-scene` restores (0,3,0) and says why. */
  .temple-scene.exhibit-scene .room-panel{display:none}
  /* and room-zoom's vertical list goes the same way, for the same reason and on the
     same evidence the DESKTOP already uses further down this file: "the map carries all
     of it, including the Quarterly week, so the panel goes rather than sitting there as
     an empty box reserving a screen of scroll". Each region's card is built by runZoom
     with that region's gatherings, their hours in the visitor's own clock, and the Join,
     Claim free gathering and See the keys buttons — so nothing here is a dead end. */
  .gathering-panel{display:none}
  /* the walkthrough taught a hover that does not exist; the plate teaches the swipe.
     (runGuide is already gated on DESK_MQ — this is belt and braces.) */
  .exhibit-scene .guide,
  .exhibit-scene .guide-say{display:none!important}
  /* ⚠ `.map-name` IS ONLY STYLED AT >=821px — see its rule further down. Below that it
     fell back to the browser default for a <span> inside an <a>, so the five continents
     were labelled in raw link blue across the gold map. It rides a hover that a finger
     does not have anyway. The plate names the section instead, for every object room. */
  .map-name,.map-call{display:none}
  /* AND THE PLATE SAYS WHAT YOU ARE STANDING IN FRONT OF. A door does not need this —
     its name is carved in marble over the arch — but a statue, a bookcase, a Stargate
     and a continent carry no label at all, so on those rooms the plate is the only thing
     that says what you are about to open. js/temple.js adds the class for object rooms. */
  .pan-plate--named{flex-direction:column;gap:.15rem;padding:.5rem 1.25rem;
    max-width:min(84vw,22rem)}
  .pan-plate--named .pan-name{position:static;width:auto;height:auto;overflow:visible;
    clip-path:none;font-family:var(--font-serif);font-size:.9rem;line-height:1.2;
    color:#3f3757;text-align:center;text-wrap:balance}
  /* line-height: the base `.pan-step` sets 1, which is fine for the one-word steps
     ("swipe") it was written for. "Tap a shelf to open it" wraps to two lines at
     .3em tracking and the two lines then touch. Measured on the Library at 390x844,
     2026-08-25. */
  /* the name above it is centred and text-balanced; the step wrapped to two lines and
     read ragged-left underneath it. Director, 2026-08-25: "all the text should be
     centred." */
  .pan-plate--named .pan-step{font-size:.6rem;opacity:.72;line-height:1.45;
    text-align:center;text-wrap:balance}
}
/* Reduced motion gets the rail back, exactly as the door rooms get their deck back:
   someone who has asked for less movement should not be handed a room that only opens
   by flinging it sideways. Mirrors the `:has(.door-deck)` rule of the same shape. */
@media (max-width:820px) and (prefers-reduced-motion:reduce),(max-height:500px) and (orientation:landscape) and (prefers-reduced-motion:reduce){
  [data-state-panel="populated"]:has(> .exhibit-scene){
    height:auto;overflow-x:visible;scroll-snap-type:none;display:block}
  .exhibit-scene{width:auto;height:auto;min-height:100svh}
  .exhibit-scene .scene-frame{position:fixed;width:100vw;height:100lvh}
  .exhibit-scene .stage{object-fit:cover;object-position:center 30%}
  .exhibit-scene .exhibit{display:none}
  .temple-scene.exhibit-scene .room-panel{display:block}
  .gathering-panel{display:block}
}
/* room-zoom keeps the old behaviour: its map regions are not objects that open */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .map-scene .exhibit:not(.map-region){display:none}
  /* Kirra 13/8 + Director (mobile too): the room's two offerings must be reachable
     on the phone. The desktop cushion cues become a fixed bottom bar of two 44pt
     pills over the panorama, opening the SAME veil cards the desktop opens - no
     second mechanism. These two rules out-specify the hide above on purpose. */
  /* Top of the screen, under the bar: the bottom centre belongs to the panorama's
     SWIPE plate, which sits exactly where a bottom bar would go and takes the tap. */
  .map-scene .map-cues{display:flex;position:fixed;left:50%;
    top:calc(var(--bar-h, 56px) + max(env(safe-area-inset-top, 0px), 8px));
    transform:translateX(-50%);
    z-index:6;gap:.5rem;flex-wrap:wrap;justify-content:center;
    width:max-content;max-width:calc(100vw - 1rem)}
  /* position:static undoes the base .exhibit absolute hotspot placement, so the
     pills sit IN the flex row and give the bar its height. */
  .map-scene .exhibit.map-cue{position:static;display:inline-flex;align-items:center;
    justify-content:center;gap:.4rem;min-height:44px;padding:.55rem 1.05rem;
    border-radius:999px;background:rgb(255 253 246 / .85);color:#3f3757;
    text-decoration:none;box-shadow:0 2px 12px rgb(0 0 0 / .2)}
  .map-scene .map-cue span{font-family:var(--font-serif);font-size:.6rem;
    letter-spacing:.2em;text-transform:uppercase;white-space:nowrap}
  .map-scene .map-cue .arr{display:none}
  .map-scene .map-cue:active{background:rgb(255 253 246 / .98)}
  .map-scene.opened .map-cues{opacity:0;pointer-events:none}
}
@media (min-width:821px) and (min-height:501px){
  .exhibit-scene .room-panel{display:none}
}

/* =====================================================================
   2026-07-29 — the threshold card and the welcome card
   Fixes from the design critique of the same date. Every rule below is
   additive or overriding and this whole block can be deleted to return
   both cards to how they were, EXCEPT the four in-place edits noted in
   their own comments above (.remember, the focus ring, .onboard-say and
   the two checkbox sizes), which are corrections, not preferences.
   ===================================================================== */

/* A control that ACTS instead of navigating. "Forgot your password?" and
   "Back to sign in" swap the face in place, so they are buttons; this makes
   a button look like the link it sits beside without pretending to be one. */
.linkish{background:none;border:0;padding:0;margin:0;font:inherit;color:inherit;
  text-decoration:underline;text-underline-offset:2px;cursor:pointer}
.linkish:hover{color:var(--color-text)}
.linkish:focus-visible{outline:2px solid #8a6f2e;outline-offset:2px;border-radius:3px}
/* under-note is a 14px muted line; a 44px target there would push the two
   links apart, so the row gets the height instead of the control. */
.auth-panel .under-note:has(.linkish){min-height:44px;display:flex;align-items:center;
  justify-content:center;margin-top:.35rem}

/* The good-news twin of .auth-error, same slot, same weight, not alarming.
   role="status" so it is announced without interrupting. */
.auth-said{color:#2f6b4a;text-align:center;font-size:.9rem;margin:0 0 var(--space-2);
  background:rgb(47 107 74 / .08);border:1px solid rgb(47 107 74 / .26);
  border-radius:var(--radius-sm);padding:.5rem .7rem}

/* THE SIGN-IN FACE IS TWO FIELDS AGAINST A 226-WORD INVITATION.
   The card is one grid and the left column set its height, so the form floated
   with ~280px of dead cream under it, on the screen returning members see most.
   Sign in and Reset drop the closing pitch; Create an account keeps all of it.
   Kirra's words are untouched either way, this only chooses which of them a
   RETURNING member is shown. */
.auth-card.is-signin .primer-more{display:none}

/* ---- the welcome card on a phone ------------------------------------
   The only mobile rule this card ever had was one line that collapsed the
   grid, with a comment saying mobile was a later pass. Measured at 390x844:
   1089px tall, two of the four rooms below the fold with nothing to say they
   were there, and four thumbnails that are the same gold door in the same
   marble room, indistinguishable at 146px wide.
   The rooms become rows here. A row can carry the name and the line at a
   readable size, all four fit above the fold, and the thumbnail crops square
   and tight so the door's own emblem is the part you see. */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .onboard-rooms{grid-template-columns:1fr;gap:.45rem}
  .onboard-room{grid-template-columns:52px 1fr;grid-template-rows:auto auto;
    grid-template-areas:"img name" "img line";
    align-items:center;column-gap:.75rem;row-gap:.1rem;
    text-align:left;padding:.45rem;min-height:64px}
  .onboard-room img{grid-area:img;width:52px;height:52px;aspect-ratio:1;
    margin:0;object-position:center 34%}
  .onboard-room .or-name{grid-area:name;align-self:end;font-size:.98rem}
  .onboard-room .or-line{grid-area:line;align-self:start}
  .onboard-room:hover,.onboard-room:focus-visible{transform:none}
  .onboard-doors{border-top:1px solid rgb(217 182 98 / .4);padding:1rem}
  .onboard-doorlead{text-align:left;margin-bottom:.6rem}
  /* Every padding on the phone is doing the same job the card's own edges do.
     Trimmed until all four rooms and the button sit inside one 844px screen
     with nothing to scroll: 899px before, 800px after. */
  .onboard{padding:.8rem}
  .onboard-say{padding:1.15rem 1.15rem 1rem}
  .onboard-card h2{font-size:1.6rem;margin-bottom:.6rem}
  .onboard-lead{line-height:1.58;margin-bottom:.6rem}
  /* stays 44: the tap floor is not where pixels get saved */
  .onboard-again{margin-top:.4rem}
}
/* The same crop reasoning on desktop, gently: the emblem is the only thing
   telling these four apart, so bias the frame toward it instead of the floor. */
.onboard-room img{object-position:center 38%}

/* "Create your free account" and "Reset your password" both broke to a short
   orphan line in a 24rem column. balance evens the two lines instead. */
.auth-panel h1{text-wrap:balance}
.onboard-card h2{text-wrap:balance}

/* ===========================================================================
   THE MEDITATION ROOM'S SIDE PANELS, 2026-08-04
   Director: "the side panels are just a little too see through and can you make
   them more professional."

   MEASURED before touching anything, on the deployed room at 1440:
     panel field vs the wall right beside it .............. 1.23:1
     "About this recording" body copy on the panel ........ 2.29:1
     the playing row's own title .......................... 1.97:1
   Body text needs 4.5:1. So this was not a taste call: the panels were a smear
   on the wall with white text floating on them, and no reading of it was
   possible. The cause was one dated override further up this file, "glassier,
   less yellow, more see-through", which took the field to 5.5% white.

   WHAT CHANGED, and why it is this and not a darker glass: the room is bright
   ivory marble in warm daylight, and the whole site already answers that with
   ivory and ink — the threshold card, the welcome card, the mobile practice
   card. Two dark slabs would have been a video app pasted into a temple. So the
   panels become the temple's own material: warm ivory glass, gold hairline, the
   ink the rest of the site writes in. You can still see the room through them.

   AND THE STATE THAT FORCED THE REST: a film dims the room, and an ivory panel
   would then be the brightest thing on a darkened screen — exactly the fault
   the top bar had on 29 Jul. So the panels flip register with the room, off the
   SAME `body:has(.room-scene.dimmed)` the bar already uses. No new state, no new
   JavaScript: the flip re-points six custom properties and every child follows.
   =========================================================================== */
.playlist.glass,.video-info.glass{
  --pl-ink:var(--color-text);
  --pl-ink-soft:var(--color-text-muted);
  --pl-row:rgb(255 255 255 / .58);
  --pl-line:rgb(217 182 98 / .5);
  --pl-row-hover:rgb(255 255 255 / .82);
  --pl-row-active:rgb(255 252 242 / .95);
  --pl-shade:rgb(63 58 85 / .16);
  /* 2026-08-04, Director: a little more glass. The ivory field thins (.9/.85 ->
     .74/.68) so the marble reads through it, and the frost deepens to hold the
     ink legible while it does. Dark ink on a light field over a light wall: the
     2026-08-01 contrast fix survives the thinning. */
  background:linear-gradient(180deg,rgb(255 252 246 / .74),rgb(251 245 233 / .68));
  -webkit-backdrop-filter:blur(26px) saturate(1.14);
  backdrop-filter:blur(26px) saturate(1.14);
  border:1px solid var(--pl-line);
  box-shadow:0 24px 56px rgb(63 48 30 / .24),0 2px 8px rgb(63 48 30 / .1),
    inset 0 1px 0 rgb(255 255 255 / .9);
  color:var(--pl-ink);
  transition:background 1.8s var(--ease-veil),border-color 1.8s var(--ease-veil),
    box-shadow 1.8s var(--ease-veil),color 1.8s var(--ease-veil)}

/* the panel writes in ink now, so every white-on-nothing rule above is answered */
.pl-head,.pl-title,.vi-title{color:var(--pl-ink);text-shadow:none}
.pl-sub,.vi-desc,.pl-ghost-note{color:var(--pl-ink-soft);text-shadow:none}
.vi-head{color:var(--pl-ink-soft);opacity:1}
.vi-desc{color:var(--pl-ink)}
.pl-item{background:var(--pl-row);border:1px solid var(--pl-line);color:var(--pl-ink)}
.pl-item:hover:not(.locked){background:var(--pl-row-hover);border-color:rgb(217 182 98 / .7)}
.pl-item.active{background:var(--pl-row-active);border-color:rgb(217 182 98 / .85);
  box-shadow:0 4px 14px rgb(63 48 30 / .16)}
.pl-play{background:rgb(217 182 98 / .3);color:var(--pl-ink)}
.pl-tier{background:rgb(217 182 98 / .18);border-color:var(--pl-line);color:var(--pl-ink)}
.pl-tier--gold{color:#7d5c12;background:rgb(217 182 98 / .26);border-color:rgb(217 182 98 / .6)}
.pl-tier--silver{color:#4e5464;background:rgb(120 135 165 / .16);border-color:rgb(120 135 165 / .38)}
/* the locked shelves stay blank on purpose (Kirra, 2026-07-27: a member sees how
   many a key holds, not what they are). They just stop looking unloaded. */
.pl-item--ghost{background:rgb(255 255 255 / .34);border:1px solid rgb(217 182 98 / .45)}
.pl-item--ghost:hover{background:rgb(255 255 255 / .6);border-color:rgb(217 182 98 / .8)}
.pl-item--ghost .pl-play{background:rgb(217 182 98 / .22);color:var(--pl-ink-soft)}
.pl-ghost-bar{background:linear-gradient(90deg,var(--pl-shade),rgb(63 58 85 / .07))}
.playlist.glass::-webkit-scrollbar-thumb,
.video-info.glass::-webkit-scrollbar-thumb{background:rgb(63 58 85 / .28)}

/* the film starts, the room goes down, and the panels go with it */
body:has(.room-scene.dimmed) .playlist.glass,
body:has(.room-scene.dimmed) .video-info.glass{
  --pl-ink:rgb(255 250 240 / .95);
  --pl-ink-soft:rgb(255 250 240 / .7);
  --pl-row:rgb(255 255 255 / .1);
  --pl-line:rgb(217 182 98 / .34);
  --pl-row-hover:rgb(255 255 255 / .2);
  --pl-row-active:rgb(255 255 255 / .3);
  --pl-shade:rgb(255 255 255 / .28);
  background:linear-gradient(180deg,rgb(38 30 22 / .56),rgb(26 21 15 / .48));
  box-shadow:0 24px 60px rgb(0 0 0 / .5),inset 0 1px 0 rgb(255 255 255 / .14)}
body:has(.room-scene.dimmed) .pl-tier--gold{color:#ffe7a6;background:rgb(255 208 120 / .18);
  border-color:rgb(255 208 120 / .4)}
body:has(.room-scene.dimmed) .pl-tier--silver{color:#eef2f8;background:rgb(215 225 245 / .16);
  border-color:rgb(215 225 245 / .32)}
body:has(.room-scene.dimmed) .pl-item--ghost{background:rgb(255 255 255 / .07);
  border-color:rgb(255 255 255 / .26)}
body:has(.room-scene.dimmed) .playlist.glass::-webkit-scrollbar-thumb,
body:has(.room-scene.dimmed) .video-info.glass::-webkit-scrollbar-thumb{background:rgb(255 255 255 / .28)}
@media (prefers-reduced-motion:reduce){
  .playlist.glass,.video-info.glass{transition:none}}

/* ===========================================================================
   THE SESSION ROOM'S TWO PLATES (2026-08-04)

   Director: "what is a session can be on the left so you still see the lotus in
   the middle of the room, and then the other one positioned well, so it can all
   be a little bit like centre and not all geometrically correct, and with a bit
   more glass."

   So: not a centred column, and not a mirrored pair either. The left plate is
   the wider one and hangs a little BELOW the room's midline; the right plate is
   narrower and hangs ABOVE it, and is held further in from its edge. Both axes
   differ, both widths differ, so the pair reads as a composition instead of a
   frame. What the two of them leave open is deliberate: the middle of the back
   wall, where the carved lotus is, and the bottom right, where the marble table
   and the Stargate are.

   The material is the Meditation Room's ivory glass, on purpose: same building.
   ⚠ Source of truth for those numbers is the `.playlist.glass,.video-info.glass`
   block above (the 2026-08-01 contrast fix, thinned 2026-08-04). If the temple's
   glass changes, change it THERE first and bring these to match.
   =========================================================================== */
.plate{
  --plate-ink:var(--color-text);
  --plate-ink-soft:var(--color-text-muted);
  position:relative;isolation:isolate;
  display:flex;flex-direction:column;align-items:flex-start;
  padding:1.5rem 1.6rem 1.6rem;border-radius:20px;
  /* Thinner than the Meditation panels on purpose: those hang on a dimmed wall,
     these hang on a lit ivory one, where the same alpha reads as paper.
     ⚠ The gradient runs THIN AT THE TOP and thickens downward, which is the
     opposite of the usual. Measured, not styled: the room is bright at the wall
     and falls into deep shadow at the floor corners, and at .48 over that corner
     the body ink came out at 3.62:1. Thin where the wall is light so the marble
     and the gold moulding read through; thicker where it darkens so the ink
     holds. Re-measure this if the room artwork is ever regenerated. */
  background:linear-gradient(168deg,rgb(255 253 248 / .52),rgb(252 246 235 / .72));
  -webkit-backdrop-filter:blur(30px) saturate(1.2) brightness(1.03);
  backdrop-filter:blur(30px) saturate(1.2) brightness(1.03);
  border:1px solid rgb(217 182 98 / .55);
  box-shadow:0 26px 60px rgb(63 48 30 / .26),0 2px 10px rgb(63 48 30 / .12),
    inset 0 1px 0 rgb(255 255 255 / .95),inset 0 -1px 0 rgb(217 182 98 / .18);
  color:var(--plate-ink)}
/* the light in the room catches the top left corner of the sheet */
.plate::before{content:"";position:absolute;inset:0;z-index:-1;border-radius:inherit;
  pointer-events:none;
  background:radial-gradient(120% 90% at 8% -10%,rgb(255 255 255 / .5),transparent 58%)}
.plate h1,.plate h2{font-family:var(--font-serif);font-weight:500;
  margin:0;line-height:1.22;letter-spacing:-.005em;text-wrap:balance}
.plate h1{font-size:clamp(1.55rem,1.5vw + .9rem,2rem)}
.plate h2{font-size:clamp(1.15rem,.7vw + .85rem,1.35rem)}
.plate p{margin:.55rem 0 0;font-size:.94rem;line-height:1.62;max-width:38ch;text-wrap:pretty}
.plate .btn{margin-top:1.25rem}
/* the note writes in the same ink as the body and lets size carry the hierarchy.
   Muted #6d6787 measured 2.74:1 on this glass, and a washed grey on a warm field
   is the wrong answer anyway. */
/* `.plate p` above is class+type and outranks a bare `.plate-note`, so the note
   rendered at full body size and the hierarchy was flat. Matched here. */
.plate p.plate-note{margin-top:1.1rem;font-size:.86rem;line-height:1.55;color:var(--plate-ink)}
.plate-note a{color:#6a4a06}
.plate-note a:hover{color:#4d3503}

@media (min-width:821px) and (min-height:501px){
  /* the plates hang in the room themselves; the wrapper is only a mobile stack */
  .room-plates{display:contents}
  .plate{position:fixed;z-index:8;max-height:78svh;overflow-y:auto;scrollbar-width:none}
  .plate::-webkit-scrollbar{display:none}
  /* rem floors, not bare vw: at 1024 and at 820 a plain 24vw squeezed the ask plate
     to 246px and broke its heading over two lines. The floor holds the measure. */
  .plate--say{left:max(4.5vw,26px);top:53%;transform:translateY(-50%);
    width:clamp(19rem,29vw,25rem)}
  .plate--ask{right:max(6.5vw,30px);top:33%;transform:translateY(-50%);
    width:clamp(17rem,24vw,20.5rem)}
  /* one arrival, and the room is already there behind it: the plates come out of
     the air rather than sliding in from an edge */
  .plate{animation:plate-settle 900ms cubic-bezier(.16,1,.3,1) backwards}
  .plate--ask{animation-delay:130ms}
}
@keyframes plate-settle{
  from{opacity:0;transform:translateY(calc(-50% + 16px));filter:blur(7px)}
  to{opacity:1;transform:translateY(-50%);filter:none}
}

@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  /* phone: the artwork is the backdrop and the plates are the room (PRODUCT.md,
     principle 1). Stacked, full width, thumb-reachable. */
  .room-plates{position:relative;z-index:4;display:flex;flex-direction:column;gap:1rem;
    width:min(92vw,34rem);margin:0 auto;
    padding:calc(var(--bar-h) + 3svh) 0 calc(9svh + env(safe-area-inset-bottom))}
  .plate{width:100%;animation:plate-rise 760ms cubic-bezier(.16,1,.3,1) backwards}
  .plate p{max-width:none}
  .plate--ask{animation-delay:110ms}
}
@keyframes plate-rise{
  from{opacity:0;transform:translateY(16px);filter:blur(6px)}
  to{opacity:1;transform:none;filter:none}
}
@media (prefers-reduced-motion:reduce){.plate{animation:none}}


/* =====================================================================
   2026-08-04 — the Gathering Room: the map IS the interface
   Director: "make that world map bigger, I want it to take up when you
   walk in... go over a country or a continent and click on it and then
   out of that slowly and in feeling comes the card that shows all the
   zooms for that region."

   No new engine and almost no new CSS: the room reuses .exhibit-scene,
   which already lights a hotspot on hover and grows its card out of the
   thing you clicked. Everything below is the three things that reuse
   actually needed.
   ===================================================================== */

/* 1. .exhibit declares display:block, which BEATS the hidden attribute. The
      hotspots ship hidden and runZoom() reveals only the regions the backend
      actually sends a gathering for, so without this every region would be
      live, including ones with nothing behind them. This is the same trap
      .exhibit-veil hit with display:grid. */
.exhibit[hidden]{display:none}

/* 2. A region has no photograph, so its card is body only. The card is a two
      column grid built around a .shot; without one the body would sit in the
      first column with an empty second beside it. */
.exhibit-card:not(:has(.shot)){grid-template-columns:minmax(0,1fr);max-width:38rem}

/* 3. On desktop the room must be the whole first screen, or the map is not
      "taking up when you walk in". The panel scrolls up from beneath it,
      carrying the heading and the Quarterly week, which are not on the map
      and must stay reachable. The phone is untouched: there the hotspots do
      not exist (max-width:820px above) and the list IS the room. */
.gathering-panel{position:relative;z-index:4;padding:calc(56px + 5svh) 0 11svh}
.map-cue,.map-cues,.map-call{display:none}
@media (min-width:821px) and (min-height:501px){
  /* the map gets the whole first screen. The panel rises from under it, carrying
     the Quarterly week, which lives on no other page and so may not be hidden. */
  /* Every child of this panel is now .room-panel, i.e. the phone's. On desktop the
     map carries all of it, including the Quarterly week, so the panel goes rather
     than sitting there as an empty box reserving a screen of scroll.
     (It used to need margin-top:100svh, NOT padding-top: padding left the panel's own
     box over the whole first screen at z-index 4 above the hotspots at 3, and it
     silently swallowed every click on the map while looking perfect.) */
  .gathering-panel{display:none}
  /* the room's only label. Ink with a light halo, never pale gold: the Alchymie
     walkthrough pointer was pale gold on ivory marble and read as nothing. */
  .map-call{position:absolute;left:50%;top:21.4%;z-index:4;margin:0;
    transform:translateX(-50%);display:flex;flex-direction:column;align-items:center;
    gap:.45rem;pointer-events:none;text-align:center;
    text-shadow:0 1px 10px rgb(255 253 246 / .95),0 0 26px rgb(255 253 246 / .85);
    transition:opacity .5s var(--ease-veil)}
  .map-call-kicker{font-family:var(--font-serif);font-size:.62rem;letter-spacing:.36em;
    text-transform:uppercase;color:#6f5a26}
  .map-call-line{font-family:var(--font-serif);font-size:clamp(1.05rem,1.5vw,1.45rem);
    letter-spacing:.06em;color:#3f3757}
  .exhibit-scene.opened .map-call{opacity:0}
  /* a lit continent does not say which region it is, so it names itself under your
     pointer. Rides the same hover as the halo, so there is no second state. */
  .map-name{position:absolute;left:var(--name-x,50%);top:var(--name-y,50%);transform:translate(-50%,-50%);
    font-family:var(--font-serif);font-size:clamp(.82rem,1.05vw,1rem);letter-spacing:.2em;
    text-transform:uppercase;color:#3f3757;pointer-events:none;
    /* measured: AUSTRALIA AND NEW ZEALAND ran off the right of the wall at 1024 and
       below, because its region sits at 84.7% of a frame that is WIDER than the
       viewport at those sizes. Wrapping the long names beats keeping a second set of
       short ones, which would be one more thing to hold in step with the cards. */
    /* Sized in rem, NOT ch. 1ch is the width of a "0", about 7px here, so 18ch was
       126px and broke UNITED KINGDOM AND EUROPE onto four lines, a word each. 13rem
       holds the two short names on one line and the two long ones on two. */
    /* width:max-content is load-bearing. Without it the label is laid out in the
       space from its own left edge to the edge of the hotspot, which for a narrow
       region is about half the hotspot, and it wrapped a word per line no matter
       what max-width said. max-content lets it take its natural width first; 13rem
       then breaks only the two genuinely long names, onto two lines. */
    width:max-content;max-width:13rem;line-height:1.35;text-align:center;
    opacity:0;transition:opacity .38s var(--ease-veil);
    text-shadow:0 1px 9px rgb(255 253 246 / .95),0 0 22px rgb(255 253 246 / .9)}
  .exhibit:hover .map-name,.exhibit:focus-visible .map-name{opacity:1}

  /* ---- THIS ROOM DOES light itself, and only this room -------------------
     Director 2026-08-04: "I think all of them should be glowing with the title
     already visible." That reverses the 2026-07-29 rule for the Alchymie room,
     and deliberately, because the two rooms ask different questions. In Alchymie
     the exhibits are objects you can see standing in the room, so lighting them
     unasked only ever told a member who had already said they were done being
     told. Here they are five regions on a wall of continents, and nothing about
     Africa or Antarctica says we do not gather there. Resting light IS the
     answer to "where do you hold these", asked before anyone moves a pointer.
     Scoped to .map-scene so the Alchymie room stays dark, as he left it.

     Hover still has somewhere brighter to go, which is what makes "this one,
     now" a different statement from "these five are live". Same argument as the
     2026-07-29 peak going .5 to .72, and the same reason it must not rest full. */
  /* Australia's hotspot covers Australia AND New Zealand, so dead centre put its
     name out over the Tasman Sea. 32% is the middle of the Australian landmass. */
  .map-scene .exhibit[data-exhibit="ausnz"] .map-name{--name-x:32%}
  .map-scene .exhibit .halo{opacity:.42}
  /* Below about 1000px the frame is much wider than the window, so the far-right
     region hangs past the edge and takes its name with it. A smaller label fits,
     and is the right size for the smaller room anyway. Measured at 840x700. */
  @media (max-width:1000px){
    .map-scene .map-name{font-size:.74rem;max-width:10rem;letter-spacing:.14em}
  }
  .map-scene .exhibit:hover .halo,.map-scene .exhibit:focus-visible .halo{opacity:1}
  .map-scene .map-name{opacity:.82;transition:opacity .38s var(--ease-veil),
    letter-spacing .38s var(--ease-veil)}
  .map-scene .exhibit:hover .map-name,.map-scene .exhibit:focus-visible .map-name{
    opacity:1;letter-spacing:.26em}
  /* 2026-08-13: the room's two offerings stand in the middle of the cushion circle,
     which is where the gathering itself sits. ONE wrapper, absolutely positioned
     against .scene-frame (the artwork's own box, the same box the five continents
     measure against), so the row stays on the cushions at every window size. The
     pills themselves keep the styling the single bottom-edge cue already had -- they
     only stop positioning themselves. */
  .map-cues{position:absolute;left:50%;top:80.4%;transform:translateX(-50%);z-index:5;
    display:flex;align-items:center;gap:1.4rem}
  .map-cue{position:relative;z-index:5;
    display:flex;flex-direction:column;align-items:center;gap:.1rem;pointer-events:none;
    color:#5a4a2a;text-shadow:0 1px 8px rgb(255 253 246 / .95),0 0 18px rgb(255 253 246 / .8);
    text-decoration:none;cursor:pointer;padding:.55rem 1.1rem;min-height:44px;
    justify-content:center;border-radius:999px;pointer-events:auto;
    transition:opacity .6s ease,color .35s ease,background .35s ease}
  .map-cue:hover,.map-cue:focus-visible{color:#3f3757;background:rgb(255 253 246 / .5)}
  .map-cue span{font-family:var(--font-serif);font-size:.6rem;letter-spacing:.32em;
    text-transform:uppercase}
  .map-cue .arr{font-size:1rem;animation:map-cue-bob 2.6s ease-in-out infinite}
  /* it belongs to the room, so it stands down while a region's card is open */
  .exhibit-scene.opened .map-cue{opacity:0}
  @keyframes map-cue-bob{0%,100%{transform:translateY(0)}50%{transform:translateY(5px)}}
}
@media (min-width:821px) and (min-height:501px) and (prefers-reduced-motion:reduce){
  .map-cue .arr{animation:none}
}

/* =====================================================================
   THE LIBRARY — five objects, one per section (Director, 2026-08-04)
   "I use images and objects to give the room a feeling ... a staircase in
   the room for another level and its sectioned off by rope for gold
   members and for the tribute I felt two large photoframes of the two."

   No new engine. This room is an .exhibit-scene like Alchymie and the
   Gathering Room: hover lights a hotspot, the click grows its card out of
   the object. Everything below is only what that reuse actually needed.
   ===================================================================== */

/* 1. THE TWO PAINTINGS ARE ONE TRIBUTE, SO THEY LIGHT AS ONE.
      Two hotspots, one card. :has() does it with no wrapper, which matters:
      .exhibit is absolutely positioned off the scene, so a wrapper element
      would break both their geometry. */
.library-scene:has(.ex-founder:hover) .ex-founder .halo,
.library-scene:has(.ex-founder:focus-visible) .ex-founder .halo{opacity:1}

/* 2. THE TWO GATED OBJECTS REST LIT, THE THREE FREE ONES REST DARK.
      Alchymie's rule (the room does not light itself) is right for objects a
      member can already open: lighting them unasked only ever spoke to
      someone who had said they were done being shown things. It is wrong for
      a LOCKED one. A gate nobody can see is not a gate, it is an absence, and
      the Silver and Gold offers would never be made at all. Same argument
      that made the Gathering Room light its own five regions.
      Hover still has somewhere brighter to go, so "this one, now" stays a
      different sentence from "there is more here than you hold". */
.library-scene .exhibit[data-gate] .halo{opacity:.3}
.library-scene .exhibit[data-gate]:hover .halo,
.library-scene .exhibit[data-gate]:focus-visible .halo{opacity:1}

/* 3. THE TITLES UNDER THE PAINTINGS.
      Director: "like the titles above the doors as well, we will need under
      the paintings". The lobby's door titles are painted INTO its artwork.
      These are not: cut in HTML they stay crisp at any width, they can be
      read by a screen reader, and they survive a re-render of the room. Ink
      with a light halo, never pale gold on ivory marble, which is the mistake
      the Alchymie walkthrough pointer made and it read as nothing. */
/* (the .plaque rules that stood here had no call sites since v7; deleted 2026-08-19) */

/* 3b. THE MUSEUM PLATE was here and is deliberately GONE (2026-08-09). The two
      founders' names were briefly a CSS gold plate, because the v6 render left
      only 8px of bare wall under each picture and a plate baked into the artwork
      came out unreadable. The v7 whole-room generation mounts a real plaque with
      clear wall around it, so the names are engraved into the picture again by
      art-source/library/v7-finish.py and the rule had no call sites left. */

/* 4. THE ROPE IS NOT IN THE ARTWORK. The two brass stanchions are (the third was taken away on the Director's word); the cord
      is drawn here, over the top. That is what makes the Silver reward possible
      at all: [data-need="silver"] hides this one element, so a Silver member's
      room is THE SAME PIXELS with the cord gone, not a second render that
      would drift the way the Meditation Chamber's twin renders drifted.
      It is also why it may never take a click: the staircase behind it is the
      hotspot, and a cord swallowing pointer events would kill the room. */
.rope{position:absolute;inset:0;width:100%;height:100%;z-index:2;
  pointer-events:none;overflow:visible}

/* 4b. THE FLOATING BOOK IS GONE (Kirra, 19/8: "I am not a fan of floating
      books - its too 'Harry Potter'. Can we have pop out text windows instead.
      For each Article."). The .shelf-cue rules that stood here (a CSS gold book
      bobbing beside each live row, Director 2026-08-09) are deleted with their
      spans; git before this commit has them. What advertises a row now is the
      Alchymie Room's hover plate (.exhibit--labelled, above): all ten shelf rows
      carry one, reading "Browse our Articles here" and the article's title, and
      each row opens its own card.
      .ex-shelf only MOVES the plate: a row is about 120x45px of a 1536px room,
      so a 14rem plate centred on it would sit over the very books it names and
      spill off the left edge of the scene. It hangs to the right of the row
      instead, on the pilaster and wall, left-aligned, riding the same hover and
      focus fade as every other plate. `small` is the title line under the
      heading phrase, set in the plate's own ink at reading case. */
/* text-align was `left` here from when this was a bare label hanging off the shelf.
   It is a capsule now and the Director's rule is that all of it reads centred
   (2026-08-25). */
.ex-shelf .plate{left:calc(100% + .7rem);transform:translateY(-50%);text-align:center;max-width:15rem}
.ex-shelf .plate small{display:block;margin-top:.3em;font-size:.74em;letter-spacing:.06em;
  text-transform:none;color:#5c5375}

/* ⚠ THE HIDDEN ATTRIBUTE DOES NOT HIDE AN SVG, AND THIS ONE LINE IS THE WHOLE
      GATE (found by measuring, 2026-08-09). Chrome's `[hidden]{display:none}`
      lives in the HTML user-agent stylesheet and does not reach elements in the
      SVG namespace: a bare <svg hidden> still computes display:inline, and this
      one, being absolutely positioned, blockifies to display:block. Proven in
      the browser, not reasoned about: a <div hidden> computed `none` on the same
      page where a <svg hidden> computed `inline`.
      So the rope hung there for every member regardless of key. Two things had
      to change together and BOTH are load-bearing: runRoomGate() now sets the
      attribute with toggleAttribute (assigning `.hidden` on an SVGElement only
      creates a meaningless expando, because `hidden` is defined on HTMLElement),
      and this rule is what actually takes it off the wall.
      Written for every svg[hidden] on the site, not just .rope, because any
      future SVG given a [data-need] would hit exactly the same wall. */
svg[hidden]{display:none}
/* 2026-08-19, Director: the Silver rope is drawn on the phone too. It is the Silver gate and the
   stairs hotspot behind it is live there; hiding the cord hid the gate. The .plaque half of this
   rule went with the .plaque rules above. */

/* 5. THE GLOSSARY CARD. 101 entries and about 7,200 words cannot be a card
      that grows out of a lectern and then scrolls the page behind it. The
      picture column every other exhibit card uses becomes the alphabet rail,
      standing like the gilt letters on a shelf end, and the reading column
      scrolls inside itself.
      Selected with :has() because the card element is fixed in the page and
      only its contents are swapped, so it cannot be given a class. This sits
      after .exhibit-card:not(:has(.shot)) on purpose: that rule would
      otherwise squeeze the glossary into a 38rem single column. */
/* 52rem, not the 74 this started at: at 74 a definition ran about 100 characters
   to the line and read like a spreadsheet. Tall and narrow is also the right
   shape for the thing, which is a reference book standing open. */
.exhibit-card:has(.gl-rail){grid-template-columns:auto minmax(0,1fr);
  max-width:min(52rem,94vw);width:52rem;height:min(84svh,54rem)}
.gl-rail{display:flex;flex-direction:column;justify-content:center;gap:.06rem;
  padding:1.4rem .85rem;border-right:1px solid rgb(217 182 98 / .4);
  background:linear-gradient(180deg,#fdfaf2,#f7f0e0)}
.gl-rail a{display:grid;place-items:center;width:2rem;height:1.62rem;
  border-radius:6px;text-decoration:none;font-family:var(--font-serif);
  font-size:.82rem;letter-spacing:.04em;color:#6f5a26;
  transition:background .18s ease,color .18s ease}
.gl-rail a:hover,.gl-rail a:focus-visible{background:rgb(217 182 98 / .26);color:#3f3757}
.gl-rail .is-empty{color:#c9bfa6;pointer-events:none}
/* align-self:stretch is LOAD BEARING and it undoes .exhibit-body's align-self:
   center. Centred, this column is only as tall as its content, so the flex box
   below it had no height to bound and .gl-entries never scrolled: all 101
   entries simply grew out through the bottom of the card and the letter rail
   jumped to nothing because there was no scroller to jump. */
.gl-body{display:flex;flex-direction:column;min-height:0;align-self:stretch;
  padding:clamp(1.5rem,2.4vw,2.2rem) clamp(1.2rem,2.4vw,2.2rem) 0}
/* ⚠ A LONG EMAIL ADDRESS IS AN UNBREAKABLE WORD AND IT PUSHES A NARROW PHONE
   SIDEWAYS. info@theesotericschoolofhigherlearning.org is 42 characters with no
   space and no hyphen in it, so the browser cannot break it anywhere: on a Galaxy
   S24 it ran privacy.html 8px off the side, and 49px off on an older small phone.
   Found during the mobile pass and NOT caused by it: it has been like that since
   the page was written. Scoped to mailto links so it can only ever affect an
   address, never body prose, where anywhere-breaking reads as broken typesetting.
   No media query: an address that cannot fit should wrap at any width. */
a[href^="mailto:"]{overflow-wrap:anywhere}

/* THE GLOSSARY SEARCH (Director, 2026-08-10). Styled to the card rather than left
   as the operating system's own search widget, which is what it looked like when
   the mobile pass found it half-built and pulled it out.
   It sits between the standfirst and the list, in the flex column, so the list
   simply gets shorter and keeps scrolling. That costs the desktop about 40px of
   visible list, which is real and was his call to accept.
   font-size is 16px on the input and that number is load-bearing: iOS Safari
   zooms the entire page when you focus an input smaller than 16px, and this one
   lives inside a card that is already the width of the screen on a phone. */
.gl-search{flex:0 0 auto;position:relative;padding:0 1.1rem .55rem 0}
.gl-search .gl-q{width:100%;box-sizing:border-box;
  font-family:var(--font-sans);font-size:16px;letter-spacing:.01em;color:#3f3757;
  padding:.5rem .8rem .5rem 2rem;border:1px solid rgb(217 182 98 / .5);border-radius:var(--radius-sm);
  background:rgb(255 253 247 / .9) no-repeat left .62rem center/.92rem .92rem;
  background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='%23a08a4e' stroke-width='1.7'%3E%3Ccircle cx='7' cy='7' r='4.6'/%3E%3Cpath d='M10.4 10.4 14 14' stroke-linecap='round'/%3E%3C/svg%3E");
  outline:none;transition:border-color .25s,box-shadow .25s}
.gl-search .gl-q::placeholder{color:#9a9080}
.gl-search .gl-q:focus-visible{border-color:rgb(217 182 98 / .95);
  box-shadow:0 0 0 3px rgb(217 182 98 / .2)}
.gl-search .gl-q::-webkit-search-cancel-button{cursor:pointer}
.gl-count{margin:.3rem 0 0;min-height:1em;color:var(--color-text-muted)}
.gl-count:empty{display:none}
/* a letter with nothing left in it after a search reads the same as a letter that
   never had entries, which is the rail's existing language for "nothing here" */
.gl-rail a.is-filtered{color:#c9bfa6;pointer-events:none}
.gl-entries{flex:1;min-height:0;overflow-y:auto;overscroll-behavior:contain;
  padding:.2rem 1.1rem .4rem 0;scrollbar-width:thin}
/* scroll-margin-top is 0, and it was .6rem. scroll-margin-top scrolls SHORT of
   the target by that much, so jumping to S left the tail of the last R entry
   sitting above it and the jump read as "nearly". Measured: it reported
   Remembrance at the top, not Satori. */
.gl-entry{scroll-margin-top:0;padding:0 0 1.15rem;border-bottom:1px solid #f0e9d8}
.gl-entry:last-child{border-bottom:0}
.gl-entry h3{font-family:var(--font-serif);font-weight:500;font-size:1.02rem;
  color:#3f3757;margin:1.15rem 0 .4rem;letter-spacing:.01em}
.gl-entry p{margin:0 0 .7rem;font-size:.93rem;line-height:1.72;color:var(--color-text)}
.gl-entry a[data-xref]{color:#8a6d1f;text-underline-offset:3px}
.gl-body .exhibit-actions{margin:0;padding:.9rem 0 1.1rem;
  border-top:1px solid rgb(217 182 98 / .34)}

/* the phone: a sheet, not a modal over a scrolling page, and the rail lies
   down across the top instead of standing beside a column that is not there. */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  /* the veil's own padding is what kept the sheet 32px narrower than the phone,
     so it read as a big dialog rather than as a screen you moved to */
  .exhibit-veil:has(.gl-rail){padding:0}
  .exhibit-card:has(.gl-rail){grid-template-columns:1fr;width:100%;max-width:100%;
    height:100svh;border-radius:0}
  .gl-rail{flex-direction:row;justify-content:flex-start;overflow-x:auto;
    padding:calc(env(safe-area-inset-top) + .5rem) .6rem .5rem;
    border-right:0;border-bottom:1px solid rgb(217 182 98 / .4);
    scrollbar-width:none;position:sticky;top:0;z-index:2}
  .gl-rail::-webkit-scrollbar{display:none}
  .gl-rail a{flex:0 0 auto;width:2.75rem;height:2.75rem;font-size:.95rem}
  .gl-body{padding:1rem 1.1rem 0}
  .gl-body .exhibit-actions{padding-bottom:calc(env(safe-area-inset-bottom) + 1rem)}
}

/* ============================================================================
   WHAT YOU MAY TOUCH GIVES OFF LIGHT. Director 2026-08-06, after two rejected
   attempts: "more like AAA games to show there is something there, like little
   fireflies but gold filaments of light moving upwards. Like little shards of
   light." Also: "just in the rooms", not the door portals, and the onboarding
   walkthrough stays.

   So: shards RISE off the object and dissolve. Not threads lying across it,
   which is what the first build did and it read as scratches on the paintings.

   Two languages:
     THE GLOW   (the .halo, already there) rests low and breathes: "there is
                something here".
     THE SHARDS (new) lift small slivers of gold light off the object's surface
                and fade them out above it: "this is alive, come and touch it".

   ROOMS ONLY, .exhibit and never .arch: the lobby doors carry their name carved
   above them and were never the thing nobody could find.

   Hover, focus and press are excluded from the glow and still take the halo to
   1, keeping the rule the 2026-07-29 "THE ROOM DOES NOT LIGHT ITSELF" decision
   left behind: rest must never reach the hover peak. .is-demo is excluded too,
   so the walkthrough's ghost still lights the real objects at full.

   No new markup and no JavaScript. Two pseudo-elements carry three columns of
   shards each, at two different speeds, so the field never reads as a machine.
   ponytail: multiple background layers on one element beat six real elements.
   Movement is a transform of exactly one tile height, so it loops seamlessly
   and stays on the compositor instead of repainting every frame.
   ------------------------------------------------------------------------- */

/* ---- the glow ---- */
@keyframes halo-breath{0%,100%{opacity:.11}50%{opacity:.40}}
.exhibit:not(.is-demo):not(:hover):not(:focus-visible):not(:active) .halo{
  animation:halo-breath 5.5s ease-in-out infinite}

/* The two rooms that already rest lit on purpose keep their own resting value:
   the Gathering Room's regions at .42, the Library's gated objects at .3. */
.map-scene .exhibit .halo,
.library-scene .exhibit[data-gate] .halo{animation:none}

/* ---- the shards ---- */
@keyframes shard-rise-a{to{transform:translateY(-104px)}}
@keyframes shard-rise-b{to{transform:translateY(-146px)}}
/* The sparkle. Each layer catches and loses the light on its own rhythm, and
   the two rhythms are deliberately not multiples of each other or of the rise,
   so the field never visibly repeats. Director 2026-08-06: "I was hoping that
   they kind of animate sparkle." */
@keyframes shard-twinkle-a{
  0%,100%{opacity:.28}14%{opacity:1}27%{opacity:.42}44%{opacity:.9}
  58%{opacity:.34}72%{opacity:.98}88%{opacity:.46}}
@keyframes shard-twinkle-b{
  0%,100%{opacity:.4}19%{opacity:.92}33%{opacity:.3}51%{opacity:1}
  66%{opacity:.38}81%{opacity:.86}}

.exhibit::before,.exhibit::after{
  content:"";position:absolute;left:27%;right:27%;bottom:0;height:132%;
  pointer-events:none;z-index:2;
  background-repeat:repeat;
  /* they must be born out of the object and die in the air above it, never
     stop dead at an edge, or the hotspot reads as a box on the artwork */
  -webkit-mask-image:linear-gradient(to top,transparent 0,#000 16%,#000 58%,transparent 93%);
  mask-image:linear-gradient(to top,transparent 0,#000 16%,#000 58%,transparent 93%)}

/* PALE CHAMPAGNE, NOT AMBER. Director 2026-08-06: "there too yellow gold." The
   saturated gold that made them visible also made them read as yellow rain, so
   the core is now a warm white and only the bloom carries any colour at all.
   What buys back the visibility the colour gave up is the twinkle: every mote
   reaches full opacity on its own beat instead of sitting at a constant value.

   Lengths are deliberately mixed. All-equal slivers read as rain; a few long
   filaments among short sparks read as light coming off an object. */
.exhibit::before{
  background-image:
    radial-gradient(ellipse 1.9px 17px at 50% 18%,rgb(255 250 238 / .95),rgb(246 224 172 / .38) 34%,rgb(226 190 120 / 0) 78%),
    radial-gradient(ellipse 1.5px 5px  at 50% 44%,rgb(255 248 232 / .9),rgb(244 220 166 / .34) 36%,rgb(226 190 120 / 0) 80%),
    radial-gradient(ellipse 2.2px 22px at 50% 68%,rgb(255 252 244 / .8),rgb(248 228 180 / .32) 32%,rgb(226 190 120 / 0) 76%),
    radial-gradient(ellipse 1.6px 7px  at 50% 88%,rgb(255 246 226 / .92),rgb(244 218 162 / .36) 35%,rgb(226 190 120 / 0) 79%),
    radial-gradient(ellipse 1.3px 4px  at 50% 6%, rgb(255 252 242 / .85),rgb(246 224 172 / 0) 74%);
  /* CONSTANT DENSITY, so a big object gets proportionally more light rather
     than the same ten motes spread thin across it (Director 2026-08-06: "for
     the larger objects maybe it's more concentrated"). The tile repeats on both
     axes now, and every layer uses a different, deliberately non-harmonic tile
     width so the layers interfere instead of stacking into a visible grid. */
  background-size:47px 104px,58px 104px,71px 104px,41px 104px,83px 104px;
  background-position:0 0,17px 0,31px 0,8px 0,38px 0;
  animation:shard-rise-a 6.5s linear infinite,shard-twinkle-a 2.3s ease-in-out infinite}

.exhibit::after{
  background-image:
    radial-gradient(ellipse 1.4px 6px  at 50% 30%,rgb(255 250 236 / .92),rgb(246 224 172 / .35) 35%,rgb(226 190 120 / 0) 78%),
    radial-gradient(ellipse 2.1px 20px at 50% 58%,rgb(255 252 244 / .82),rgb(248 228 180 / .3) 33%,rgb(226 190 120 / 0) 77%),
    radial-gradient(ellipse 1.6px 9px  at 50% 82%,rgb(255 248 230 / .88),rgb(244 220 166 / .34) 34%,rgb(226 190 120 / 0) 79%),
    radial-gradient(ellipse 1.2px 4px  at 50% 12%,rgb(255 253 246 / .8),rgb(246 224 172 / 0) 76%),
    radial-gradient(ellipse 1.8px 14px at 50% 70%,rgb(255 250 238 / .78),rgb(246 222 170 / .3) 34%,rgb(226 190 120 / 0) 78%);
  background-size:53px 146px,64px 146px,44px 146px,76px 146px,61px 146px;
  background-position:24px 0,6px 0,35px 0,18px 0,46px 0;
  animation:shard-rise-b 9.5s linear infinite,shard-twinkle-b 3.1s ease-in-out infinite}

/* Nothing may rise in unison, which reads as a machine rather than a room.
   ponytail: negative delays start them mid-flight, so none waits to appear. */
.exhibit:nth-of-type(4n+2) .halo{animation-delay:-1.3s}
.exhibit:nth-of-type(4n+3) .halo{animation-delay:-2.7s}
.exhibit:nth-of-type(4n+4) .halo{animation-delay:-4.1s}
.exhibit:nth-of-type(4n+2)::before{animation-delay:-2.1s,-.7s}
.exhibit:nth-of-type(4n+3)::before{animation-delay:-4.3s,-1.4s}
.exhibit:nth-of-type(4n+4)::before{animation-delay:-5.6s,-1.9s}
.exhibit:nth-of-type(4n+2)::after{animation-delay:-3.4s,-1.1s}
.exhibit:nth-of-type(4n+3)::after{animation-delay:-6.2s,-2.2s}
.exhibit:nth-of-type(4n+4)::after{animation-delay:-8.1s,-2.8s}

/* Reduced motion keeps the ANSWER and drops the movement: a member who asks for
   stillness still needs to know what opens, so the shards hold where they are. */
@media (prefers-reduced-motion:reduce){
  .exhibit:not(.is-demo):not(:hover):not(:focus-visible):not(:active) .halo{
    animation:none;opacity:.2}
  .exhibit::before,.exhibit::after{animation:none}
}

/* The member's own say in the rising light (Settings > Light on the objects).
   Only the shards go. The GLOW stays, because it is what still answers "is
   there anything here" once the sparkle is gone, and hover still answers "this
   one, now". Turning the sparkle off must not turn the room into a guessing
   game. */
html.shards-off .exhibit::before,
html.shards-off .exhibit::after{display:none}

/* ===== THE REST OF THE ACCOUNT, TO THE SAME RULE ========================
   Settings, Billing and Help were the same fault as the Keys: a stack of
   separate cream cards, each bordered and shadowed, sitting on a page already
   made of cream cards. They become one white panel of divided rows too, so the
   four tabs read as one account rather than four different pages.
   The cream now means exactly one thing on this screen: the rail. */
.acct-panel .card{background:var(--color-bg);box-shadow:none;margin:0;
  border:1px solid var(--color-border);border-bottom-width:0;border-radius:0;
  padding:1.25rem var(--space-3)}
.acct-panel .card:first-of-type{border-radius:var(--radius-md) var(--radius-md) 0 0}
.acct-panel .card:last-of-type{border-bottom-width:1px;
  border-radius:0 0 var(--radius-md) var(--radius-md)}
.acct-panel .card h3{font-size:1.05rem;margin:0 0 .3rem}
.acct-panel .card .acct-note{margin:0 0 .75rem;max-width:52ch}
.acct-panel .card > :last-child{margin-bottom:0}
/* A setting is a row: what it is on the left, the control on the right, both on
   the same top line. The checkbox used to sit under its own paragraph, which is
   why the words looked scattered rather than tabulated. */
/* The control column is a FIXED width, not `auto`. Each row is its own grid, so
   an auto column sized itself to that row's own label and every control landed
   at a different x down the page. That was the "words are all over the place"
   fault in miniature, and it is the difference between a settings list and a
   pile of settings. */
.acct-panel .card:has(.onboard-again),
.acct-panel .card:has(> .btn){display:grid;
  grid-template-columns:minmax(0,1fr) 15rem;gap:0 var(--space-3);align-items:start}
.acct-panel .card:has(.onboard-again) h3,
.acct-panel .card:has(.onboard-again) .acct-note,
.acct-panel .card:has(> .btn) h3,
.acct-panel .card:has(> .btn) .acct-note{grid-column:1}
.acct-panel .card:has(.onboard-again) .acct-note,
.acct-panel .card:has(> .btn) .acct-note{margin-bottom:0}
.acct-panel .card .onboard-again,
.acct-panel .card > .btn{grid-column:2;grid-row:1 / span 2;
  align-self:center;justify-self:start;margin:0;white-space:nowrap}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .acct-panel .card{padding:1.1rem var(--space-2)}
  .acct-panel .card:has(.onboard-again),
  .acct-panel .card:has(> .btn){grid-template-columns:1fr}
  .acct-panel .card .onboard-again,
  .acct-panel .card > .btn{grid-column:1;grid-row:auto;margin-top:.85rem}
}

/* An add-on inside a Key: something you can buy WITHOUT changing which Key you
   hold. It sits below that Key's unlocks, behind a hairline, so it reads as
   "and also" rather than as a fourth Key. Director 2026-08-06. */
.key-addon{display:grid;grid-template-columns:minmax(0,1fr) auto auto;
  align-items:center;gap:.25rem .9rem;
  margin:.9rem 0 0;padding:.85rem 0 0;border-top:1px solid var(--color-border)}
.key-addon-name{margin:0;font-weight:500}
.key-addon-price{margin:0;font-size:.95rem;color:var(--color-text-muted);white-space:nowrap}
.key-addon-btn{padding:.35rem 1.1rem;font-size:.88rem}
.key-addon-note{grid-column:1 / -1;margin:.35rem 0 0;color:var(--color-text-muted)}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .key-addon{grid-template-columns:minmax(0,1fr) auto}
  .key-addon-btn{grid-column:1 / -1;justify-self:start;margin-top:.5rem}
}

/* ===== THE ACCOUNT BELONGS TO THE TEMPLE ================================
   Director 2026-08-06: "this has to be the ugliest account page I've ever seen.
   Did you even use /impeccable?"

   Fair, and the honest answer is that the last pass used it to clear rule
   breaches (an eyebrow, a dingbat, nested cards) and never committed to a look.
   Clearing faults makes a page correct, not good. Four things were actually
   wrong, and all four are about the page having no presence:

   1. NO GROUND. Flat white, while every other surface on this site is warm
      marble in daylight. The account read as a form bolted onto a temple.
   2. DEAD SPACE. The panel was capped at 47rem inside a 54rem column inside a
      74rem page, so the content huddled top-left with an empty white half.
   3. PAYPAL RAN THE PAGE. A saturated yellow third-party button was the single
      loudest thing on a screen whose brand is unhurried ivory and gold.
   4. NO WEIGHT ANYWHERE. Everything was one size, so nothing led. */

/* 1. THE GROUND. The same daylight the rooms stand in: warm at the edges, light
      falling from above. Fixed, so it does not slide under the content. */
/* Both layers fixed, and put on <html> as well, or a page shorter than the
   viewport leaves a bare white band under the content where the ground stops. */
html:has(.acct),body:has(.acct){
  min-height:100%;
  background:
    radial-gradient(120% 62% at 50% -8%, rgb(255 252 244 / .95), rgb(255 252 244 / 0) 60%) fixed,
    linear-gradient(180deg,#f7f1e6 0%,#fbf7f0 38%,#f5eee2 100%) fixed}
body:has(.acct){background-color:#f7f1e6}
.acct{max-width:66rem}
/* 2. FILL THE COLUMN. The list is the content; it has no business being narrower
      than the space it was given. */
.acct-panel .keys-grid{max-width:none}
.acct-panel .card{max-width:none}
/* Panels sit ON the ground, so they carry a real edge and a real lift. */
.keys-grid,.acct-panel .card:first-of-type,.acct-panel .card{background:#fffdf9}
.keys-grid{border-color:rgb(200 178 130 / .5);box-shadow:0 10px 34px rgb(120 90 30 / .09)}
.acct-panel .card{border-color:rgb(200 178 130 / .45)}

/* 3. PAYPAL IS A GUEST, NOT THE HOST. It cannot be restyled, so it is given a
      narrower slot: at this width the yellow stops being the loudest thing on
      the page and sits inside the row instead of running it. */
.key-action{max-width:11.5rem}
.paypal-slot{max-width:11.5rem}

/* 4. WEIGHT. The Key you are looking at should read before its price does. */
.key-card h2{font-size:1.55rem;letter-spacing:-.01em}
.key-card{padding:1.6rem var(--space-3)}
.key-card.current{background:linear-gradient(90deg,rgb(247 236 205 / .55),rgb(255 253 249 / .5) 60%);
  box-shadow:inset 3px 0 0 var(--color-primary)}

/* THE RAIL CARRIES THE MEMBER'S OWN KEY, so it is the one place on this page
   that may be an object rather than a panel: the metal of the Key they hold,
   on warm stone, under a gold hairline. */
.acct-me{background:linear-gradient(165deg,#fdf6e6,#f6ecd9 60%,#f1e4cd);
  border-color:rgb(196 168 108 / .55);
  box-shadow:0 8px 26px rgb(120 90 30 / .1),inset 0 1px 0 rgb(255 255 255 / .7)}
.account-key-badge{width:4rem;height:4rem;margin-bottom:1rem}
.account-key-name{font-size:1.45rem}
.acct-nav a{border-radius:var(--radius-sm)}

/* ===========================================================================
   THE PHONE, 2026-08-10 (Director: "make this mobile compatible for the entire
   website ... it's not gonna work like the desktop ... the cards have to come
   over the top automatically instead of clicking on an aspect of the screen.
   Optimise for mobile on all levels, NOT touching the desktop.")

   EVERYTHING NEW IN THIS PASS IS IN HERE, and 820 is the only device line left.
   The 700 and the 780 were renamed onto it by find-and-replace and the dead 900
   was deleted, so this file now answers exactly three questions:
     820  is this a phone — no hover, one column, a thumb.     THE DEVICE LINE.
     920  does this two-column card still fit.                 A FIT QUESTION.
     1024 do the seven nav tabs still fit.                     A FIT QUESTION.
   Renaming 700 to 820 cannot reach the desktop, because no rule written at
   700/701/780/781/900 ever applied at 1025. Measured, not asserted: full
   computed style plus box geometry for every element on 26 pages, at 1025 and
   at 1512, before and after — zero differences.

   NO THIRD ENGINE WAS WRITTEN. This site already owned both of the ones the
   Director asked for and both are the browser's own scroll-snap, not drag maths:
     THE PANORAMA, for artwork that carries DOORS. The scene becomes a sideways
       scroller, the room runs past both edges, every .arch is a snap point.
       Already live on the lobby, teaching (4 doors) and practitioner (2).
     THE CARD RAIL, below. For rooms whose artwork carries no doors: the picture
       stays as the fixed backdrop it already is, and the cards come over the top
       by themselves — one screen, swiped sideways, nothing to hunt for.
   The gate is MARKUP, never a page list: `.temple-scene:has(.door-deck)` picks
   the panorama, `.room-panel:has(.pane)` picks the rail. There is not one
   per-room selector in this whole block, so four people working four rooms
   cannot make four different rooms.
   =========================================================================== */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){

  /* ---- 1. THE CARD RAIL ---------------------------------------------------
     Any room panel carrying .pane cards stops being a column you scroll down
     and becomes one screen of cards you swipe across, standing over the room.
     This is the lobby's own mechanism pointed at a different element: the
     momentum, the snap and the lock are all the browser's.

     ⚠ NEVER SET `display` ON `.pane` IN HERE. The Library's two tier-locked
       panes are gated with the `hidden` attribute, and any display value beats
       it — the same trap `.exhibit{display:block}` and `.exhibit-veil{display:grid}`
       both fell into further up this file. Verified: 5 of 7 panes in the rail's
       measured scrollWidth, the two gated ones still computing display:none.

     ⚠ NO `touch-action` HERE, and that is deliberate rather than forgotten. The
       panorama sets `touch-action:pan-x` because nothing in it scrolls vertically.
       These cards do. Copying pan-x across from the panorama would kill the inner
       scroll of every card that has more to read than fits. */
  .room-panel:has(.pane){
    /* width:auto, NOT 100vw: 100vw includes the classic scrollbar, so on a narrow
       desktop WINDOW the rail would stand ~15px wider than its container and put a
       horizontal scrollbar on the page. The parent is already full bleed. */
    width:auto;max-width:none;margin:0;height:100svh;
    /* ⚠ `stretch`, AND IT STAYS `stretch`. It was changed to `center` earlier on
       2026-08-11 to stop the short cards drawing a full-height slab of cream, and the
       Director's answer was immediate: "the cards are all different sizes and
       shouldn't be". He is right — a rail you swipe reads as one shelf, and ragged
       card heights make it read as a pile. One size, every card, every room.
       The emptiness that change was chasing is real, and it is fixed the way he asked
       for instead: by putting something in the card. See the Alchymie room's Heart of
       Space shot. A short card wants CONTENT, not a smaller box. */
    display:flex;align-items:stretch;gap:.7rem;
    /* svh, never lvh. Sized in lvh this scroller stands taller than the visible
       phone whenever the toolbar is showing, and the PAGE then scrolls by exactly
       the toolbar's height — which is the one thing the rail exists to remove. */
    padding:calc(var(--bar-h) + 1.6svh) 7vw calc(2.4svh + env(safe-area-inset-bottom));
    overflow-x:auto;overflow-y:hidden;
    scroll-snap-type:x mandatory;overscroll-behavior-x:contain;
    scrollbar-width:none}
  .room-panel:has(.pane)::-webkit-scrollbar{display:none}
  /* 86vw cards inside 7vw gutters IS the swipe cue: the next card's edge is always
     showing, so no arrow, no dots and no "swipe" label are added. Same argument the
     lobby's panorama makes by letting the room run past both edges. */
  /* A shot at the head of a rail card. Director, 2026-08-11: the Alchymie room's Heart
     of Space card carried the words but not the picture the desktop exhibit leads with.
     Sized by aspect-ratio rather than a height, so it keeps its shape at any card width
     and cannot push the card's own scroll around when the artwork decodes. `top center`
     matches `.exhibit-card .shot img`, so the phone and the desktop crop the same web
     page the same way. */
  .room-panel:has(.pane) .pane-shot{margin:0 0 1rem;aspect-ratio:16/10;overflow:hidden;
    border-radius:var(--radius-sm);background:#efe7d5;
    border:1px solid rgb(217 182 98 / .4)}
  .room-panel:has(.pane) .pane-shot img{display:block;width:100%;height:100%;
    object-fit:cover;object-position:top center}
  .room-panel:has(.pane)>.pane{
    /* the other half of `align-items:stretch` above: one height, every card. */
    flex:0 0 86vw;height:100%;margin:0;
    scroll-snap-align:center;scroll-snap-stop:always;
    overflow-y:auto;overscroll-behavior-y:contain;
    /* .88, and it is not taste. Measured against the rendered photograph behind it,
       the base .3 glass put "Dates to be released soon." at 2.65:1 on courses and
       the sessions footnote at 3.88:1 — both below the 4.5:1 body text needs. At
       .88 the same two strings measure 4.76:1 and 6.86:1. Do not thin it.
       The card IS the scrim, which is why no dimming layer is added over the
       artwork: the room stays the room, at full brightness, in the gutters. */
    background:rgb(255 253 246 / .88);
    /* and with an opaque field there is nothing left to see through, so the blur is
       pure cost: five to seven blurred layers over a fixed cover image inside a
       snapping scroller is the classic iOS stutter. Measured still computing
       blur(22px) on all four rail rooms before this line. */
    backdrop-filter:none;-webkit-backdrop-filter:none;
    /* ⚠ THE FADE THAT SAYS "THERE IS MORE". The four sessions cards measure
       scrollHeight/clientHeight of 834/748, 1215/748, 1515/748 and 1771/748, so the
       last one hides 1023px — 1.37 card-screens — below its own fold, and the
       paragraph was being chopped mid-word at a hard card edge with nothing to
       suggest it continued. iOS and Android overlay scrollbars are invisible at
       rest, so the card looked finished. This is the SAME gradient the deleted
       sphere wall carried on .pane-3d, moved onto the element the phone actually
       scrolls.
       ⚠ 1.4rem, NOT the wall's old 92% or a round 94%. A percentage fade is 45px on a
       748px card and it lands on real text — but only 4 of the 15 panes on this site
       actually overflow, so on the other 11 it was dimming the last line of a card
       that had already finished, which says "there is more" where there is not.
       1.4rem is exactly the pane's own minimum padding-bottom, and in a scroll
       container that padding sits at the END of the scrollable content, not at the
       bottom of the box. So on a card that FITS the fade covers padding and nothing
       else — invisible. On a card that overflows, the padding is far below the fold
       and the fade lands on the cut-off line, which is the whole point. One value,
       both cases, no JavaScript asking how tall anything is. */
    mask-image:linear-gradient(180deg,#000 0,#000 calc(100% - 1.4rem),transparent);
    -webkit-mask-image:linear-gradient(180deg,#000 0,#000 calc(100% - 1.4rem),transparent)}
  /* The pane deliberately keeps its scrollbar while the rail hides its own. Up to
     1023px of a sessions card lives below its own fold, and a scroller with no
     scrollbar, no fade and no cue is content that is simply gone. iOS and Android
     both draw overlay scrollbars — invisible at rest, a flick of a hairline the
     moment a finger moves — which is the native affordance, for free, by NOT
     writing the two lines that would have hidden it. */

  /* ---- 2. room-zoom: the gathering list ----------------------------------
     This page is the exception that proves the gate: its three .room-panel
     elements carry zero .pane, so `:has(.pane)` correctly leaves them alone and
     they stay a vertical stack. A list of dated events is a document.
     Two of the three are also `.card`, i.e. the element is doing double duty as
     a layout wrapper AND as a visible card, so it was drawing 197px of inherited
     page padding around 87px of content. The wrap carries the page clearance now
     and the card carries a card's padding. Only #zoom-live is zeroed: naming
     `.gathering-panel .room-panel` here would out-specify `.room-panel.card`
     and strip the two real cards back to no vertical padding at all. */
  .room-panel.card{padding:var(--space-3)}
  .gathering-panel{padding-top:calc(var(--bar-h) + 3svh);
    padding-bottom:calc(9svh + env(safe-area-inset-bottom))}
  #zoom-live{padding-top:0;padding-bottom:0}

  /* ---- 3. five cards that all said the same thing -------------------------
     2026-08-12: the g-when/g-title hierarchy moved to the desktop base rule
     (near .circle-head), so this block no longer reorders anything. It only
     stacks the row and stretches the button, which is all a narrow screen
     still needs. */
  #zoom-live .gathering .row{flex-direction:column;align-items:stretch}
  #zoom-live .gathering .btn{width:100%;margin-top:.6rem}

  /* ---- 4. what sits on the phone's bottom edge ---------------------------
     .pan-plate, .deck-cue--pan, .mroom, .room-plates, .gl-body and
     .exhibit-actions already carry the home-indicator inset. These two did not,
     so on a notched iPhone the music button sat under the indicator.
     .deck-cue is NOT here on purpose — see its own rule near the top of the
     file, where it has to sit before .deck-cue--pan to avoid overriding it.
     .map-cue needs nothing: it lives inside @media (min-width:821px). */
  .music-toggle{bottom:calc(1rem + env(safe-area-inset-bottom));
    right:calc(1rem + env(safe-area-inset-right))}
  /* ⚠ THE TOP INSET IS ON THIS RULE BECAUSE IT CANNOT GO WHERE IT BELONGS.
     account.html ships `<main style="padding-top:60px">` as an INLINE attribute, and
     an inline style beats every stylesheet rule, so the one page whose clearance is
     hardcoded is the one page --bar-h could not reach. 60 is the bar WITHOUT the
     notch; with a 47px inset live the bar's bottom is at 107 and the shortfall is
     exactly 107 - 60 = 47 = env(safe-area-inset-top). Measured at 390x844 with the
     inset: "Your account" sat at y=84, 23px behind the bar, and on #keys the
     `< Account` link sat at y=76 with 31px of its 44px target covered. Adding the
     inset here clears both without touching the attribute (removing it would move
     desktop) and it cannot reach 1025px.
     ⚠ It is `var(--space-2) + env()`, not a bare `env()`: --space-2 is the 24px gap
     .acct already has between the bar and the title, and a bare env() would replace
     it rather than add to it, landing the title exactly ON the bar's bottom edge. */
  .acct{padding-top:calc(var(--space-2) + env(safe-area-inset-top));
    padding-bottom:calc(var(--space-3) + env(safe-area-inset-bottom))}

  /* ---- 5. the sign-in card ------------------------------------------------
     It is position:fixed over a frozen page, so it needs a top, a floor, and a
     stop on the overscroll that otherwise chains straight into the panorama
     behind it. Centred, a 1636px create-account card in an 844px window left
     about 1000px of empty gold blur under the button and put the first field
     off screen; top-aligned it is an ordinary scroll with a real end.
     The 1.2rem side padding is KEPT: dropping it in the shorthand sends the card
     edge to edge, measured x=0 w=390 with both gutters gone. */
  .threshold-over{align-items:flex-start;overscroll-behavior:contain;
    padding:var(--bar-h) 1.2rem calc(4svh + env(safe-area-inset-bottom))}
  /* ...and the card's own 8vh top margin is now dead space under the bar rather
     than centring, because the overlay stopped centring. */
  .threshold-over .book-panel{margin-top:0;margin-bottom:0}
  /* The lobby's furniture is meaningless behind a sign-in card: STEP INSIDE opens
     a door you cannot reach, and "swipe" at 9.6px is the smallest text on the site.
     ⚠ `:not([hidden])` IS THE WHOLE RULE. lobby.html and index.html both ship
       `<div id="threshold-mount" class="threshold-over" hidden>` from first paint,
       and :has() does not care about the hidden attribute — without the guard this
       permanently deletes the plate and the cue from the one screen the Director
       called good, and the panorama loses its only 44px way in. */
  body:has(.threshold-over:not([hidden])) .deck-cue,
  body:has(.threshold-over:not([hidden])) .pan-plate{display:none}

  /* ---- 6. the 44px floor -------------------------------------------------
     The complete measured list of targets under 44px that are not inline links
     inside a sentence. WCAG 2.2 target-size exempts those explicitly and forcing
     them breaks the paragraph, so privacy's two mailto links, distributor's
     "write to us" and room-practitioners' "Sessions room" are left alone ON
     PURPOSE — they are not an oversight, do not chase them. */
  .porch-footer a{display:inline-flex;align-items:center;min-height:44px;padding:0 .55rem}
  /* The seek slider is 107x3 and is the worst target on the whole site: a 3px tall
     control that scrubs a meditation. The BAR grows to 44 and the painted track
     stays 3, so the room looks identical and the thumb has somewhere to land.
     `.practice .cinema-seek` is named as well as `.cinema-seek` because it is
     (0,2,0) and would otherwise put the height straight back to 3px. Below 820 the
     only cinema on screen is the meditation room's, so only the gold variant is
     drawn; the white desktop slider is untouched. */
  .cinema-seek,.practice .cinema-seek{height:44px;background:none;
    -webkit-appearance:none;appearance:none}
  .cinema-seek::-webkit-slider-runnable-track{height:3px;border-radius:2px;
    background:rgb(201 169 78 / .32)}
  .cinema-seek::-webkit-slider-thumb{-webkit-appearance:none;width:16px;height:16px;
    margin-top:-6.5px;border-radius:50%;background:#c9a94e;border:0}
  .cinema-seek::-moz-range-track{height:3px;border-radius:2px;background:rgb(201 169 78 / .32)}
  .cinema-seek::-moz-range-thumb{width:16px;height:16px;border-radius:50%;
    background:#c9a94e;border:0}

  /* ---- 7. contact.html's form: MOVED OUT OF THIS BLOCK on 2026-08-11 ------
     It is now unconditional, up beside `.book-panel` itself. See the note there. */

  /* ---- 8. room-howto and distributor ------------------------------------
     Both cards ran x=0 to x=390, edge to edge with no gutter, because those two
     pages set `padding:calc(56px + 5svh) 0 11svh` in an INLINE STYLE ATTRIBUTE on
     the .wrap-wide, and inline beats a stylesheet. This is the only !important in
     the pass and it is earned: the alternative is editing the inline 0, which
     would add a 32px gutter at 1440 and move the desktop. */
  .room-scene>.wrap-wide{padding-left:var(--space-2)!important;
    padding-right:var(--space-2)!important}

  /* ---- 9. the long course documents -------------------------------------
     The week label is the only orientation on an 18-screen page and it was the
     smallest type on it, at 11.52px.
     ⚠ ...and then the bar covered it. `.course-doc` carries `padding:calc(56px +
     4svh) 0 8svh`, which is the same hardcoded 56 this pass claims to have
     eliminated, surviving in a stylesheet rule instead of an inline attribute:
     89.76px at an 844 viewport against a bar whose bottom is at 107 once the notch
     is real, so the kicker measured y=89.8 on all four course pages, 17.2px behind
     the bar. --bar-h is only declared below 1024px, so this cannot reach desktop,
     and the 4svh rhythm is left exactly as it was. */
  .course-doc .kicker{font-size:.85rem}
  .course-doc{padding-top:calc(var(--bar-h) + 4svh)}

  /* ---- 10. the welcome card's rooms -------------------------------------
     `.onboard-room:hover` already loses its lift a few rules up, but it kept the
     border and the shadow, which latch after a tap on a touch screen. */
  .onboard-room:hover{border-color:var(--color-border);box-shadow:none}
}

/* ---- reduced motion, part two: the TRANSFORMS ------------------------------
   Killing the keyframes was only half of it. Measured under `prefers-reduced-motion:
   reduce` at 390x844: 0 keyframe animations and 0 infinite loops, correct — and yet
   four `a.door-card` on lobby.html and four more on room-teaching.html still computed
   `transition: transform 1.1s`, and `div.scene-zoom` still computed
   `transition: transform 1.15s` on room-meditation and room-sessions. Someone who
   asked for less movement still got the door cards sliding for over a second and the
   room pushing in behind them.
   ⚠ IT HAS TO BE HERE, AT THE END. `.scene-zoom` is redeclared at "the crossing"
     block (`transition:transform 1.15s`) with no media query and equal specificity,
     so it beats anything written earlier by source order alone — which is precisely
     why the `@media (prefers-reduced-motion: reduce){.scene-zoom{transition:none}}`
     that has been sitting up near the door-deck rules all along has never once
     applied. Do not move this block up to join it.
   ⚠ `transition-property`, not `transition-duration`, on the card: `.door-card`
     transitions transform AND opacity in one shorthand, and zeroing the duration
     would take the crossfade with it. A fade is not vestibular motion; a 1.1s
     transform push is. Naming opacity as the only transitioned property leaves the
     fade at its 1.1s and stops the transform dead.
   2026-08-11: THE WIDTH CLAUSES COME OFF, so this is now every width. It was written
   `(max-width:820px) and (prefers-reduced-motion:reduce)` because the desktop was
   frozen for the mobile pass, and the note left here said in as many words that the
   desktop half was a real defect needing its own line. This is that line, and it is
   the same one: a member above 820px who has asked their operating system for less
   movement was still being walked through a 1.15s scale-2.8 push on every door,
   because the `@media (prefers-reduced-motion: reduce){.scene-zoom{transition:none}}`
   at line 595 loses on source order to line 1293 and has never once applied.
   Reduced motion is not a phone feature, and this is the last rule in the file, so it
   wins where the one at 595 could not.
   Nothing moves for anyone who has NOT asked for reduced motion: the whole block is
   inside that query. `.door-card` is display:none above 820px, so its line is inert
   there and is kept only so the two halves stay one rule. */
@media (prefers-reduced-motion:reduce){
  .door-card{transition-property:opacity}
  .scene-zoom{transition:none}
}

/* 2026-08-19 (audit round): pill placement per object. The Press pill sat over the very
   lectern it labels, so it hangs under its hotspot; the Mezzanine pill ran off the right
   edge, so it hugs the right; the Inner Sanctum plate no longer repeats the carved lintel. */
.exhibit--soon .plate{top:calc(100% + .4rem);transform:translateX(-50%);max-width:18rem}
[data-exhibit="writeups"] .plate{left:auto;right:.5rem;transform:translateY(-50%)}
.onboard .btn:focus-visible,.guide-say .btn:focus-visible{outline-color:#8a6f2e}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .library-scene .ex-shelf{min-height:44px}   /* a shelf row is a tap target on the phone */
  .map-scene .map-cue{font-size:.72rem}}
.map-scene .map-cue{font-size:.72rem}

/* THE BOOKSHELF'S OWN HEADING (Kirra, 19/8). One plate beside the bookcase, in the same
   cream pill the doors and gated hotspots use, lit while any shelf is under the pointer or
   the keyboard. It never takes a click: the shelves are the targets. */
.library-scene .shelf-heading{position:absolute;left:3.4%;top:10.2%;
  margin:0;z-index:6;pointer-events:none;opacity:0;transition:opacity .35s var(--ease-veil);
  padding:.55em 1.2em;border-radius:999px;background:rgb(250 246 236 / .88);
  box-shadow:0 2px 14px rgb(60 45 25 / .12);color:#565073;
  font-family:var(--font-serif);font-size:.68rem;letter-spacing:.12em;
  text-transform:uppercase;white-space:nowrap}
/* the welcome card and the heading never fight: reading a shelf dims the welcome */
.library-scene:has(.ex-shelf:hover) .lib-welcome{opacity:.3}
.library-scene:has(.ex-shelf:hover) .shelf-heading,
.library-scene:has(.ex-shelf:focus-visible) .shelf-heading{opacity:1}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .library-scene .shelf-heading{display:none}}   /* the phone plate already names the article */

/* A shelf row is narrow and sits at the very left of the room, so its title cannot be
   centred on it: at that width the pill runs off the screen. It stands to the RIGHT of
   its own row instead, on the open floor, pointing at the shelf it opens. */
.library-scene .ex-shelf .plate{left:100%;top:50%;transform:translateY(-50%);margin-left:.7rem;
  max-width:20rem;white-space:normal;text-align:center}

/* =====================================================================
   2026-08-25 — THE MOBILE ROUND (the Director's list, in his order)
   Every rule below is additive or an override of a rule named in its own
   comment. This whole block can be deleted to return the phone to how it
   was on 2026-08-24.
   ===================================================================== */

/* ---- 1. THE WARP AT THE START OF EVERY DOOR FILM, ON iOS ONLY -------------
   Director: "on iOS whenever entering a room with the dove animation there is a
   warp in the video before it starts. This doesn't happen on an android."
   A DIFFERENT warp from the 2026-08-11 one documented up at `.door-film--full`
   (that one was the reset clamping the film's width; `max-width:none` fixed it).
   This one is the INTRINSIC SIZE BEFORE THE METADATA ARRIVES. `.door-film--full`
   is `width:auto;height:100%`, so the width is worked out from the film's own
   aspect — and until `loadedmetadata` fires there is no aspect to work from, so
   the box takes the default intrinsic video size every engine ships, 300x150.
   That is 2:1: a full-height box more than three times too wide. Chrome on
   Android resolves the real dimensions before it paints anything; iOS Safari
   paints the default box first and then snaps to 9:16, and the snap IS the warp.
   Declaring the ratio up front removes the guess: the box is the right shape from
   the very first frame, with or without metadata, on every engine.
   ⚠ MEASURED, NOT ASSUMED: every portrait film in assets/ is 720x1280 exactly
     (ffprobe, 2026-08-25), which is 9/16. seat() still sets an explicit width and
     height inline the moment the metadata is in, and an inline size outranks this,
     so a film re-rolled at another ratio corrects itself as it always did. This
     only governs the frames BEFORE that. Re-check it if crop-portrait.py ever cuts
     at something other than 9:16. */
.door-film--full{aspect-ratio:9/16}

/* ---- 2. THE LABEL THAT POPS UP AND THEN DISAPPEARS ------------------------
   Director, on the Courses room: "there is a weird added artefact when it loads,
   like a label pops up and then disappears, now in all the courses." And on the
   Alchymie room: "there is a capsule on load then it disappears."
   ONE CAUSE, AND IT IS A NAME COLLISION. `.plate` is two unrelated components in
   this stylesheet: the big threshold/welcome CARD (line ~2462, which legitimately
   wants `animation:plate-rise 760ms backwards`), and the little LABEL on a hotspot
   (line ~1914, which is `opacity:0` at rest and only comes up under a pointer).
   The card's animation matched the label too. `backwards` handed the label the
   animation's opening frame before it started, so it appeared at load, ran the
   760ms rise, and then fell back to its real resting style — invisible. Traced in
   a browser at 390x844: all four course labels visible at 120ms, opacity climbing
   0.63 -> 0.90 -> 0.98 -> 1.0, gone by 900ms.
   A hotspot label is not a card and never wanted an entrance. */
.arch .plate,.exhibit .plate{animation:none}

/* ---- 3. ...AND ON A PHONE THE LABEL STAYS UP ------------------------------
   Director: "it should also have a capsule under everyone."
   The resting state of a hotspot label is `opacity:0` because it rides a hover,
   and a finger has no hover — so on the phone these rooms were unlabelled objects
   and the only thing naming anything was the swipe plate at the bottom, which
   names ONE of them. Every object now carries its own capsule underneath it, at
   rest, permanently. The one you are standing in front of wears it at full
   strength; the ones either side of it are dimmed, so the room still says which
   one is yours without going quiet about the rest.
   ⚠ `.ex-shelf` IS EXCLUDED. The Library stacks TEN shelf hotspots at the same
     left:5.7%, one above another; ten capsules there would be ten labels in a pile.
     That room gets rule 4 below instead. */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .exhibit--labelled:not(.ex-shelf) .plate,
  .exhibit[data-exhibit="hos"] .plate{
    opacity:.62;top:auto;bottom:-.55rem;transform:translate(-50%,100%);
    width:max-content;max-width:min(44vw,12rem);
    padding:.42em 1em;border-radius:999px;
    background:rgb(250 246 236 / .84);box-shadow:0 2px 14px rgb(60 45 25 / .12);
    text-shadow:none;color:#565073;font-size:.6rem;letter-spacing:.16em;
    /* two lines at most: a long course title is three lines of capsule otherwise,
       and these are the objects you are NOT standing in front of */
    display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
    overflow:hidden;
    transition:opacity .38s var(--ease-veil)}
  /* ⚠ NOT ON THE ONE YOU ARE STANDING IN FRONT OF. The swipe plate at the top or
     bottom of the screen already names that one, larger and in the room's own
     serif, so lighting its capsule too printed the same words twice, one directly
     above the other. Measured at 390x844 in the Courses room: "Foundational
     Practice with the Alchymie" appeared as a three line capsule at y=1347 and
     again as the plate at y=1568. The capsules label the NEIGHBOURS, which is the
     thing the room could not say before: what else is along this wall. */
  .exhibit--labelled:not(.ex-shelf).locked .plate,
  .exhibit[data-exhibit="hos"].locked .plate{opacity:0}
  /* while a card is open the room behind it is not being read */
  .exhibit-scene.opened .exhibit--labelled .plate,
  .exhibit-scene.opened .exhibit[data-exhibit="hos"] .plate{opacity:0}
}

/* ---- 4. THE LIBRARY: YOU CANNOT TELL WHAT IS ON THE BOOKSHELF -------------
   Director: "in the library it's hard to tell what's on the bookshelf. The bottom
   capsule should say like tap different levels of the bookshelves, maybe it should
   have a glow or a way to define the different pieces of content."
   He is describing a real hole. The bookcase carries TEN separate pieces of
   writing, stacked at left:5.7% from top:19.5% down to top:64.6%, and nine of them
   are marked `pan-skip` so the swipe treats the whole case as one stop and the
   plate names only the first. Nothing on screen said the other nine were there.
   So each shelf level now rests with its own soft light, which is what makes ten
   things read as ten things, and js/temple.js changes the capsule's wording when
   you are standing in front of the case. The glow is the same halo the room already
   uses, simply not waiting for a pointer that a phone does not have. */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .ex-shelf .halo{opacity:.5}
  .ex-shelf:active .halo{opacity:1}
  /* a hairline of warm light along each shelf, so the levels are countable */
  .ex-shelf::after{content:"";position:absolute;left:6%;right:6%;bottom:0;height:1px;
    background:linear-gradient(90deg,transparent,rgb(255 236 187 / .85),transparent);
    pointer-events:none}
  /* AND AN ARROW INTO EACH ONE (2026-08-25). Director: "you've just made it move with
     gold filament but I think it needs more clear definition that something is there,
     maybe a little arrow pointing into each shelf."
     The light says the levels are separate; it does not say they are things you can
     open. A mark that POINTS at a row does, and ten of them down the case read as ten
     handles rather than ten stripes of light. It sits just off the row's right edge,
     which is empty at rest (the shelf plate is the one label the phone keeps hidden
     until you tap, see rule 3's `:not(.ex-shelf)` carve-out), and it points back into
     the shelf.
     ⚠ IT IS A REAL ELEMENT AND IT HAS TO BE. Both pseudos on a hotspot are spoken for:
     `.exhibit::before` IS the gold filament he is talking about, and `.ex-shelf::after`
     is the hairline under each row. Writing the arrow into either one takes the light
     away to add the arrow, which is the opposite of what he asked for. */
  .shelf-mark{position:absolute;left:calc(100% + .18rem);top:50%;
    transform:translateY(-50%);z-index:3;
    font-family:var(--font-serif);font-size:1.15rem;line-height:1;font-style:normal;
    color:rgb(255 240 205 / .92);
    text-shadow:0 0 7px rgb(150 112 40 / .75),0 1px 2px rgb(60 44 18 / .55);
    pointer-events:none}
  .ex-shelf:active .shelf-mark{color:#fff8e8;text-shadow:0 0 12px rgb(255 226 150 / .95)}

  /* AND A GLASS CAPSULE ON EACH ONE, CARRYING THE WRITING'S NAME (2026-08-25).
     Director: "it may be put a glass capsule over each bookshelf as well with the name
     of the writing." The arrow says a shelf is a thing you can open; the capsule says
     WHICH thing, so the case stops being ten anonymous rows.
     Rule 3 above deliberately excluded `.ex-shelf` from the resting capsules on the
     grounds that ten labels stacked at the same left would be a pile. They are not, now
     that each hotspot sits on its own measured shelf: the shelves are 5.53% of the
     artwork apart, which is ~47px on a 390x844 phone, and a capsule of this size is 26px
     -- so each one has its own row with clear air above and below.
     It rides beside the arrow rather than over the books, because the books ARE the
     picture. One line, and a long title ends in an ellipsis: the capsule is a name, not
     the writing, and tapping it opens the whole thing. */
  /* ⚠ AND ONLY WHILE YOU ARE STANDING AT THE CASE (2026-08-25, same day). Director:
     "when I move to About the Founders, those labels or capsules should disappear so
     they don't get in the way. It's only when we're hovering over that section."
     Ten capsules are the right amount of help at the bookcase and pure clutter two
     sections along, where they reach out across the founders' wall. The panorama already
     knows which section you are at: `lock()` in js/temple.js puts `.locked` on it as you
     come to rest, and nine of the ten shelves carry `pan-skip`, so the case rests as ONE
     stop and it is the first shelf that wears the class. `:has(.ex-shelf.locked)` is
     therefore exactly "the visitor is at the bookcase".
     The ARROWS stay wherever you are: they sit on the case's own edge, they never reach
     over anything else, and they are what says the shelves can be opened at all. It is
     the names that had to learn to keep out of the way. */
  .library-scene .ex-shelf .plate{
    opacity:0;transition:opacity .5s var(--ease-veil,ease);
    margin-left:1.15rem;max-width:min(58vw,13rem);
    padding:.3rem .62rem;border-radius:999px;
    font-size:.6rem;letter-spacing:.055em;line-height:1.25;
    white-space:nowrap;overflow:hidden;text-overflow:ellipsis;
    background:linear-gradient(180deg,rgb(255 253 246 / .8),rgb(252 246 232 / .68));
    border:1px solid rgb(217 182 98 / .5);
    box-shadow:0 1px 10px rgb(60 44 18 / .28),inset 0 1px 0 rgb(255 255 255 / .7);
    -webkit-backdrop-filter:blur(3px);backdrop-filter:blur(3px);
    color:#4a3f66;text-transform:none}
  /* ⚠ AND THE CAPSULE IS THE TAP TARGET, NOT JUST A LABEL (2026-08-25). Director:
     "the capsules should also be tappable." Every `.plate` on this site is
     `pointer-events:none` (line ~2080) because a plate is normally a caption floating
     over the thing it names, and a caption that eats taps is a caption in the way. This
     one is different: it is the only part of a shelf big enough to hit, since the shelf
     itself is a 78x42px sliver of artwork. So it takes taps, and because it is a child
     of the shelf's own <a>, the tap opens that writing with no new handler.
     ⚠ POINTER EVENTS RIDE WITH THE OPACITY, DELIBERATELY. These capsules are invisible
     everywhere except at the bookcase; an invisible capsule that still took taps would
     swallow presses meant for the founders' wall two sections along. Both properties
     hang off the same `:has(.ex-shelf.locked)`, so a capsule can only be hit while it
     can be seen.
     The pill is 24px and a finger wants 44px, so `::after` extends the hit area by 10px
     top and bottom without moving a pixel of the design. Rows are 47px apart, so 44px
     targets still leave air between them and no two can overlap. */
  .library-scene:has(.ex-shelf.locked) .ex-shelf .plate{opacity:1;pointer-events:auto}
  .library-scene .ex-shelf .plate::after{content:"";position:absolute;inset:-10px -6px}
  .ex-shelf:active .plate{background:linear-gradient(180deg,rgb(255 247 226 / .95),rgb(248 234 200 / .9));
    border-color:rgb(190 152 62 / .75)}
}

/* ---- 5. THE CORE SESSIONS BUTTON, SLICED IN HALF --------------------------
   Director: "the core sessions pop-up button is cut off by the text view. I think
   it's because there's more practitioners than any other view which creates a new
   line and pushes the button down."
   That is exactly it, and the mechanism is the `:has(img)` test three hundred lines
   up. The mobile rules tell a PICTURE panel from a PLATE panel by asking whether
   the `.shot` contains an `<img>` — a picture is pinned to a 16/9 letterbox at the
   top of the card, a plate is allowed to size itself and sit under the words. But
   the Sessions plate contains the PRACTITIONERS' FACES, which are `<img>`, so every
   session card has been treated as a photograph. With one face the content happens
   to fit inside 16/9; Core Structure has FIVE, the row wraps to a second line
   (measured: 76px tall against 34px for the others), and the booking button is
   pushed past the bottom of a box that cannot grow, where the body text begins.
   Told apart by what the panel IS (`.sess-plate`) instead of by what it happens to
   contain, so adding a sixth practitioner cannot bring this back. */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .exhibit-card .shot.sess-plate{aspect-ratio:auto;order:2;min-height:0;
    border-bottom:0;border-top:1px solid rgb(217 182 98 / .4)}
  .exhibit-card:has(.shot.sess-plate){grid-template-rows:minmax(0,1fr) auto}
  /* and the faces wrap without shoving: they are a detail, the booking is the point */
  .sess-plate .sp-faces{flex-wrap:wrap;gap:.35rem .7rem}
  .sess-plate .sp-act{margin-top:.85rem}
}

/* ---- 6. THE GATHERING ROOM'S CAPSULES ------------------------------------
   Director: "there still needs to be labels on the map as well as on the bottom,
   but the country still needs to be labelled. The top capsules need to be below
   the country capsule."
   Three changes, no new mechanism:
   (a) the five continents are named ON the map again. `.map-name` was hidden below
       821px because it was unstyled there and fell back to raw link blue (see the
       note at line ~2212). It is styled here instead of hidden, and it rests
       visible rather than waiting for a hover, because the map is the room.
   (b) the COUNTRY capsule — the swipe plate, which names whichever region you are
       standing in front of — moves from the bottom edge to the top, under the bar.
   (c) the two offer capsules (Weekly Zooms, Heart of Space Week) sit directly
       UNDER it rather than in its place.
   The swipe cue stays at the bottom, where the swipe is. */
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .map-scene .map-name{display:block;position:absolute;
    left:var(--name-x,50%);top:var(--name-y,50%);transform:translate(-50%,-50%);
    width:max-content;max-width:9rem;
    font-family:var(--font-serif);font-size:.66rem;letter-spacing:.14em;
    text-transform:uppercase;text-align:center;line-height:1.3;
    color:#3f3757;pointer-events:none;opacity:.9;
    text-shadow:0 1px 9px rgb(255 253 246 / .95),0 0 22px rgb(255 253 246 / .9)}
  /* Australia's hotspot covers Australia AND New Zealand, so dead centre puts the
     name out over the Tasman. Same 32% the desktop uses, same reason. */
  .map-scene .exhibit[data-exhibit="ausnz"] .map-name{--name-x:32%}
  .map-scene.opened .map-name{opacity:0}

  /* (b) and (c) REVISED 2026-08-25, later the same day. Director: "in the zoom room
     the country capsules should be at the bottom." The first pass read his earlier
     "the top capsules need to be below the country capsule" as an instruction to move
     the country capsule up to the bar and hang the offers off it. Both of his
     sentences hold at once the other way up: the whole group lives at the BOTTOM,
     the country capsule above the two offers, so the offers are still below the
     country capsule and the map is still clear of all three.
     Stacked from the bottom edge up: the offers, the country capsule, the swipe cue.
     The offsets are measured heights (46px offers, 44px capsule at this size), not
     guesses, and each keeps the 8px gap the group already had. */
  .map-scene .map-cues{top:auto;
    bottom:calc(3.2svh + env(safe-area-inset-bottom, 0px))}
  .map-scene .pan-plate{
    bottom:calc(3.2svh + env(safe-area-inset-bottom, 0px) + 54px)}
  .map-scene .deck-cue--pan{
    bottom:calc(3.2svh + env(safe-area-inset-bottom, 0px) + 106px)}
}

/* ---- 7. PLAY AND PAUSE, IN THE MEDITATION CHAMBER -------------------------
   Director: "the transition from play to pause can be more smoother."
   Pressing play adds `.dimmed` to the room behind the film and pressing pause takes
   it off. The dim itself had no transition, so the whole chamber changed brightness
   between one frame and the next, twice per tap. It now breathes over 900ms, which
   is the length of the film's own fade at the loop point, so the room and the
   picture move together instead of one snapping while the other drifts. */
.room-scene .scene-frame,.room-scene .stage-dim{transition:opacity .9s var(--ease-veil,ease)}
.deck-card .poster,.deck-card .poster .play{transition:opacity .32s ease,transform .32s ease}
.deck-card .poster:active .play{transform:scale(.92)}

/* The Heart of Space capsule carried a long name and a short one so the phone row
   would not wrap ("Heart of Space Week - Quarterly"). Director, 2026-08-25: "the
   capsule shouldn't say Quarterly Week, it should say Heart of Space" — which is
   short enough at every width, so there is one name again and the pair of rules that
   swapped them is gone with it. */
/* The capsules are a selector now (2026-08-25): one of the two is always pressed, and
   the pressed one has to be readable as pressed at a glance, on a wall of gold, without
   a hover to help it. Gold ground, dark text, the same pair the room's own plates use. */
/* `.map-scene .exhibit.map-cue` sets the resting look at (0,3,0), so a bare
   `.map-cue--on` at (0,1,0) loses to it and the pressed capsule looked exactly like
   the unpressed one. Measured on the phone before this: both capsules identical. */
.map-scene .exhibit.map-cue--on{
  background:linear-gradient(180deg,rgb(238 214 150 / .97),rgb(222 190 112 / .97));
  color:#3a3050;border-color:rgb(180 142 58 / .75);
  box-shadow:0 2px 16px rgb(150 116 40 / .3)}
.map-scene .exhibit.map-cue--on .arr{opacity:.5}
@media (max-width:820px),(max-height:500px) and (orientation:landscape){
  .map-scene .map-cues{gap:.4rem;flex-wrap:nowrap}
}
