  /* ---------- Splash screen ---------- */
  #splashScreen {
    position: fixed; inset: 0; z-index: 10000;
    pointer-events: none;
    opacity: 1;
    /* Field: smooth gradient + fine SVG feTurbulence grain (per-pixel noise,
       no low-frequency splotches like the old photo/canvas grain). */
    background:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.2'/%3E%3C/svg%3E") repeat,
      linear-gradient(180deg, #454a37 0%, #3d422f 55%, #363b29 100%);
    transition: opacity 0.9s cubic-bezier(.4,0,.2,1);
  }
  #splashScreen.fade-out { opacity: 0; }
  #splashLogoWrap {
    position: absolute;
    top: 50%; left: 50%;
    transform: translate(-50%, -50%);
  }
  .splash-logo-base {
    display: block;
    height: clamp(44px, 9vw, 96px);
    width: auto;
    filter: brightness(0) invert(1);
    opacity: 0.15;
  }
  .splash-logo-fill {
    position: absolute;
    top: 0; left: 0;
    height: clamp(44px, 9vw, 96px);
    width: auto;
    filter: brightness(0) invert(1);
    clip-path: inset(100% 0 0 0);
    animation: splashFillUp 3s cubic-bezier(0.16, 1, 0.3, 1) 0.3s forwards;
  }
  @keyframes splashFillUp {
    from { clip-path: inset(100% 0 0 0); }
    to   { clip-path: inset(0% 0 0 0); }
  }
  :root{
    --cream:#FBF7F0; --paper:#FFFDF9; --ink:#EDE8DF; --soft:#C8C2B4;
    --accent:#B0552F; --accent-dark:#8E4222; --gold:#C99A4B; --line:#E7DECF; --green:#5E7C5A;
    --g1:#EDE8DF; --g2:rgba(237,232,223,0.70); --g3:rgba(237,232,223,0.38);
    --glass:rgba(237,232,223,0.07); --glass-b:rgba(237,232,223,0.13);
    /* Landing-circle radius. THE single source of truth: the clip-path, both
       glow halos and #landingHeadline's bottom anchor all derive from it, so
       they can never drift apart. Overridden per-breakpoint below. */
    --orb-r: 26vmin;
    /* Landing-circle edge softness: the mask is fully opaque out to this much
       of the radius, then fades to nothing at the edge. 100% = hard cut. */
    --orb-feather: 76%;
    /* How far below screen centre the circle sits. Everything anchored to the
       circle — clip-path, mask, both glow halos, the headline, the start pill's
       gradient — derives from this one value, so moving it moves the whole
       composition together. Overridden per-breakpoint below. */
    --orb-cy: calc(17vmin - 4vh);
    /* circleLife drift periods. The -fast pair is what the orbs switch to
       while Eleanor speaks: same paths, ~60% of the period, so they read as
       moving MORE rather than merely brighter. */
    --orb-drift-a: 7s;    --orb-drift-a-fast: 4.2s;
    --orb-drift-b: 9.5s;  --orb-drift-b-fast: 5.7s;
    --orb-drift-c: 8.2s;
    /* How far the orbs push out from centre at full voice. Uses the standalone
       `translate` property, which composites WITH the orbit's `transform`
       instead of overwriting it — so they breathe outward while still circling.
       Driven by --voice-slow (the damped envelope) because this moves them
       bodily; the fast signal would jitter their position. */
    --orb-push: 6vmin;
    /* Mic button diameter. The arc hint centres itself on this, so the text
       stays concentric with the button at every breakpoint. */
    --mic-d: 79px;
    /* Live voice level, written every frame by startBlobLoop() in index.html
       from the TTS analyser. 0 = silent. Declared here so every calc() below
       is valid before the loop has ever run (and if JS never runs at all). */
    --voice: 0;
    --voice-slow: 0;
    /* --voice-slow drives PLAYBACK RATE, applied in JS (see startBlobLoop).
       It is not consumed by any rule here. */
  }
  *{box-sizing:border-box; margin:0; padding:0; -webkit-tap-highlight-color:transparent;}
  html,body{height:100%; overflow:hidden;}
  body{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    background: #3d3d30;
    color:var(--ink);
    display:flex; flex-direction:column; height:100dvh;
  }

  /* ---------- Hamburger menu ---------- */
  /* The menu now slides in OVER the page from the left (drawer overlay)
     instead of pushing #mainWrap aside. */
  #menuPanel{
    position:fixed; top:0; left:0; z-index:95;
    width:100vw; height:100vh;
    background:#f0f0ea;
    display:flex; flex-direction:column; padding:72px 32px 40px;
    transform:translateX(-100%);
    transition:transform 0.52s cubic-bezier(0.16, 1, 0.3, 1),
               box-shadow 0.52s cubic-bezier(0.16, 1, 0.3, 1);
    will-change:transform;
  }
  /* Real close button inside the panel — positioned where the hamburger pill was */
  #menuClose{
    position:fixed; top:16px; left:16px;
    width:54px; height:54px; border-radius:50%;
    padding:0; border:1px solid #d9d8d2; cursor:pointer;
    font-size:0; line-height:0; color:transparent;
    background-color:#fbfaf7;
    background-image:
      linear-gradient(45deg,  transparent calc(50% - 0.75px), rgba(0,0,0,0.88) calc(50% - 0.75px), rgba(0,0,0,0.88) calc(50% + 0.75px), transparent calc(50% + 0.75px)),
      linear-gradient(-45deg, transparent calc(50% - 0.75px), rgba(0,0,0,0.88) calc(50% - 0.75px), rgba(0,0,0,0.88) calc(50% + 0.75px), transparent calc(50% + 0.75px));
    background-size:18px 18px;
    background-position:center;
    background-repeat:no-repeat;
    transition:background-color 0.2s;
    z-index:96;
  }
  #menuClose:hover{ background-color:#efeee8; }
  #menuClose:active{ opacity:0.75; transform:scale(0.95); }
  body:has(#mainWrap.menu-open) #menuPanel{
    transform:translateX(0);
    box-shadow:8px 0 32px rgba(0,0,0,0.18);
  }
  #menuPrimary{
    display:flex; flex-direction:column; gap:2px;
    padding-top:15vh; padding-bottom:0;
    justify-content:flex-start;
  }
  #menuSecondary{
    margin-top:auto; display:flex; flex-direction:column; gap:0; padding-bottom:24px;
  }
  .menuItem{
    background:none; border:none; text-align:left; cursor:pointer;
    /* Matches intro headline typography (#landingHeadline) */
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:clamp(27px, 5.4vw, 48px); font-weight:500; line-height:1.25;
    color:#161513; padding:4px 0;
    transition:color 0.15s;
    letter-spacing:-0.01em;
  }
  .menuItem:hover{ color:#000; }
  .menuItemSm{
    background:none; border:none; text-align:left; cursor:pointer;
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:14px; font-weight:100;
    color:rgba(20,20,20,0.45); padding:5px 0;
    transition:color 0.15s;
  }
  .menuItemSm:hover{ color:rgba(20,20,20,0.85); }

  /* Same pill language as the intro/nav buttons: flat fill, hairline border, no shadows */
  #menuSignOut{
    width:100%; text-align:center; padding:16px;
    border-radius:50px;
    background:#161513;
    border:1px solid #161513;
    box-shadow:none;
    color:#fbfaf7; /* light text on dark background — was incorrectly #161513 (invisible) */
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    font-size:15px; font-weight:500; letter-spacing:-0.01em; text-transform:none;
    transition: background-color 0.2s, color 0.2s;
    cursor:pointer;
  }
  #menuSignOut:hover{
    background:#efeee8;
    color:#161513;
  }
  #menuSignOut:focus-visible{
    outline:2px solid #161513; outline-offset:2px;
  }
  #menuSignOut:active{ transform:scale(0.97); }
  #menuUser{
    display:flex; align-items:center; gap:10px;
    padding-top:20px; border-top:1px solid rgba(0,0,0,0.08);
  }
  #menuUserAvatar{
    flex-shrink:0; width:32px; height:32px; border-radius:50%;
    background:rgba(20,20,20,0.12);
    display:flex; align-items:center; justify-content:center;
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:13px; font-weight:100;
    color:rgba(20,20,20,0.5);
  }
  #menuUserEmail{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:12px; font-weight:100;
    color:rgba(20,20,20,0.35); letter-spacing:0.01em;
    word-break:break-all; line-height:1.4;
  }

  #mainWrap{
    position:fixed; top:0; left:0; right:0; bottom:0; z-index:10;
    overflow:hidden;
    /* Soft white canvas — the dark living field now lives INSIDE #blobWrap,
       clipped to a circle on landing and expanded fullscreen in session. */
    background: #f0f0ea;
    transition:transform 0.52s cubic-bezier(0.16, 1, 0.3, 1),
               border-radius 0.52s cubic-bezier(0.16, 1, 0.3, 1),
               top 0.52s cubic-bezier(0.16, 1, 0.3, 1),
               bottom 0.52s cubic-bezier(0.16, 1, 0.3, 1),
               box-shadow 0.52s cubic-bezier(0.16, 1, 0.3, 1);
    will-change:transform;
  }
  /* The page itself stays put now — the drawer overlays it. */
  #mainWrap.menu-open{
    transform:none;
  }

  /* Hide the menu + book buttons while a session with Eleanor is live
     (#mainDock gains .active for the whole conversation; it drops the class
     when the session closes, so the buttons come right back). !important
     beats the inline visibility/opacity the script manages. */
  #mainWrap:has(#mainDock.active) #menuBtn,
  #mainWrap:has(#mainDock.active) #bookBtn {
    opacity: 0 !important;
    visibility: hidden !important;
    pointer-events: none;
    transition: opacity 0.3s ease, visibility 0s linear 0.3s;
  }

  /* liquid glass shared style */
  .glassBtn{
    position:fixed; top:16px; z-index:50; cursor:pointer;
    border:1px solid rgba(255,255,255,0.18);
    display:flex; align-items:center; justify-content:center;
    width:54px; height:54px; padding:0; border-radius:50%;
    /* solid warm grey pill with black icon lines (reference screenshot) */
    background: #fbfaf7;
    backdrop-filter: none; -webkit-backdrop-filter: none;
    /* thin edge line in a slightly darker grey (reference screenshot) */
    border: 1px solid #d9d8d2;
    box-shadow: none;
    transition: opacity 0.2s ease, background 0.2s ease;
    opacity: 1;
  }
  .glassBtn:hover{ background: #efeee8; }
  .glassBtn:active{ opacity: 0.75; transform:scale(0.95); }

  #menuBtn{
    left:16px;
    flex-direction:column; gap:4px;
  }
  #menuBtn span{
    display:block; width:20px; height:2px;
    background:rgba(0,0,0,0.88); border-radius:0;
  }

  #bookBtn{
    right:16px;
    color:rgba(0,0,0,0.88);
  }
  #bookBtn svg{ width:26px; height:26px; }

  /* --------- Book Panel --------- */
  #bookPanel{
    position:fixed; inset:0; z-index:90;
    background:#f0f0ea;
    display:flex; flex-direction:column;
    transform:translateX(100%);
    transition:transform 0.52s cubic-bezier(0.16, 1, 0.3, 1),
               box-shadow 0.52s cubic-bezier(0.16, 1, 0.3, 1);
    will-change:transform;
    pointer-events:none; overflow:hidden;
  }
  #bookPanel.open{
    transform:translateX(0); pointer-events:auto;
    box-shadow:-8px 0 32px rgba(0,0,0,0.18);
  }
  #bookPanelHeader{
    display:flex; align-items:center; justify-content:flex-end;
    padding:16px; flex-shrink:0;
  }
  /* Close button styled to match the menu's close glyph: grey pill + thin X */
  #bookPanelClose{
    border:1px solid #d9d8d2; cursor:pointer;
    width:54px; height:54px; border-radius:50%;
    padding:0; font-size:0; line-height:0; color:transparent;
    background-color:#fbfaf7;
    background-image:
      linear-gradient(45deg,  transparent calc(50% - 0.75px), rgba(0,0,0,0.88) calc(50% - 0.75px), rgba(0,0,0,0.88) calc(50% + 0.75px), transparent calc(50% + 0.75px)),
      linear-gradient(-45deg, transparent calc(50% - 0.75px), rgba(0,0,0,0.88) calc(50% - 0.75px), rgba(0,0,0,0.88) calc(50% + 0.75px), transparent calc(50% + 0.75px));
    background-size:18px 18px;
    background-position:center;
    background-repeat:no-repeat;
    transition:background-color 0.2s;
  }
  #bookPanelClose:hover{ background-color:#efeee8; }
  #bookPanelClose:active{ opacity:0.75; transform:scale(0.95); }
  #bookPanelBody{ flex:1; overflow-y:auto; padding:0 24px 64px; }
  .bpSection{ margin-bottom:40px; }
  .bpSectionTitle{
    font-family:-apple-system,system-ui,sans-serif;
    font-size:11px; letter-spacing:0.14em; text-transform:uppercase;
    color:rgba(0,0,0,0.35); margin:0 0 16px;
    padding-bottom:10px; border-bottom:1px solid rgba(0,0,0,0.08);
  }
  /* Accordion cards (native details/summary): pill-language cards with a
     status badge on the right, matching the app's flat hairline style. */
  .bpAccordion{
    background:#fbfaf7; border:1px solid #d9d8d2; border-radius:18px;
    margin-bottom:14px;
  }
  .bpAccHead{
    cursor:pointer; list-style:none;
    display:flex; align-items:center; gap:12px;
    padding:20px 22px;
    user-select:none; -webkit-user-select:none;
  }
  .bpAccHead::-webkit-details-marker{ display:none; }
  .bpAccTitle{
    flex:1;
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    font-size:18px; font-weight:500; letter-spacing:-0.01em;
    color:#161513;
  }
  .bpStageBadge{
    padding:6px 14px; border-radius:50px;
    font-family:-apple-system,system-ui,sans-serif;
    font-size:12px; font-weight:500; white-space:nowrap;
    background:rgba(0,0,0,0.055); color:rgba(20,20,20,0.5);
  }
  .bpStageBadge.going{ background:rgba(110,138,96,0.16); color:#4c6242; }
  .bpStageBadge.done{ background:rgba(110,138,96,0.30); color:#3c5234; }
  /* Chevron: a rotating hairline caret after the badge */
  .bpAccHead::after{
    content:''; flex-shrink:0;
    width:8px; height:8px; margin-top:-4px;
    border-right:1.5px solid rgba(0,0,0,0.35);
    border-bottom:1.5px solid rgba(0,0,0,0.35);
    transform:rotate(45deg);
    transition:transform 0.25s ease;
  }
  .bpAccordion[open] .bpAccHead::after{
    transform:translateY(4px) rotate(225deg);
  }
  .bpAccBody{ padding:0 22px 20px; }
  /* Progress tab */
  #bpProgressSummary{ text-align:center; padding:6px 0 32px; }
  /* Semicircle gauge: three arc sections (scaffolding / outline / chapters) */
  #bpGaugeWrap{ position:relative; width:252px; max-width:100%; margin:0 auto; }
  /* viewBox widened to -16 -18 252 130 so the arced labels outside the
     stroke aren't clipped; px size matches 1:1 to keep the gauge scale. */
  #bpGauge{ display:block; width:252px; max-width:100%; height:auto; }
  /* Screen-reader-only text: the arc labels live in an aria-hidden SVG, so
     the legend keeps a hidden label paired with each percentage. */
  .srOnly{
    position:absolute; width:1px; height:1px; padding:0; margin:-1px;
    overflow:hidden; clip:rect(0 0 0 0); white-space:nowrap; border:0;
  }
  .bpGaugeArcLabel{
    font-family:-apple-system,system-ui,sans-serif;
    /* letter-spacing omitted: iOS Safari applies trailing spacing after the
       last glyph on a textPath, which shifts centered text sideways and can
       push it into the viewBox clip boundary.  The labels read clearly
       without it at font-size 10px / text-transform uppercase. */
    font-size:10px; text-transform:uppercase;
    fill:rgba(0,0,0,0.35);
  }
  .bpGaugeTrack{
    fill:none; stroke:rgba(0,0,0,0.08); stroke-width:10; stroke-linecap:round;
  }
  /* Stages darken left→right: scaffolding lightest, chapters the base grey */
  .bpGaugeTrack0{ stroke:rgba(0,0,0,0.03); }
  .bpGaugeTrack1{ stroke:rgba(0,0,0,0.055); }
  .bpGaugeTrack2{ stroke:rgba(0,0,0,0.08); }
  .bpGaugeFill{
    fill:none; stroke:var(--green); stroke-width:10; stroke-linecap:round;
  }
  #bpGaugeCenter{
    position:absolute; left:0; right:0; bottom:0; text-align:center;
  }
  #bpPercent{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:64px; font-weight:400;
    color:rgba(0,0,0,0.82); line-height:1;
  }
  .bpPctSign{ font-size:0.5em; }
  .bpThreadRow{
    display:flex; align-items:center; gap:14px;
    padding:11px 0; border-bottom:1px solid rgba(0,0,0,0.07);
  }
  .bpThreadDots{ display:flex; gap:5px; flex-shrink:0; }
  .bpDot{
    width:7px; height:7px; border-radius:50%;
    background:rgba(0,0,0,0.12);
  }
  .bpDot.filled{ background:rgba(0,0,0,0.65); }
  .bpThreadTitle{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:15px; font-weight:100;
    color:rgba(0,0,0,0.72); flex:1;
  }
  .bpThreadCoverage{
    font-family:-apple-system,system-ui,sans-serif;
    font-size:10px; letter-spacing:0.1em; text-transform:uppercase;
    color:rgba(0,0,0,0.28); flex-shrink:0;
  }
  /* Tappable rows (topic + outline selection → Eleanor goes there) */
  .bpTappable{ cursor:pointer; border-radius:10px; transition:background .2s ease; }
  .bpTappable:hover{ background:rgba(0,0,0,0.045); }
  .bpTappable:active{ background:rgba(0,0,0,0.08); }
  /* Set-aside (closed) doors: dimmed, but never locked — reopening is theirs */
  .bpClosed{ opacity:0.38; }
  .bpClosed:hover{ opacity:0.6; }
  /* ---- Chapter reader: nested view, tap-a-sentence editing, listen ---- */
  .bpChapRow{
    display:flex; align-items:center; gap:12px;
    padding:16px 6px; border-bottom:1px solid rgba(0,0,0,0.07); cursor:pointer;
    border-radius:10px; transition:background .2s ease;
  }
  .bpChapRow:hover{ background:rgba(0,0,0,0.045); }
  .bpChapRow:active{ background:rgba(0,0,0,0.08); }
  .bpChapRowTitle{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:17px; font-weight:100;
    color:rgba(0,0,0,0.8); flex:1;
  }
  .bpChapRowMeta{
    font-family:-apple-system,system-ui,sans-serif;
    font-size:10px; letter-spacing:0.1em; text-transform:uppercase;
    color:rgba(0,0,0,0.3); flex-shrink:0;
  }
  .bpChapRowChev{ color:rgba(0,0,0,0.25); font-size:20px; line-height:1; flex-shrink:0; }
  .bpReaderHead{ display:flex; align-items:center; gap:8px; margin:2px 0 4px; }
  .bpReaderBack{
    background:none; border:none; cursor:pointer; flex-shrink:0;
    font-size:26px; line-height:1; color:rgba(0,0,0,0.4); padding:2px 10px 4px 0;
  }
  .bpReaderBack:hover{ color:rgba(0,0,0,0.75); }
  .bpReaderTitle{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:20px; font-weight:100;
    color:rgba(0,0,0,0.85); flex:1;
  }
  .bpListenBtn{
    flex-shrink:0; cursor:pointer;
    font-family:-apple-system,system-ui,sans-serif; font-size:12px;
    color:rgba(0,0,0,0.55); background:rgba(0,0,0,0.05);
    border:1px solid rgba(0,0,0,0.1); border-radius:999px; padding:7px 14px;
    transition:background .2s;
  }
  .bpListenBtn:hover{ background:rgba(0,0,0,0.09); }
  .bpReaderMeta{
    font-family:-apple-system,system-ui,sans-serif;
    font-size:10px; letter-spacing:0.12em; text-transform:uppercase;
    color:rgba(0,0,0,0.3); margin:0 0 22px;
  }
  .bpPara{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:16px; font-weight:100;
    color:rgba(0,0,0,0.68); line-height:1.85; margin:0 0 1.25em;
    max-width:64ch; border-radius:8px; transition:background .35s;
  }
  .bpPara.playing{ background:rgba(176,85,47,0.07); }
  .bpSent{ cursor:pointer; }
  .bpSent.sel{
    text-decoration:underline; text-decoration-style:dotted;
    text-decoration-color:rgba(0,0,0,0.55); text-decoration-thickness:2px;
    text-underline-offset:5px; background:rgba(176,85,47,0.08);
  }
  #bpActionBar{
    position:fixed; left:0; right:0; bottom:0; z-index:96;
    background:#fff; border-top:1px solid rgba(0,0,0,0.08);
    border-radius:18px 18px 0 0;
    box-shadow:0 -10px 34px rgba(0,0,0,0.1);
    padding:14px 20px calc(18px + env(safe-area-inset-bottom,0px));
    transform:translateY(110%); transition:transform .32s cubic-bezier(.22,.8,.36,1);
  }
  #bpActionBar.up{ transform:translateY(0); }
  .bpSelLabel{
    font-family:-apple-system,system-ui,sans-serif;
    font-size:10px; letter-spacing:0.12em; text-transform:uppercase;
    color:rgba(0,0,0,0.35); margin:0 0 10px; display:flex; justify-content:space-between; align-items:center;
  }
  .bpBarClear{ background:none; border:none; cursor:pointer; color:rgba(0,0,0,0.35); font-size:16px; padding:0 2px; }
  .bpActBtns{ display:flex; gap:10px; }
  .bpActBtn{
    flex:1; padding:12px 10px; border-radius:12px; cursor:pointer;
    font-family:-apple-system,system-ui,sans-serif; font-size:14px;
    border:1px solid rgba(0,0,0,0.14); background:#fff; color:rgba(0,0,0,0.75);
  }
  .bpActBtn:active{ transform:scale(0.98); }
  .bpActBtn.primary{ background:#B0552F; border-color:#B0552F; color:#ebe5da; font-weight:600; }
  .bpActBtn:disabled{ opacity:0.5; cursor:default; }
  .bpAskInput, .bpEditArea{
    width:100%; box-sizing:border-box; margin:0 0 10px;
    font-size:16px; color:#2B2622; background:#fff;
    border:1px solid rgba(0,0,0,0.16); border-radius:10px; padding:11px 12px;
    -webkit-appearance:none; appearance:none;
  }
  .bpEditArea{ font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; line-height:1.6; min-height:120px; resize:vertical; }
  .bpAskInput{ font-family:-apple-system,system-ui,sans-serif; }
  /* Voice dictation for the Editor. Deliberately the biggest thing in the
     sheet: for the person this product is for, tapping is easy and typing on
     a phone is the hardest thing in the app. The text field stays underneath
     as the alternative, not the default. */
  .bpMicBtn{
    width:100%; box-sizing:border-box; margin:0 0 8px;
    display:flex; align-items:center; justify-content:center; gap:10px;
    padding:16px 12px; border-radius:12px; cursor:pointer;
    font-family:-apple-system,system-ui,sans-serif; font-size:15px; font-weight:600;
    border:1px solid #B0552F; background:#B0552F; color:#ebe5da;
  }
  .bpMicBtn:active{ transform:scale(0.99); }
  .bpMicBtn:disabled{ opacity:0.5; cursor:default; }
  .bpMicDot{ width:11px; height:11px; border-radius:50%; background:#ebe5da; flex:none; }
  .bpMicBtn.rec{ background:#8E4222; border-color:#8E4222; }
  .bpMicBtn.rec .bpMicDot{ background:#ffd9c9; animation:bpPulse 1.1s ease-in-out infinite; }
  .bpMicBtn.busy{ background:#fff; color:rgba(0,0,0,0.5); border-color:rgba(0,0,0,0.14); }
  .bpMicBtn.busy .bpMicDot{ background:rgba(0,0,0,0.25); }
  @keyframes bpPulse{ 0%,100%{ opacity:1; transform:scale(1); } 50%{ opacity:0.3; transform:scale(0.7); } }
  .bpOrType{
    font-family:-apple-system,system-ui,sans-serif; font-size:10px;
    letter-spacing:0.12em; text-transform:uppercase;
    color:rgba(0,0,0,0.26); margin:0 0 8px; text-align:center;
  }
  .bpHearBtn{
    background:none; border:none; cursor:pointer; padding:0 0 10px;
    font-family:-apple-system,system-ui,sans-serif; font-size:12.5px; color:#8E4222;
  }
  .bpPropOld{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:14px; font-weight:100;
    color:rgba(0,0,0,0.35); text-decoration:line-through; margin:0 0 8px; line-height:1.55;
    max-height:72px; overflow:hidden;
  }
  .bpPropNew{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:15.5px; font-weight:100;
    color:rgba(0,0,0,0.82); margin:0 0 12px; line-height:1.65;
    max-height:180px; overflow-y:auto;
  }
  .bpBarMsg{
    font-family:-apple-system,system-ui,sans-serif; font-size:12px;
    color:#8E4222; margin:8px 0 0; min-height:15px;
  }
  /* Outline tab */
  .bpOutlineItem{ padding:18px 0; border-bottom:1px solid rgba(0,0,0,0.07); }
  .bpOutlineTitle{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:17px; font-weight:100;
    color:rgba(0,0,0,0.82); margin:0 0 7px;
  }
  .bpOutlineSummary{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:14px; font-weight:100; font-style:italic;
    color:rgba(0,0,0,0.45); line-height:1.55; margin:0;
  }
  .bpOutlineCovBadge{
    display:inline-block; margin-top:8px;
    font-family:-apple-system,system-ui,sans-serif;
    font-size:10px; letter-spacing:0.12em; text-transform:uppercase;
    color:rgba(0,0,0,0.28);
  }
  /* ---- BOOK-2 C1: the corkboard (docs/corkboard-binding-spec.md §2) ----
     Titles-only living outline: chapter headings + warm badges + scene rows.
     No prose surfaces here; the reader styles below return at 'draft'. */
  .bpStageLine{
    text-align:center; margin:10px 0 0;
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    font-size:16px; font-weight:100; color:rgba(0,0,0,0.62);
  }
  .bpCorkChapter{ padding:16px 0 12px; border-bottom:1px solid rgba(0,0,0,0.07); }
  .bpCorkChapter:last-child{ border-bottom:none; }
  .bpCorkHead{ display:flex; align-items:center; gap:10px; margin-bottom:4px; }
  .bpCorkTitle{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    font-size:17px; font-weight:100; color:rgba(0,0,0,0.82);
    flex:1; margin:0;
  }
  .bpCorkTail{ color:rgba(0,0,0,0.5); font-style:italic; }
  .bpSceneRow{
    display:flex; align-items:baseline; gap:9px;
    padding:5px 0 5px 12px;
  }
  .bpSceneDot{ color:rgba(0,0,0,0.22); flex-shrink:0; }
  .bpSceneTitle{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    font-size:15px; font-weight:100; color:rgba(0,0,0,0.6);
    flex:1; line-height:1.45;
  }
  .bpSceneYear{
    font-family:-apple-system,system-ui,sans-serif;
    font-size:10px; letter-spacing:0.1em; text-transform:uppercase;
    color:rgba(0,0,0,0.28); flex-shrink:0;
  }
  .bpSceneNew .bpSceneTitle{ color:rgba(60,82,52,0.92); }
  .bpNewPip{
    display:inline-block; width:6px; height:6px; border-radius:50%;
    background:#6e8a60; margin:0 7px 1px 0; vertical-align:middle;
  }
  /* ---- BOOK-2 C6: the DONE switch (two-step finalize) ---- */
  .bpDoneWrap{ text-align:center; padding:26px 0 10px; }
  .bpDoneBtn{
    cursor:pointer; padding:12px 26px; border-radius:999px;
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    font-size:15px; font-weight:500;
    color:#3c5234; background:rgba(110,138,96,0.16);
    border:1px solid rgba(110,138,96,0.4);
  }
  .bpDoneBtn:hover{ background:rgba(110,138,96,0.26); }
  .bpDoneNote{
    margin:10px auto 0; max-width:44ch;
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    font-size:12px; font-weight:100; font-style:italic;
    color:rgba(0,0,0,0.4); line-height:1.5;
  }
  .bpConfirm{ text-align:center; padding:48px 12px; }
  .bpConfirmTitle{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    font-size:22px; font-weight:500; color:#161513; margin:0 0 14px;
  }
  .bpConfirmBody{
    margin:0 auto 24px; max-width:52ch;
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    font-size:15px; font-weight:100; color:rgba(0,0,0,0.65); line-height:1.65;
  }
  .bpConfirmBtns{ display:flex; gap:12px; justify-content:center; flex-wrap:wrap; }
  /* Chapters tab */
  .bpChapterItem{ padding:32px 0; border-bottom:1px solid rgba(0,0,0,0.07); }
  .bpChapterTitle{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:20px; font-weight:100;
    color:rgba(0,0,0,0.85); margin:0 0 16px;
  }
  .bpChapterBody{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:16px; font-weight:100;
    color:rgba(0,0,0,0.62); line-height:1.75; margin:0;
    max-width:64ch; white-space:pre-wrap;
  }
  .bpChapterMeta{
    margin-top:12px;
    font-family:-apple-system,system-ui,sans-serif;
    font-size:10px; letter-spacing:0.1em; text-transform:uppercase;
    color:rgba(0,0,0,0.28);
  }
  /* Shared empty / loading states */
  .bpEmpty,.bpLoading{
    text-align:center; padding:72px 0;
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size:15px; font-style:italic;
    color:rgba(0,0,0,0.3); margin:0;
  }

  /* ---------- Logo ---------- */

  /* ---------- Main screen ---------- */
  #welcome{
    flex:1; display:flex; flex-direction:column; align-items:center; justify-content:center;
    text-align:center; padding:24px; gap:12px; min-height:0;
  }



  /* ---------- First-load ring + label ---------- */
  @keyframes pulseRing {
    0%, 100% { opacity: 0.15; box-shadow: 0 0 0px 0px rgba(235,229,218,0); }
    50%       { opacity: 0.7;  box-shadow: 0 0 18px 4px rgba(235,229,218,0.12); }
  }
  #loadingRing { display: none; }
  @keyframes loadingDots {
    0%,100% { opacity: 0.2; }
    50%      { opacity: 1;   }
  }
  @keyframes labelBreath {
    0%, 100% { opacity: 0.55; text-shadow: 0 0 18px rgba(235,229,218,0.0); }
    50%       { opacity: 1.00; text-shadow: 0 0 22px rgba(235,229,218,0.35); }
  }
  #loadingLabel {
    position: absolute;
    left: 50%; top: 50%;
    transform: translate(-50%, -50%);
    font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    font-size: 18px; font-style: italic;
    color: rgba(237,232,223,0.80);
    letter-spacing: 0.06em;
    pointer-events: none;
    opacity: 0;
    transition: opacity 1.1s ease;
    white-space: nowrap;
    z-index: 10;
  }
  #loadingLabel.visible {
    opacity: 1;
    animation: labelBreath 2.4s ease-in-out infinite;
  }
  #loadingPhrase { transition: opacity 0.4s ease; }
  #loadingLabel .dot { display: none; }

  /* ---------- Living background (full-page morphing glow) ----------
     Lives on #mainWrap so it sits above the opaque bg.png but below all
     z-indexed UI. Animates only transform + opacity (compositor-friendly);
     the organic shape comes from static border-radius + blur. */
  @keyframes bgDriftA {
    0%   { transform: translate3d(0, 0, 0) scale(1) rotate(0deg); }
    33%  { transform: translate3d(12vw, 8vh, 0) scale(1.18) rotate(14deg); }
    66%  { transform: translate3d(-6vw, 14vh, 0) scale(0.9) rotate(-10deg); }
    100% { transform: translate3d(0, 0, 0) scale(1) rotate(0deg); }
  }
  @keyframes bgDriftB {
    0%   { transform: translate3d(0, 0, 0) scale(1) rotate(0deg); }
    30%  { transform: translate3d(-10vw, -9vh, 0) scale(1.16) rotate(-12deg); }
    65%  { transform: translate3d(7vw, -13vh, 0) scale(0.88) rotate(11deg); }
    100% { transform: translate3d(0, 0, 0) scale(1) rotate(0deg); }
  }
  @keyframes bgBreath {
    0%, 100% { opacity: 0.35; }
    50%      { opacity: 1; }
  }
  /* The ambient page-level glows are retired: the dark living field now lives
     inside #blobWrap (which covers them completely in session), so on the
     cream landing they only tinted the background green. */
  #mainWrap::after {
    content: "";
    display: none;
    position: absolute;
    z-index: 0;
    pointer-events: none;
    filter: blur(60px);
  }
  #mainWrap::after {
    width: 95vmax; height: 95vmax;
    bottom: -38vmax; right: -28vmax;
    border-radius: 42% 58% 62% 38% / 48% 62% 38% 52%;
    background:
      radial-gradient(ellipse at 58% 62%,
        rgba(110,138,95,0.20) 0%,
        rgba(88,116,84,0.13) 38%,
        rgba(76,100,68,0.05) 62%,
        rgba(76,100,68,0.00) 80%);
    animation:
      bgDriftB 26s ease-in-out infinite,
      bgBreath 7s ease-in-out 2s infinite;
  }
  /* ---------- Speaking-surge custom properties ----------
     One place to tune the effect; media queries below scale it back on
     larger screens so the same code path never looks over-blown on a big
     monitor or invisible on a phone. */
  :root {
    --surge-brightness:    1.06;   /* mobile default — barely above ambient; Justin flagged white-out twice */
    --surge-blur-before:   60px;
    --surge-blur-after:    50px;
    --surge-drift-before:  14s;
    --surge-drift-after:   12s;
    --surge-breath:        3.2s;
  }
  @media (min-width: 600px) {
    :root {
      --surge-brightness:   1.05;
      --surge-blur-before:  68px;
      --surge-blur-after:   58px;
      --surge-drift-before: 16s;
      --surge-drift-after:  14s;
      --surge-breath:       3.6s;
    }
  }
  @media (min-width: 900px) {
    :root {
      --surge-brightness:   1.04;
      --surge-blur-before:  80px;
      --surge-blur-after:   68px;
      --surge-drift-before: 18s;
      --surge-drift-after:  16s;
      --surge-breath:       4.0s;
    }
  }

  /* ---------- Speaking state: background surges brighter + faster ---------- */
  #mainWrap::before {
    content: "";
    display: none;
    position: absolute;
    z-index: 0;
    pointer-events: none;
    width: 110vmax; height: 110vmax;
    top: -30vmax; left: -20vmax;
    border-radius: 58% 42% 62% 38% / 45% 55% 45% 55%;
    filter: blur(72px);
    background:
      radial-gradient(ellipse at 44% 40%,
        rgba(126,152,112,0.18) 0%,
        rgba(94,128,84,0.10) 40%,
        rgba(76,104,68,0.04) 65%,
        rgba(76,104,68,0.00) 82%);
    animation:
      bgDriftA 30s ease-in-out infinite,
      bgBreath 9s ease-in-out infinite;
    transition: opacity 0.8s ease, filter 0.8s ease;
  }
  /* Brighten both glow layers and speed up drift/breath while speaking */
  #mainWrap.speaking::before {
    animation:
      bgDriftA var(--surge-drift-before) ease-in-out infinite,
      bgBreath  var(--surge-breath)      ease-in-out infinite;
    filter: blur(var(--surge-blur-before)) brightness(var(--surge-brightness));
    /* don't pin opacity at full — let the breath animation keep its range so
       the surge reads as faster movement, not extra light */
  }
  #mainWrap.speaking::after {
    animation:
      bgDriftB var(--surge-drift-after) ease-in-out infinite,
      bgBreath  var(--surge-breath)     ease-in-out 0.4s infinite;
    filter: blur(var(--surge-blur-after)) brightness(var(--surge-brightness));
  }
  /* The page-level glows (#mainWrap::before/::after, bgDriftA/bgDriftB) stay
     retired — see the display:none above. Restoring them in session was tried
     and reverted: laid over #blobWrap they veiled the circleLife orbs, which
     are the layer that actually reads as MOVING LIGHT. Three soft sage washes
     in the same place average out to one flat haze (cf. the white-out flagged
     twice on --surge-brightness). The drift surge they used to provide now
     lives on the orbs themselves, below. */
  /* Ease both pseudo-elements between speaking / ambient states */
  #mainWrap::before,
  #mainWrap::after {
    transition: filter 0.9s ease, opacity 0.9s ease;
  }

  /* ---------- Speaking state: text glow lifts with Eleanor's voice ----------
     Glow is scoped to #interviewerTextInner (child of #interviewerText) so
     that the outer element's mask-image — which fades overflowing upper lines —
     clips the brightened, shadowed content before it composites.
     Previously removed twice because filter + mask-image on the SAME element
     caused iOS to draw a visible rectangle around the text block. Scoping the
     filter to the inner child (while the mask stays on the outer element) is
     the correct fix: the mask now clips the filtered output rather than
     fighting it. Do not move this rule back onto #interviewerText. */
  /* DESIGN DECISION (Justin, rejected 3x): NO glow/brightness on the transcript,
     on ANY wrapper. The inner-wrapper approach above still draws a visible
     rectangle on iOS (user screenshot Aug 15, 2026, v217). Do not reintroduce. */
  #mainWrap.speaking #interviewerTextInner {
    filter: none;
    text-shadow: none;
  }
  #mainWrap.speaking #chapterHeading {
    filter: none;
    text-shadow: 0 2px 12px rgba(0,0,0,0.25);
  }

  @media (prefers-reduced-motion: reduce) {
    #mainWrap::after { animation: none; }
    #mainWrap::before { animation: none; }
    #mainWrap.speaking::before,
    #mainWrap.speaking::after { animation: none; filter: blur(60px) brightness(1.3); }
    #mainWrap.speaking #interviewerTextInner,
    #mainWrap.speaking #chapterHeading { filter: none; text-shadow: none; }
    #blobWrap, #beginBtn,
    #blobWrap.loading #beginBtn,
    #mainWrap:has(#mainDock.active) #blobWrap.loading #beginBtn { animation: none !important; }
    #shimmerCanvas { display: none; }
  }

  /* ---------- Blob orb ---------- */
  @keyframes blobMorph {
    0%   { border-radius: 58% 42% 35% 65% / 55% 38% 62% 45%; }
    20%  { border-radius: 42% 58% 62% 38% / 48% 62% 38% 52%; }
    40%  { border-radius: 65% 35% 48% 52% / 62% 45% 55% 38%; }
    60%  { border-radius: 38% 62% 55% 45% / 42% 58% 42% 58%; }
    80%  { border-radius: 52% 48% 38% 62% / 58% 35% 65% 42%; }
    100% { border-radius: 58% 42% 35% 65% / 55% 38% 62% 45%; }
  }
  @keyframes blobFloat {
    0%, 100% { transform: translateY(0px); }
    50%       { transform: translateY(-10px); }
  }
  /* Gentle breathing pulse for the landing circle (replaces the bounce) */
  @keyframes blobPulse {
    0%, 100% { transform: scale(1); }
    50%       { transform: scale(1.015); }
  }
  @keyframes blobGlow {
    0%, 100% { box-shadow: 0 0 60px 18px rgba(237,232,223,.14), 0 0 120px 40px rgba(237,232,223,.06); }
    50%       { box-shadow: 0 0 80px 28px rgba(237,232,223,.22), 0 0 160px 60px rgba(237,232,223,.10); }
  }
  /* The orb IS the background now: the wrap covers the whole viewport
     (oversized so breathe/think scale pulses never reveal an edge) and the
     button's gradient + voice-reactive shimmer canvas become a full-page
     living glow. All existing JS state hooks keep working untouched. */
  #blobWrap {
    position: fixed;
    top: calc(50% - 80vmax); left: calc(50% - 80vmax);
    width: 160vmax; height: 160vmax;
    display: flex; align-items: center; justify-content: center;
    animation: blobPulse 5s ease-in-out infinite;
    z-index: 1;
    /* Always visible: the script fades this layer in only at session start,
       but the landing circle must show from the first paint (inline style
       toggles are overridden on purpose). */
    opacity: 1 !important;
    /* The dark living field is carried by this layer now (was #mainWrap):
       on landing it's clipped to a small circle in the lower third; tapping
       start (.loading) or an active session expands the circle fullscreen —
       "Eleanor coming to life". clip-path animates on the compositor. */
    /* Field: darkened gradient + fine SVG feTurbulence grain (matches the
       splash layer above). */
    background:
      url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='300' height='300'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)' opacity='0.45'%3E%3C/rect%3E%3C/svg%3E") repeat,
      linear-gradient(180deg, #383e2c 0%, #303625 55%, #2a3021 100%);
    clip-path: circle(var(--orb-r) at 50% calc(50% + var(--orb-cy)));
    -webkit-clip-path: circle(var(--orb-r) at 50% calc(50% + var(--orb-cy)));
    /* Expansion easing: slow "inhale" at the start, confident accelerating
       bloom through the middle, soft settle at the edges of the screen. */
    /* Expansion easing: slow "inhale" at the start, confident accelerating
       bloom through the middle, soft settle at the edges of the screen. */
    /* v327 easing experiment: shorter, snappier bloom — holds a beat, then
       commits hard through the middle and glides into a long silky settle
       (previous: 2.2s cubic-bezier(.6,.04,.2,1)). */
    transition: opacity 1.1s cubic-bezier(.22,.8,.36,1),
                clip-path 1.8s cubic-bezier(.83,0,.17,1),
                -webkit-clip-path 1.8s cubic-bezier(.83,0,.17,1),
                /* The soft edge rides the bloom: mask-size is animatable where
                   mask-image is not, so the feather grows WITH the circle
                   instead of snapping off the moment expansion starts. Must
                   stay in lockstep with clip-path above. */
                mask-size 1.8s cubic-bezier(.83,0,.17,1),
                -webkit-mask-size 1.8s cubic-bezier(.83,0,.17,1);
    pointer-events: none;
  }
  /* Subtle vignette that darkens the field's edges once it's fullscreen
     (loading + session). Sits above the field/blobs (z1) and below all
     text/UI. Hidden on the cream landing. */
  #fieldVignette {
    position: fixed; inset: 0;
    z-index: 2;
    pointer-events: none;
    background: radial-gradient(ellipse at 50% 48%,
      rgba(0,0,0,0.00) 52%,
      rgba(0,0,0,0.10) 78%,
      rgba(0,0,0,0.22) 100%);
    opacity: 0;
    transition: opacity 1.4s ease;
  }
  #mainWrap:has(#mainDock.active) #fieldVignette,
  #mainWrap:has(#blobWrap.loading) #fieldVignette {
    opacity: 1;
  }
  /* Landing-only: livelier innards so the circle feels alive — a brighter,
     faster-drifting mini version of the speaking glow. Reverts automatically
     once loading/session state expands the circle. */
  #mainWrap:not(:has(#mainDock.active)) #blobWrap:not(.loading) #beginBtn {
    animation: orbDrift 8s ease-in-out infinite;
    filter: blur(38px) brightness(1.55);
  }
  #mainWrap:not(:has(#mainDock.active)) #blobWrap:not(.loading) #shimmerCanvas {
    opacity: 0.8;
  }
  /* Soften the circle's edge. clip-path alone cuts a hard, mechanical disc;
     this mask fades the last of the radius out instead.
     A filter: blur() CANNOT do this — filters are applied BEFORE clip-path,
     so the clip slices the blur straight back to a hard edge.

     Sized rather than scoped: the gradient uses closest-side so its feather is
     always relative to the mask BOX, and the box is what we animate. That lets
     the same soft edge survive the expansion — see mask-size in the transition
     above and the expanded override further down. mask-position uses
     background-position semantics, so calc(50% + …) lands the mask's CENTRE on
     the circle's centre, matching clip-path's own centre expression. */
  #blobWrap {
    -webkit-mask-image: radial-gradient(circle closest-side,
      /* Smootherstep falloff (6t^5-15t^4+10t^3). Its slope is ZERO at both
         ends, so the fade eases out of the solid core and eases into nothing —
         no line where the blur begins, none where it finishes. A plain
         two-stop gradient is linear, which shows a crease at both ends. */
      rgba(0,0,0,1) var(--orb-feather),
      rgba(0,0,0,0.9988) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.05),
      rgba(0,0,0,0.9914) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.10),
      rgba(0,0,0,0.9682) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.16),
      rgba(0,0,0,0.9256) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.22),
      rgba(0,0,0,0.8624) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.28),
      rgba(0,0,0,0.7801) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.34),
      rgba(0,0,0,0.6826) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.40),
      rgba(0,0,0,0.5747) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.46),
      rgba(0,0,0,0.5) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.50),
      rgba(0,0,0,0.4253) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.54),
      rgba(0,0,0,0.3174) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.60),
      rgba(0,0,0,0.2199) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.66),
      rgba(0,0,0,0.1376) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.72),
      rgba(0,0,0,0.0744) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.78),
      rgba(0,0,0,0.0318) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.84),
      rgba(0,0,0,0.0086) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.90),
      rgba(0,0,0,0.0012) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.95),
      rgba(0,0,0,0) 100%);
    mask-image: radial-gradient(circle closest-side,
      /* Smootherstep falloff (6t^5-15t^4+10t^3). Its slope is ZERO at both
         ends, so the fade eases out of the solid core and eases into nothing —
         no line where the blur begins, none where it finishes. A plain
         two-stop gradient is linear, which shows a crease at both ends. */
      rgba(0,0,0,1) var(--orb-feather),
      rgba(0,0,0,0.9988) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.05),
      rgba(0,0,0,0.9914) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.10),
      rgba(0,0,0,0.9682) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.16),
      rgba(0,0,0,0.9256) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.22),
      rgba(0,0,0,0.8624) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.28),
      rgba(0,0,0,0.7801) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.34),
      rgba(0,0,0,0.6826) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.40),
      rgba(0,0,0,0.5747) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.46),
      rgba(0,0,0,0.5) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.50),
      rgba(0,0,0,0.4253) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.54),
      rgba(0,0,0,0.3174) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.60),
      rgba(0,0,0,0.2199) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.66),
      rgba(0,0,0,0.1376) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.72),
      rgba(0,0,0,0.0744) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.78),
      rgba(0,0,0,0.0318) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.84),
      rgba(0,0,0,0.0086) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.90),
      rgba(0,0,0,0.0012) calc(var(--orb-feather) + (100% - var(--orb-feather)) * 0.95),
      rgba(0,0,0,0) 100%);
    -webkit-mask-repeat: no-repeat;  mask-repeat: no-repeat;
    -webkit-mask-position: 50% calc(50% + var(--orb-cy));
    mask-position: 50% calc(50% + var(--orb-cy));
    -webkit-mask-size: calc(var(--orb-r) * 2) calc(var(--orb-r) * 2);
    mask-size: calc(var(--orb-r) * 2) calc(var(--orb-r) * 2);
  }
  /* Dedicated circle-sized light: two compact sage blobs drifting inside the
     landing circle (the page-scale glow is too diffuse to read at this size).
     Clipped by the circle; hidden once the circle expands. */
  /* Paths are FIXED. Two earlier attempts to drive motion from the voice both
     failed here, and the reasons are worth keeping:
       - modulating animation-duration re-maps the running animation's phase,
         so the orb teleported along its path on every change;
       - scaling these distances kept it smooth but deformed the route itself,
         which read as inorganic — the orbs wandered in odd shapes.
     The path is the character; only its SPEED should answer the voice, and
     that is done with playbackRate from startBlobLoop() (see index.html),
     which changes rate while preserving position. */
  /* Elliptical orbits, sampled every 45 degrees (x = Rx·cos, y = Ry·sin).
     They used to be three stops that swung out and back THROUGH the origin,
     i.e. an oscillation along a diagonal, not an orbit.
     Two things matter here:
       - the paths are slightly elliptical and A/B run in OPPOSITE directions
         with different radii, so the pair never looks mechanically locked;
       - the timing function must be LINEAR. ease-in-out is applied between
         every consecutive pair of stops, so on an 8-stop orbit it decelerates
         into and accelerates out of all eight, and the lap visibly pulses.
     Scale and opacity still breathe once per lap, which keeps them alive
     without disturbing the circular path. */
  @keyframes circleLifeA {           /* clockwise, 9 x 7vmin */
    0%,100% { transform: translate(14vmin, 0)           scale(1.00); opacity: 0.85; }
    12.5%   { transform: translate(9.9vmin, 7.8vmin)    scale(1.06); opacity: 0.95; }
    25%     { transform: translate(0, 11vmin)           scale(1.12); opacity: 1.00; }
    37.5%   { transform: translate(-9.9vmin, 7.8vmin)   scale(1.06); opacity: 0.90; }
    50%     { transform: translate(-14vmin, 0)          scale(1.00); opacity: 0.75; }
    62.5%   { transform: translate(-9.9vmin, -7.8vmin)  scale(0.94); opacity: 0.65; }
    75%     { transform: translate(0, -11vmin)          scale(0.88); opacity: 0.60; }
    87.5%   { transform: translate(9.9vmin, -7.8vmin)   scale(0.94); opacity: 0.70; }
  }
  @keyframes circleLifeB {           /* counter-clockwise, 8 x 10vmin */
    0%,100% { transform: translate(12vmin, 0)           scale(1.00); opacity: 0.60; }
    12.5%   { transform: translate(8.5vmin, -10.6vmin)  scale(1.08); opacity: 0.72; }
    25%     { transform: translate(0, -15vmin)          scale(1.16); opacity: 0.85; }
    37.5%   { transform: translate(-8.5vmin, -10.6vmin) scale(1.08); opacity: 0.95; }
    50%     { transform: translate(-12vmin, 0)          scale(1.00); opacity: 0.85; }
    62.5%   { transform: translate(-8.5vmin, 10.6vmin)  scale(0.92); opacity: 0.65; }
    75%     { transform: translate(0, 15vmin)           scale(0.86); opacity: 0.50; }
    87.5%   { transform: translate(8.5vmin, 10.6vmin)   scale(0.92); opacity: 0.45; }
  }
  @keyframes circleLifeC {           /* clockwise, 6.5 x 9vmin */
    0%,100% { transform: translate(10vmin, 0)           scale(1.00); opacity: 0.55; }
    12.5%   { transform: translate(7.1vmin, 9.9vmin)    scale(1.05); opacity: 0.68; }
    25%     { transform: translate(0, 14vmin)           scale(1.10); opacity: 0.80; }
    37.5%   { transform: translate(-7.1vmin, 9.9vmin)   scale(1.05); opacity: 0.72; }
    50%     { transform: translate(-10vmin, 0)          scale(1.00); opacity: 0.60; }
    62.5%   { transform: translate(-7.1vmin, -9.9vmin)  scale(0.93); opacity: 0.48; }
    75%     { transform: translate(0, -14vmin)          scale(0.87); opacity: 0.40; }
    87.5%   { transform: translate(7.1vmin, -9.9vmin)   scale(0.93); opacity: 0.46; }
  }
  /* ---- Speaking: the three orbs swell outward from the centre ------------
     Each is pushed along its own outward direction, proportional to her voice,
     so together they expand and contract around the middle of the screen as
     she talks. `translate` is separate from `transform`, so the orbits keep
     running untouched underneath. */
  #mainWrap:where(:has(#mainDock.active)) #blobWrap::before {
    translate: calc(var(--voice-slow) * var(--orb-push) * -1)
               calc(var(--voice-slow) * var(--orb-push) * -0.35);
  }
  #mainWrap:where(:has(#mainDock.active)) #blobWrap::after {
    translate: calc(var(--voice-slow) * var(--orb-push) * 1)
               calc(var(--voice-slow) * var(--orb-push) * 0.55);
  }
  #mainWrap:where(:has(#mainDock.active)) #orb3 {
    translate: calc(var(--voice-slow) * var(--orb-push) * 0.25)
               calc(var(--voice-slow) * var(--orb-push) * 1);
  }
  /* Gathered for the spin — no outward push while thinking. */
  #mainWrap.thinking #blobWrap::before,
  #mainWrap.thinking #blobWrap::after,
  #mainWrap.thinking #orb3 { translate: 0 0; }

  /* ---- Thinking / waking up: the three orbs become a loading indicator ----
     They gather onto a shared centre and sweep one common circle, phased a
     third of a lap apart via negative animation-delay, at a steady linear
     rate. Reads as deliberate work rather than ambient drift. Ends the moment
     she starts speaking, when they scatter back to their own orbits. */
  @keyframes orbSpin {
    0%   { transform: rotate(0deg)   translateX(12.5vmin) rotate(0deg)   scale(1); }
    50%  { transform: rotate(180deg) translateX(12.5vmin) rotate(-180deg) scale(1.06); }
    100% { transform: rotate(360deg) translateX(12.5vmin) rotate(-360deg) scale(1); }
  }
  #mainWrap.thinking #blobWrap::before,
  #mainWrap.thinking #blobWrap::after,
  #mainWrap.thinking #orb3 {
    animation-name: orbSpin;
    animation-duration: 3.4s;
    animation-timing-function: linear;
    animation-iteration-count: infinite;
    top: 50%;
  }
  /* Centre each one on the same point (own width differs), and phase them
     120 degrees apart with a negative delay so the ring starts already formed. */
  #mainWrap.thinking #blobWrap::before {
    left: calc(50% - 33vmin); top: calc(50% - 33vmin); animation-delay: 0s;      opacity: 0.8;
  }
  #mainWrap.thinking #blobWrap::after {
    left: calc(50% - 28vmin); top: calc(50% - 28vmin); animation-delay: -1.133s; opacity: 0.8;
  }
  #mainWrap.thinking #orb3 {
    left: calc(50% - 24vmin); top: calc(50% - 24vmin); animation-delay: -2.266s; opacity: 0.8;
  }

  /* Third orb — same treatment as ::before/::after, just a real element. */
  #orb3 {
    position: absolute;
    border-radius: 50%;
    pointer-events: none;
    opacity: 0;
    width: 30vmin; height: 30vmin;
    left: calc(50% - 15vmin);
    top: calc(50% + var(--orb-cy) - 15vmin);
    background: radial-gradient(circle at 52% 46%,
      rgba(168,196,140,0.52) 0%,
      rgba(124,158,104,0.26) 40%,
      rgba(98,128,84,0.00) 68%);
    filter: blur(16px);
    transition: filter 0.12s linear, left 0.7s ease, top 0.7s ease, translate 0.25s ease-out;
    animation: circleLifeC var(--orb-drift-c) linear 3.6s infinite;
  }
  #blobWrap.loading #orb3,
  #mainWrap:where(:has(#mainDock.active)) #orb3 {
    width: 48vmin; height: 48vmin;
    left: calc(50% - 14vmin); top: calc(50% + 16vmin);
    filter: blur(calc(24px - var(--voice) * 6px))
            brightness(calc(0.9 + var(--voice) * 0.42));
    animation-delay: 0s;
  }
  #blobWrap::before,
  #blobWrap::after {
    content: "";
    position: absolute;
    border-radius: 50%;
    pointer-events: none;
  }
  #blobWrap::before {
    opacity: 0; /* hidden during the animation-delay window (post-session contraction) */
    width: calc(var(--orb-r) * 2.42); height: calc(var(--orb-r) * 2.42);
    left: calc(50% - var(--orb-r) * 1.21);
    top: calc(50% + var(--orb-cy) - var(--orb-r) * 1.21);
    background: radial-gradient(circle at 42% 40%,
      rgba(158,188,130,0.60) 0%,
      rgba(118,152,100,0.30) 38%,
      rgba(95,125,82,0.00) 68%);
    filter: blur(18px);
    /* delay lets the circle finish contracting before its light returns */
    animation: circleLifeA var(--orb-drift-a) linear 2.4s infinite;
  }
  #blobWrap::after {
    opacity: 0; /* hidden during the animation-delay window (post-session contraction) */
    width: calc(var(--orb-r) * 2); height: calc(var(--orb-r) * 2);
    left: calc(50% - var(--orb-r));
    top: calc(50% + var(--orb-cy) - var(--orb-r));
    background: radial-gradient(circle at 60% 62%,
      rgba(180,205,150,0.50) 0%,
      rgba(130,160,105,0.24) 40%,
      rgba(100,130,85,0.00) 68%);
    filter: blur(14px);
    animation: circleLifeB var(--orb-drift-b) linear 3s infinite;
  }
  /* Loading + session: the drifting light blobs stay alive fullscreen
     (previously hidden here — the field went dead flat between her turns).
     Bigger + softer to suit the fullscreen field, dimmed to ~80% baseline;
     they lift to full brightness while she speaks. */
  /* NOTE the :where() around :has(). Without it this rule silently killed the
     speaking surge below, for the whole session:
       :has() takes the specificity of its most specific argument, and
       #mainDock is an ID — so `#mainWrap:has(#mainDock.active) #blobWrap`
       scores THREE ids, out-ranking `#mainWrap.speaking #blobWrap` (two ids).
       Source order never gets a say. The .speaking rule matched the element
       and lost, so the orbs stayed at brightness(0.8) while Eleanor spoke —
       exactly the state this block's own comment says they should leave.
     :where() contributes ZERO specificity, so this reads as 2 ids and the
     speaking rule (2 ids + a class) correctly wins again. */
  /* In session the orbs used to grow to 64/54vmin at blur 32/28px, both
     CONCENTRIC on the same centre. Two big soft discs stacked on one point
     average into a single haze — which is why the orbs "went missing" once the
     circle bloomed. They stay compact and sharp instead, and are offset from
     each other so circleLifeA/B's drift separates them into two lights. */
  #blobWrap.loading::before,
  #mainWrap:where(:has(#mainDock.active)) #blobWrap::before {
    width: 66vmin; height: 66vmin;
    left: calc(50% - 52vmin); top: calc(50% - 40vmin);
    /* Tied to her voice, not to a class: brighter and a touch sharper as she
       gets louder, and the drift period contracts with the slow envelope, so
       they visibly quicken through a phrase and ease off in the pauses. */
    filter: blur(calc(30px - var(--voice) * 8px))
            brightness(calc(0.9 + var(--voice) * 0.42));
    /* No stagger here: that delay is only for the first page load. Leaving it
       made the orbs hang motionless for seconds when leaving the think-spin. */
    animation-delay: 0s;
    /* Duration stays CONSTANT; speed is changed via playbackRate in JS. */
    animation-duration: var(--orb-drift-a);
  }
  #blobWrap.loading::after,
  #mainWrap:where(:has(#mainDock.active)) #blobWrap::after {
    width: 56vmin; height: 56vmin;
    left: calc(50% + 4vmin); top: calc(50% - 10vmin);
    filter: blur(calc(26px - var(--voice) * 7px))
            brightness(calc(0.9 + var(--voice) * 0.42));
    animation-delay: 0s;
    animation-duration: var(--orb-drift-b);
  }
  /* Short, linear: this filter is now driven per frame from the analyser, and
     a 0.9s ease made it chase the voice about a second behind. Enough to take
     the edge off the step quantisation, not enough to lag. */
  #blobWrap::before,
  #blobWrap::after { transition: filter 0.12s linear, left 0.7s ease, top 0.7s ease, translate 0.25s ease-out; }
  /* NOTE: the orbs deliberately have NO .speaking rule any more. A class is
     binary — on for the whole reply, off between — which is why the surge read
     as unrelated to what she was actually saying. They now follow --voice /
     --voice-slow continuously (see the in-session block above), so a class rule
     here would only override the live value with a flat one. The --orb-drift-*-fast
     tokens remain as the reference for what "fully surged" means. */
  #blobWrap.loading,
  #mainWrap:has(#mainDock.active) #blobWrap {
    clip-path: circle(120vmax at 50% calc(50% + var(--orb-cy)));
    -webkit-clip-path: circle(120vmax at 50% calc(50% + var(--orb-cy)));
    /* Mask box tracks the clip radius (120vmax -> 240vmax box), so the feather
       stays proportional the whole way out. At rest the fade sits far beyond
       the viewport, so the fullscreen field still reads edge-to-edge. */
    -webkit-mask-size: 240vmax 240vmax;
    mask-size: 240vmax 240vmax;
  }
  @keyframes orbDrift {
    0%   { transform: translate3d(0, 0, 0) scale(1) rotate(0deg); }
    33%  { transform: translate3d(11vmax, 7vmax, 0) scale(1.14) rotate(9deg); }
    66%  { transform: translate3d(-8vmax, 12vmax, 0) scale(0.88) rotate(-8deg); }
    100% { transform: translate3d(0, 0, 0) scale(1) rotate(0deg); }
  }
  #beginBtn{
    width:100%; height:100%; border:none; padding:0;
    border-radius: 50%;
    /* gradient center matches the landing circle so the glow lives inside it */
    background: radial-gradient(ellipse at 50% calc(50% + var(--orb-cy)), rgba(140,164,118,.22) 0%, rgba(112,140,98,.12) 30%, rgba(96,124,86,.05) 58%, rgba(96,124,86,.00) 78%);
    cursor:default;
    animation: orbDrift 14s ease-in-out infinite;
    filter: blur(60px) brightness(1.0);
    visibility: hidden;
    /* Not clickable: this glow layer spans the whole screen, which made a tap
       ANYWHERE start a session. Only the "Tap to Start" pill (#blobLabel,
       which has its own click handler) starts one now. */
    pointer-events: none;
  }
  #beginBtn:active { opacity: 0.85; }
  #beginBtn { position: relative; overflow: hidden; }
  /* Loading state — freeze morph/float/glow, pure CSS breathe pulse */
  /* Transform/opacity only — blur stays constant so the huge surface never
     re-rasterizes; brightness pulses are expressed as opacity over the dark bg. */
  @keyframes loadingBreath {
    0%, 100% { opacity: 0.45; transform: scale(0.92); }
    50%       { opacity: 1.0;  transform: scale(1.06); }
  }
  #blobWrap.loading { animation: none; }
  #blobWrap.loading #beginBtn {
    animation: loadingBreath 2.4s ease-in-out infinite !important;
    border-radius: 50% !important;
    visibility: visible;
    /* Once the circle is fullscreen the landing gradient is too faint to
       read. The pulse must not LIGHTEN the field (washed-out look rejected):
       instead of a pale additive glow, breathe a deeper, saturated green —
       reads as the color growing richer, not lighter. Blur stays constant
       (no re-rasterize); the pulse remains opacity/transform-only. */
    background: radial-gradient(ellipse at 50% calc(50% + var(--orb-cy)),
      rgba(74, 112, 52, 0.55) 0%,
      rgba(58, 92, 44, 0.32) 36%,
      rgba(44, 72, 36, 0.14) 62%,
      rgba(44, 72, 36, 0.00) 82%);
    filter: blur(60px) saturate(1.25);
  }
  /* Once the session is live (dock active) the loading class can linger on
     #blobWrap — hand back to the normal ambient/speaking visuals so the
     loading pulse doesn't keep breathing under her voice. */
  #mainWrap:has(#mainDock.active) #blobWrap.loading #beginBtn {
    animation: orbDrift 14s ease-in-out infinite !important;
    background: radial-gradient(ellipse at 50% calc(50% + var(--orb-cy)), rgba(140,164,118,.22) 0%, rgba(112,140,98,.12) 30%, rgba(96,124,86,.05) 58%, rgba(96,124,86,.00) 78%);
    filter: blur(60px) brightness(1.0);
  }
  /* Must live AFTER the rule above (same specificity + !important — source
     order decides) so reduced-motion still wins. */
  @media (prefers-reduced-motion: reduce) {
    #mainWrap:has(#mainDock.active) #blobWrap.loading #beginBtn { animation: none !important; }
  }
  /* The shimmer canvas keeps its 200x200 backing store (JS-owned), so instead
     of stretching it 10x+ across the viewport we present it as a centered
     voice-reactive bloom: modest upscale, heavy blur, blended into the glow. */
  /* Shimmer retired — the circleLife blobs carry the living-field feel. */
  #shimmerCanvas { display: none !important; }
  #shimmerCanvas {
    position:absolute;
    left:50%; top:calc(50% + var(--orb-cy));
    /* Anchored to the landing-circle center so the voice bloom lives inside
       the circle and stays lower-center once expanded fullscreen. */
    width:110vmin; height:110vmin;
    transform: translate(-50%, -50%);
    inset:auto;
    pointer-events:none;
    border-radius:50%;
    /* sage pixels painted at source — blur only, no tint filter needed */
    filter: blur(16px);
    opacity:1;
    transition: filter 0.8s ease;
  }
  /* (Speaking brightness lift removed — the circleLife blobs carry the
     speaking surge now; the shimmer is a faint supporting layer.) */
  @keyframes blobThink {
    0%   { opacity: 0.45; transform: scale(0.94) translate(0px,   0px); }
    25%  { opacity: 0.75; transform: scale(0.98) translate(28px, -34px); }
    50%  { opacity: 1.00; transform: scale(1.02) translate(-12px, -46px); }
    75%  { opacity: 0.70; transform: scale(0.96) translate(-34px, -12px); }
    100% { opacity: 0.45; transform: scale(0.94) translate(0px,   0px); }
  }

  /* ---------- Landing headline ---------- */
  @keyframes headlineFadeIn {
    from { opacity: 0; transform: translateX(-50%) translateY(calc(-50% + 10px)); }
    to   { opacity: 1; transform: translateX(-50%) translateY(-50%); }
  }
  #welcomeText {
    position: fixed;
    top: 7%; left: 50%;
    transform: translate(-50%, -50%);
    text-align: center;
    z-index: 2;
    font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    pointer-events: none;
    /* Logo removed per design request (CSS-only; script toggles stay harmless). */
    display: none !important;
    transition: opacity 0.8s ease;
  }
  #welcomeText img { height: clamp(27px, 6vw, 54px); width: auto; filter: brightness(0) invert(1); opacity: 0.92; }

  #lgConnector {
    position: fixed;
    left: 50%; transform: translateX(-50%);
    top: calc(12% + clamp(35px, 18vw, 75px));
    bottom: calc(max(28px, env(safe-area-inset-bottom, 28px)) + 365px);
    width: 1px;
    background: linear-gradient(to bottom, rgba(255,255,255,0.70) 0%, rgba(255,255,255,0.20) 100%);
    z-index: 2;
    pointer-events: none;
    opacity: 0;
    transition: opacity 0.8s ease;
  }
  #lgConnector.visible { opacity: 1; }
  /* Design: thin connector line removed from the logged-in landing (CSS-only; JS still toggles classes harmlessly) */
  #lgConnector, #lgConnector.visible { display: none; }
  #lgConnector.hidden  { opacity: 0; }

  #landingHeadline {
    position: fixed;
    /* Bottom-anchored just above the landing circle (top edge = 50% + 17vmin
       - 4vh - 19vmin radius) so longer headlines grow UPWARD instead of
       colliding with the circle. Keep the 19vmin in sync with the circle
       radius in #blobWrap's clip-path. The script's translateY(-100%) pins
       the text block's bottom to this line. */
    top: calc(50% + var(--orb-cy) - var(--orb-r) - 6vh); left: 50%;
    z-index: 2;
    font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size: clamp(27px, 5.4vw, 48px); font-weight: 500;
    /* Dark warm grey sampled from the user's reference screenshot */
    color: #161513; text-align: center;
    letter-spacing: -0.01em; line-height: 1.1;
    width: min(70.4vw, 515px); /* 20% narrower than the original 88vw/644px */
    transition: opacity 1.3s cubic-bezier(.22,.8,.36,1), transform 1.3s cubic-bezier(.22,.8,.36,1);
    pointer-events: none;
  }
  /* Post-session re-entry: the script drives these reveals with INLINE
     transitions (which beat CSS transition-delay), so the delayed fade-in is
     expressed as a keyframe animation instead — animations outrank inline
     styles. Runs whenever the landing state (re)applies: page load and the
     moment a session ends. fill-mode both keeps them hidden through the
     circle's contraction; the pinned rules below (!important) still win
     during loading/session. */
  @keyframes welcomeBackIn {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  #mainWrap:not(:has(#mainDock.active)):not(:has(#blobWrap.loading)):not(:has(#loginGate.visible)) #landingHeadline {
    animation: welcomeBackIn 1.3s cubic-bezier(.22,.8,.36,1) 1.9s both !important;
  }
  #mainWrap:not(:has(#mainDock.active)):not(:has(#blobWrap.loading)):not(:has(#loginGate.visible)) #blobLabel {
    animation: welcomeBackIn 0.9s cubic-bezier(.22,.8,.36,1) 2.3s both !important;
  }
  /* "Just a moment…" waits for the circle to finish expanding (1.8s) —
     same keyframe device: outranks the script's inline reveal. */
  #welcome:has(#blobWrap.loading) #loadingLabel {
    animation: welcomeBackIn 1.1s ease 1.8s both !important;
  }
  /* Nav + dashboard buttons wait out the closing contraction too */
  #mainWrap:not(:has(#mainDock.active)):not(:has(#blobWrap.loading)):not(:has(#loginGate.visible)) #menuBtn,
  #mainWrap:not(:has(#mainDock.active)):not(:has(#blobWrap.loading)):not(:has(#loginGate.visible)) #bookBtn {
    animation: welcomeBackIn 0.9s ease 1.9s both !important;
  }
  /* While loading or in-session the welcome text and start label are pinned
     hidden (beats the script's early inline reveal at session end); when the
     session state clears they fade in on the delayed transition, after the
     circle has finished contracting. */
  #welcome:has(#blobWrap.loading) #landingHeadline,
  #mainWrap:has(#mainDock.active) #landingHeadline {
    opacity: 0 !important;
    transition-delay: 0s, 0s;
  }
  #welcome:has(#blobWrap.loading) #blobLabel,
  #mainWrap:has(#mainDock.active) #blobLabel {
    opacity: 0 !important;
    transition-delay: 0s, 0s, 0s;
  }
  #landingHeadline.hidden { opacity: 0; }

  #blobLabel {
    /* Pill button below the circle, same style family as the nav/dashboard
       pills (light fill + hairline edge, no shadow) */
    position: fixed;
    top: auto; bottom: 9vh; left: 50%;
    transform: translateX(-50%);
    z-index: 2;
    background: #fbfaf7;
    border: 1px solid #d9d8d2;
    backdrop-filter: none; -webkit-backdrop-filter: none;
    box-shadow: none;
    border-radius: 999px; padding: 16px 36px;
    font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    font-size: 16px; font-style: normal; letter-spacing: 0.04em; text-transform: none;
    color: rgba(0,0,0,0.88); font-weight: 500;
    white-space: nowrap; user-select: none;
    opacity: 0;
    transition: opacity 0.9s cubic-bezier(.22,.8,.36,1), background 0.2s;
    visibility: hidden;
    cursor: pointer;
  }
  #blobLabel:hover { background: #efeee8; }
  #blobLabel:active { transform: translateX(-50%) scale(0.97); }
  /* ---------- Listening indicator ---------- */

  #finishPill {
    /* cancel the dock's raised bottom padding so the pill stays where it was */
    margin-bottom: -28px;
    opacity: 0; pointer-events: none;
    transform: translateY(10px);
    transition: opacity .45s ease, transform .45s ease;
  }
  #finishPill.show {
    opacity: 1; pointer-events: auto;
    transform: translateY(0);
  }
  #finishPillBtn {
    /* Flat pill language (dark variant) — matches the mic.
       Fill nudged to 0.14 and border to 0.40 so the button shape reads
       against the animated dark field without any glass/blur/shadow. */
    background: rgba(237,232,223,0.14);
    border: 1px solid rgba(237,232,223,0.40);
    color: rgba(237,232,223,0.78);
    width: 44px; height: 44px; border-radius: 50%; display: grid; place-items: center;
    cursor: pointer; font-family: inherit;
    transition: background-color .2s, color .2s;
  }
  #finishPillBtn svg { width: 18px; height: 18px; }
  #finishPillBtn:hover {
    background: rgba(237,232,223,0.22);
    color: rgba(237,232,223,0.95);
  }
  #finishPillBtn:active { transform: scale(0.97); }

  /* ---------- Interviewer's live text — overlaps blob's bottom third ---------- */
  #interviewerText {
    position: fixed;
    /* Box centered on the upper third of the viewport */
    top: 33.333%; left: 50%; transform: translate(-50%, -50%);
    z-index: 3;
    width: min(82vw, 460px);
    /* matched to the landing-headline style per design request: thin weight,
       soft warm-white, relaxed leading */
    /* Sized/weighted to match #landingHeadline (intro type) */
    font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size: clamp(27px, 5.4vw, 48px); font-weight: 500; line-height: 1.35; letter-spacing: -0.01em;
    color: rgba(237,232,223,0.82); text-align: center;
    opacity: 0; transition: opacity .5s;
    /* CONSTANT-height box (not max-height): the box must never grow with the
       text, or the top fade band travels with the box edge. Text is bottom-
       anchored via flex, so new lines enter low and older lines rise up into
       the stationary fade at the top — opacity changes as type approaches it. */
    height: calc(1.35em * 9);
    display: flex; flex-direction: column; justify-content: flex-end;
    overflow: hidden;
    /* Scroll drift is handled by the rAF typewriter loop (lerp per frame),
       so CSS scroll-behavior is intentionally absent — the two would fight. */
  }
  /* Inner wrapper receives the speaking glow so that the outer element's
     mask-image (which fades overflowing upper lines) clips the brightened
     content correctly — filter and mask-image on the same element can
     interact unpredictably across browsers, lifting faded upper lines. */
  #interviewerTextInner {
    display: block;
    transition: text-shadow 0.9s ease, filter 0.9s ease;
  }
  @media (min-width: 900px) {
    #interviewerText { width: min(70vw, 720px); height: calc(1.35em * 9); }
  }
  #interviewerText.visible { opacity: 1; }
  /* Spans carry tokens — keep whitespace intact, no per-word animation */
  #interviewerText span {
    white-space: pre-wrap;
  }
  /* ── Line-by-line reveal ──────────────────────────────────────────────────
     Each rendered line arrives as a .tw-line div; JS measures offsetTop to
     determine groupings, then reveals one div per interval in sync with audio.
     Soft opacity fade only — no transform, no per-word flicker.
     ABSOLUTE CONSTRAINT: no text-shadow/filter/brightness anywhere in this
     block — rejected three times, guard comments in the speaking-state rules. */
  @keyframes lineIn {
    /* OPACITY ONLY — no transform, no will-change. Animating transforms on
       children of the mask-image container makes the mask intermittently
       drop out (random full-white flashes at the faded top, esp. iOS).
       Confirmed by user Aug 15, 2026 (v222). Do not add motion back here. */
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  .tw-line {
    opacity: 0;
  }
  .tw-line.tw-line-visible {
    animation: lineIn 1s cubic-bezier(.25,.6,.35,1) forwards;
  }
  @media (prefers-reduced-motion: reduce) {
    .tw-line,
    .tw-line.tw-line-visible { animation: none; opacity: 1; }
  }
  /* Top fade: only active when the reply is long enough to overflow the
     container. Non-overflowing replies (short answers, first lines) are fully
     opaque — the gradient starts at transparent 0% which would clip a single
     line that sits at the top of the element. The JS typewriter loop toggles
     .overflowing via el.classList.toggle('overflowing', el.scrollHeight > el.clientHeight). */
  /* The fade is ALWAYS on (not gated behind the script's .overflowing
     toggle): the user wants it present from the first line, before the text
     reaches the top. New lines appear lower in the container, so short
     replies aren't clipped by the transparent top band. A task merge has
     restored the ".overflowing" gate once before — keep it ungated. */
  #interviewerText {
    /* Fixed-length fade band (px, not %): percentage stops travel as the
       element grows line-by-line, which reads as the gradient drifting.
       Pixel stops keep the fade pinned to the top of the container no
       matter how tall the reply gets. Band scales with the type via em
       (1em = current font-size; ~4.5 lines to fully opaque, as before). */
    -webkit-mask-image: linear-gradient(to bottom, transparent 0, rgba(0,0,0,0.12) 1.2em, rgba(0,0,0,0.30) 2.4em, rgba(0,0,0,0.55) 3.8em, rgba(0,0,0,0.80) 5em, black 6.2em);
    mask-image: linear-gradient(to bottom, transparent 0, rgba(0,0,0,0.12) 1.2em, rgba(0,0,0,0.30) 2.4em, rgba(0,0,0,0.55) 3.8em, rgba(0,0,0,0.80) 5em, black 6.2em);
  }

  /* ---------- Live chapter heading — which part of the book we're in ----------
     Sits just BENEATH the #siteMark wordmark; setChapterHeading measures the
     wordmark's real bottom edge per device and overrides this fallback top. */
  #chapterHeading {
    position: fixed;
    top: 84px; left: 50%; transform: translateX(-50%);
    z-index: 3;
    max-width: min(74vw, 560px);
    font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif; font-size: 15px; font-weight: 100;
    letter-spacing: 0.16em; text-transform: uppercase;
    color: var(--g1); text-align: center;
    white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
    text-shadow: 0 2px 12px rgba(0,0,0,0.25);
    opacity: 0; transition: opacity .45s ease, text-shadow 0.9s ease, filter 0.9s ease;
    pointer-events: none;
  }
  #chapterHeading.visible { opacity: 0.62; }
  /* Design: chapter heading hidden for now (JS still toggles classes harmlessly) */
  #chapterHeading, #chapterHeading.visible { visibility: hidden; }
  @media (max-width: 600px) {
    #beginBtn {
      background: radial-gradient(ellipse at 46% 42%, rgba(136,160,114,.16) 0%, rgba(110,138,96,.08) 30%, rgba(96,124,86,.03) 58%, rgba(96,124,86,.00) 78%);
      filter: blur(48px) brightness(0.8);
    }

    /* Menu is full-screen everywhere now; just tighten phone padding. */
    #menuPanel{
      padding:60px 24px 32px;
    }
    /* clamp() in the base .menuItem rule handles phone sizing now */
    #menuUserEmail{ word-break:normal; overflow-wrap:anywhere; }
  }


  /* ---------- Login gate — floating glass card ---------- */
  #loginGate{
    position:fixed; inset:0; z-index:8;
    display:flex; align-items:flex-end; justify-content:center;
    padding:0; /* card spans edge to edge, flush with the bottom */
    opacity:0; pointer-events:none;
    transition:opacity 0.5s ease, padding-bottom 0.25s ease;
    /* NOTE: no transform/will-change here! Promoting #loginGate to its own
       compositing group makes it the backdrop root, so the ::before's
       backdrop-filter can no longer sample (blur) the page behind the gate.
       The iOS first-frame layer promotion lives on the ::before instead. */
  }
  /* While the gate is up, the landing UI (nav/dashboard buttons, intro
     headline) sits BENEATH the frosted blur as a preview of the app. */
  #mainWrap:has(#loginGate.visible) #menuBtn,
  #mainWrap:has(#loginGate.visible) #bookBtn{
    z-index:5;
  }
  /* Frosted wash over everything behind the gate (circle, landing); the
     card and logo are children so they stay sharp. Fades with the gate. */
  #loginGate::before{
    content:"";
    position:absolute; inset:0;
    backdrop-filter:blur(14px);
    -webkit-backdrop-filter:blur(14px);
    background:rgba(238,238,231,0.30);
    pointer-events:none;
    /* Own compositing layer keeps the blur smooth during the parent opacity
       fade and prevents the stale unblurred frame on iOS Safari that appears
       when backdrop-filter is composited lazily alongside the parent. */
    -webkit-transform:translateZ(0); transform:translateZ(0);
    will-change:opacity;
  }
  #loginGate.visible{ opacity:1; pointer-events:auto; }
  /* Wordmark pinned top-center of the sign-in screen */
  #lgLogo{
    position:absolute;
    top:25vh; /* top quarter, just above the headline */
    left:50%; transform:translate(-50%,-100%);
    height:56px; width:auto;
    pointer-events:none;
  }
  /* Card restyled for the CREAM landing page (the old white-glass/white-text
     look was designed for a dark field and was unreadable here). */
  #lgCard{
    width:100%;
    background:#161513;
    border:1px solid #161513;
    border-bottom:none;
    border-left:none; border-right:none;
    border-radius:24px 24px 0 0; /* bottom-sheet: flush with screen edges */
    padding:44px 28px calc(30px + env(safe-area-inset-bottom, 0px));
    transform:translateY(180px);
    transition:transform 0.68s cubic-bezier(0.16,1,0.3,1);
    box-shadow:0 20px 60px rgba(0,0,0,0.30);
  }
  #loginGate.visible #lgCard{ transform:translateY(0); }
  #lgHandle{ display:none; }
  #lgCardLabel{
    font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;
    font-size:clamp(26px,5.5vw,38px); font-weight:500;
    text-align:center;
    color:#fbfaf7;
    line-height:1.1; letter-spacing:-0.02em;
    margin-bottom:10px; display:block;
  }
  #lgSubtitle{
    font-family:-apple-system,system-ui,sans-serif;
    font-size:14px; font-weight:400; line-height:1.55;
    color:rgba(251,250,247,0.60);
    margin:0 0 22px;
    text-align:center;
  }
  #lgEmail{
    width:100%; display:block;
    text-align:center;
    background:rgba(255,255,255,0.08);
    border:1px solid rgba(255,255,255,0.20);
    border-radius:999px;
    padding:13px 20px;
    font-family:-apple-system,system-ui,sans-serif;
    font-size:15px; color:#fbfaf7;
    caret-color:rgba(251,250,247,0.70);
    outline:none;
    transition:border-color 0.2s, background 0.2s;
    -webkit-appearance:none;
  }
  #lgEmail::placeholder{ color:rgba(251,250,247,0.35); }
  #lgEmail:focus{
    border-color:rgba(255,255,255,0.40);
    background:rgba(255,255,255,0.12);
  }
  #lgSubmit{
    width:100%; display:block;
    margin-top:10px;
    background:#fbfaf7;
    border:1px solid #fbfaf7;
    border-radius:999px;
    padding:14px;
    font-family:-apple-system,system-ui,sans-serif;
    font-size:15px; font-weight:500; letter-spacing:0.01em;
    color:#161513;
    cursor:pointer;
    opacity:0.30;
    transition:opacity 0.25s ease, background 0.2s;
    pointer-events:none;
  }
  #lgSubmit.ready{ opacity:1; pointer-events:auto; }
  #lgSubmit.ready:hover{ background:#ffffff; }
  #lgSubmit.ready:active{ opacity:0.80; }
  #lgMsg{
    font-family:-apple-system,system-ui,sans-serif;
    font-size:13px; font-style:italic;
    color:rgba(251,250,247,0.50);
    margin-top:12px;
    min-height:18px; display:block;
  }

  /* ---------- Inactivity nudge ---------- */

  /* ---------- Mic trouble notice ----------
     Nothing in this app could tell the person anything was wrong: setStatus()
     writes into a display:none sink and the text input is hidden too, so every
     speech failure presented as a button that simply did nothing. This is the
     one surface that says what happened. Hidden unless something actually fails. */
  #micTrouble{
    position:fixed; left:50%; transform:translateX(-50%);
    bottom:calc(env(safe-area-inset-bottom, 0px) + 196px);
    width:min(500px, calc(100vw - 36px));
    background:rgba(22,18,14,0.95); color:#f3ece1;
    border:1px solid rgba(255,255,255,0.15); border-radius:15px;
    padding:15px 34px 14px 17px; z-index:9000;
    font:400 14px/1.55 -apple-system,BlinkMacSystemFont,'Segoe UI',sans-serif;
    box-shadow:0 18px 50px rgba(0,0,0,0.5);
    opacity:0; pointer-events:none; transition:opacity .3s ease;
    -webkit-backdrop-filter:blur(12px); backdrop-filter:blur(12px);
  }
  #micTrouble.visible{ opacity:1; pointer-events:auto; }
  #micTroubleTitle{ display:block; font-weight:600; font-size:15px; margin-bottom:5px; }
  #micTrouble ol{ margin:9px 0 0 17px; padding:0; }
  #micTrouble li{ margin:3px 0; }
  #micTrouble .k{ font-weight:600; }
  #micTroubleClose{
    position:absolute; top:7px; right:9px; background:none; border:0;
    color:rgba(255,255,255,0.5); font-size:21px; line-height:1;
    cursor:pointer; padding:3px 7px; font-family:inherit;
  }

  /* ---------- Mic dock (shown after conversation starts) ---------- */
  #mainDock{
    display:none; flex-direction:column; align-items:center;
    gap:20px; padding:0 0 calc(64px + env(safe-area-inset-bottom, 0px));
    position:fixed; bottom:0; left:50%; transform:translateX(-50%);
    z-index:10;
  }
  /* Controls stay hidden while the circle expands (2.2s), then fade in */
  @keyframes dockIn {
    from { opacity: 0; }
    to   { opacity: 1; }
  }
  #mainDock.active{ display:flex; animation: dockIn 0.8s ease 2.2s both; }
  .mic{
    /* extra space below lifts the mic without moving the finish pill */
    margin-bottom:16px;
    /* Flat pill language (dark variant): solid subtle fill, hairline border,
       no glass gradients/blur/shadows — matches the light nav pills.
       Fill nudged to 0.14 and border to 0.40 so the button shape reads
       against the animated dark field without any glass/blur/shadow. */
    width:var(--mic-d); height:var(--mic-d); border-radius:50%; border:1px solid rgba(237,232,223,0.40);
    cursor:pointer;
    background:rgba(237,232,223,0.14);
    color:rgba(237,232,223,0.82); display:grid; place-items:center;
    position:relative; transition:transform .12s, background-color .2s;
  }
  .mic:hover{
    background:rgba(237,232,223,0.21);
  }
  .mic:not(.listening):active{transform:scale(.94); background:rgba(237,232,223,0.30);}
  .mic.listening:active{transform:scale(.94);}
  .mic:disabled{opacity:.35; cursor:default;}
  .mic svg{width:33px; height:33px;}
  .mic.listening{
    background:rgba(210,80,40,0.72);
    animation: micGlow 1.4s ease-in-out infinite;
  }
  @keyframes micGlow{
    0%,100%{ box-shadow:0 8px 24px rgba(0,0,0,.2), 0 0 0 0 rgba(210,80,40,0); }
    50%     { box-shadow:0 8px 24px rgba(0,0,0,.2), 0 0 22px 7px rgba(210,80,40,.45); }
  }
  .mic.listening::before,.mic.listening::after{
    content:""; position:absolute; inset:0; border-radius:50%;
    border:1.5px solid rgba(255,180,120,.6);
    animation:ring 1.6s ease-out infinite; opacity:0;
  }
  .mic.listening::after{animation-delay:.8s;}
  @keyframes ring{0%{transform:scale(1);opacity:.55;}100%{transform:scale(1.85);opacity:0;}}
  #micWrap{ display:inline-flex; flex-direction:column; align-items:center; gap:10px; position:relative; }
  #micHint{
    /* Centre the 120x120 SVG on the button's centre: the arc used to sit 6px
       high, so it read as a different circle rather than a ring around the
       button. */
    position:absolute; top:calc(var(--mic-d) / 2 - 60px); left:50%;
    transform:translateX(-50%);
    pointer-events:none; user-select:none;
    opacity:0; transition:opacity 0.5s ease;
  }
  #micHint.visible{ opacity:1; }
  @media (hover:none),(pointer:coarse){ #micHint{ display:none; } }


  /* Hidden legacy conversation area (kept for JS refs) */
  #app{display:none !important;}

  /* =====================================================================
     DESKTOP COMPOSITION (min-width: 768px)
     Additive layer only — every rule below lives inside min-width media
     queries so the mobile experience stays pixel-identical. The goal:
     the same warm, unhurried product, composed for a large screen instead
     of a stretched phone.
     ===================================================================== */

  @media (min-width: 768px) {

    /* ---------- Sign-up gate: bottom sheet becomes a floating card ----- */
    /* Vertical rhythm at 720-900px tall: wordmark pinned near the top,
       headline beneath it, card starting at 35vh, orb glow breathing below.
       The card is top-anchored (not centered) so it can never ride up into
       the headline on shorter desktop viewports. */
    #loginGate{
      align-items:flex-start;       /* was flex-end (bottom sheet) */
      padding:35vh 48px 40px;
    }
    /* While the gate is up, tuck the landing headline into the band between
       the wordmark and the card. It is bottom-anchored (JS translateY(-100%))
       so longer lines grow upward toward the logo, never down into the card. */
    #mainWrap:has(#loginGate.visible) #landingHeadline{
      top:31vh;
      width:min(46vw, 560px);
      font-size:clamp(24px, 2.4vw, 34px);
    }
    #lgCard{
      width:min(460px, calc(100vw - 96px));
      border:1px solid #2a3021;     /* restore the edges the sheet dropped */
      border-radius:28px;
      padding:44px 48px 36px;
      text-align:center;
      transform:translateY(28px);
      box-shadow:0 32px 90px rgba(30,36,22,0.35), 0 4px 18px rgba(30,36,22,0.18);
    }
    #loginGate.visible #lgCard{ transform:translateY(0); }
    #lgLogo{
      /* !important: showGate() writes an inline top based on the headline's
         measured position; on desktop the CSS placement is authoritative. */
      top:96px !important;          /* bottom edge at 96px — clear of the headline band */
      height:56px;
    }
    #lgCardLabel{ font-size:34px; }
    #lgSubtitle{ font-size:15px; margin-bottom:26px; }
    #lgEmail{ padding:14px 22px; font-size:16px; }
    #lgSubmit{ margin-top:12px; padding:15px; font-size:16px; }

    /* Keep-your-stories modal (injected styles) — a touch more room to
       breathe on a large screen. div#id outranks the injected #id rules. */
    div#hlAuthCard{
      max-width:420px;
      border-radius:22px;
      padding:34px 36px 26px;
    }
    div#hlAuthCard h3{ font-size:22px; }
    div#hlAuthCard p{ font-size:15px; }

    /* ---------- Menu: full-screen sheet becomes a left drawer ---------- */
    #menuPanel{
      width:min(440px, 100vw);
      padding:120px 56px 44px;
      border-right:1px solid rgba(0,0,0,0.07);
    }
    #menuPrimary{ padding-top:0; }
    .menuItem{ font-size:38px; padding:6px 0; }

    /* ---------- Book / archive panel: centered reading column ---------- */
    #bookPanelHeader{ padding:24px 28px 8px; }
    #bookPanelBody{
      padding-left:max(24px, calc(50vw - 380px));
      padding-right:max(24px, calc(50vw - 380px));
      padding-bottom:96px;
    }
    .bpSection{ margin-bottom:52px; }
    .bpAccHead{ padding:22px 26px; }
    .bpAccBody{ padding:0 26px 22px; }
    /* Selection action bar: floating panel instead of edge-to-edge sheet */
    #bpActionBar{
      left:50%;
      right:auto;
      width:min(620px, calc(100vw - 64px));
      bottom:28px;
      margin-left:calc(min(620px, 100vw - 64px) / -2); /* center without touching transform */
      border:1px solid rgba(0,0,0,0.10);
      border-radius:18px;
      padding:16px 22px 18px;
      box-shadow:0 18px 60px rgba(0,0,0,0.18);
    }
    #bpActionBar{ transform:translateY(140%); }
    #bpActionBar.up{ transform:translateY(0); }

    /* ---------- Landing: headline + start pill scaled for distance ------ */
    /* The circle reads far heavier on a desktop viewport than on a phone:
       19vmin of a wide window is a much larger disc relative to the text.
       Pulled back to 60% of the mobile radius. Halos and the headline anchor
       follow automatically — they all derive from this one value. */
    :root{
      --orb-r: 16vmin;
      /* Desktop: sit the circle at screen centre rather than low in the page —
         there is plenty of height here, and the low anchor left it stranded
         near the bottom with the headline floating far above it. */
      --orb-cy: 0vh;
      --mic-d: 88px;
    }
    #landingHeadline{
      width:min(56vw, 640px);
      font-size:clamp(34px, 3.6vw, 52px);
    }
    #blobLabel{
      bottom:10vh;
      padding:18px 44px;
      font-size:17px;
    }

    /* ---------- Conversation: captions, mic dock, loading ---------- */
    #interviewerText{
      width:min(64vw, 760px);
      font-size:clamp(28px, 3vw, 40px);
      line-height:1.4;
      height:calc(1.4em * 9);
    }
    #loadingLabel{ font-size:21px; }
    #mainDock{
      gap:22px;
      padding-bottom:calc(72px + env(safe-area-inset-bottom, 0px));
    }
    .mic{ width:var(--mic-d); height:var(--mic-d); }
    .mic svg{ width:36px; height:36px; }
    #finishPillBtn{ width:48px; height:48px; }
    #finishPillBtn svg{ width:20px; height:20px; }

    /* Nav pills sit a little further from the edge on a big screen */
    #menuBtn{ left:24px; top:24px; }
    #bookBtn{ right:24px; top:24px; }
    #menuClose{ left:24px; top:24px; }

    /* Mic trouble notice: keep it a card, not a banner */
    #micTrouble{ width:min(440px, calc(100vw - 48px)); }
  }

  @media (min-width: 1200px) {
    /* Very wide screens: give the reading column a hair more presence */
    #bookPanelBody{
      padding-left:calc(50vw - 400px);
      padding-right:calc(50vw - 400px);
    }
    #interviewerText{ width:min(58vw, 800px); }
  }
