/* ==========================================================================
   Wandernote — The Living Page
   Loaded AFTER css/app.css. Consumes css/tokens.css only; every scene pigment
   is DERIVED from a token via color-mix (same pattern app.css uses for
   --leather), with one per-theme anchor so nothing hardcodes a colour.

   DESIGN PRINCIPLE: the light comes from a candle.
   One warm source at the page's corner drives the tint, the vignette, the
   caret glow, the mote drift and the flare on typing. Whisper volume by
   default: typography and warmth do the work. Nothing here blocks typing,
   nothing animates a layout property, and nothing touches #note-editor's DOM.

   --------------------------------------------------------------------------
   CONTRAST AUDIT — worst case under the STRONGEST tint, both themes
   --------------------------------------------------------------------------
   Tint colour: amber rgba(214,150,66, a).  --flame is 0..1, so alpha peaks at
   its cap. Computed with WCAG 2.1 relative luminance, full compositing of the
   blend mode against the real token surfaces.

   DAYLIGHT — #light-layer sits ABOVE the page, mix-blend-mode: multiply,
   alpha = calc(.10 + .14 * --flame)  =>  a_max = 0.24.
   Multiplier at a_max: (0.9614, 0.9011, 0.8222).
   Text is multiplied too, so the ratio barely moves:

     text tier     surface        before    after a_max
     --ink         --bg-raised    12.77      10.95
     --ink-soft    --bg-raised     7.71       6.98
     --ink-faint   --bg-raised     6.29       5.79
     --ink         --bg           11.69      10.05   (bound only: while the
     --ink-faint   --bg            5.76       5.31     candle is lit the page
                                                       keeps an opaque surface,
                                                       so no text is ever read
                                                       through the pane's --bg)

   The same layer also carries an edge vignette (up to 0.26 alpha of
   --scene-dark), which multiply DARKENS with, so it moves the ratio the other
   way but stays clear:
     --ink       --bg-raised  vignette only   8.57
     --ink-faint --bg-raised  vignette only   5.08
     --ink-faint --bg-raised  tint AND vignette stacked at full strength
                              (geometrically impossible, since the vignette is
                              transparent inside 38% of the radius)      4.61

   Worst case Daylight = 4.61:1  >= 4.5:1.  PASS.

   HEARTHLIGHT — the light layer sits BELOW the page (z-index: -1 inside the
   isolated #editor-pane), additive warm glow + deep-indigo edge vignette,
   alpha = calc(.06 + .18 * --flame)  =>  a_max = 0.24.
   WHY below, not above: Hearthlight's tightest token pairing is --ink-faint on
   --bg-raised at 4.65:1 (tokens.css audit) — only 0.15 of headroom. Because of
   WCAG's +0.05 flare term, BOTH directions of compositing eat it:
       screen/additive amber a=0.05 -> 4.43:1      (fails)
       multiply amber      a=0.08 -> 4.48:1        (fails)
   So in Hearthlight nothing is composited over the text at all. The candle
   lights the ROOM instead: the glow and vignette paint the pane around the
   page, the page itself stays the brightest, cleanest thing on screen, and its
   edges fall to shadow via the paper shadow. That is also the truer reading of
   "the page is lit by it".

     text tier     surface        before    after a_max
     --ink         --bg-raised    11.21      11.21   (page: never composited)
     --ink-soft    --bg-raised     7.28       7.28
     --ink-faint   --bg-raised     4.65       4.65

   Worst case Hearthlight = 4.65:1  >= 4.5:1 (the untouched token baseline).

   The glow's only surface is --bg, the empty gutter around the page, where the
   sole text is #whisper / the title / the footer — all of which paint ABOVE the
   layer (--z-atmo-chrome) and are therefore untinted. Text must never be read
   through that tinted gutter: if the page went transparent there (as plain
   focus mode would make it), --ink-faint on --bg would fall to 4.02:1 at
   a_max. The rule "#editor-pane.atmo-candle .prose keeps --bg-raised" below is
   what guarantees it cannot happen; do not remove it.

   Caps are therefore 0.24 (Daylight, multiply) and 0.18 (Hearthlight, over
   --bg only). Raising either one re-opens the audit.

   Nothing else in this file composites over text, so the numbers above are the
   complete set. (Round 4 note: the Writing Desk that used to be audited here
   was retired with spec S12; the realm veil's own derivation lives in the
   header of css/realm.css.)
   ========================================================================== */

/* ==========================================================================
   SCENE PALETTE + Z-SCALE
   --scene-dark is the one per-theme anchor: in Daylight the darkest token is
   the ink-brown, in Hearthlight it is the sunken indigo. Every derived depth in
   this file (the page shadow, the vignette) is mixed against it, so both themes
   get the same warm falloff without a single parallel colour value.
   ========================================================================== */
:root {
  --scene-dark: var(--ink);

  /* the one warm source. The two alpha ramps are declared ON #light-layer,
     not here: var() inside a custom property is substituted at computed-value
     time for the element that declares it, so a ramp declared on :root would
     freeze --flame at the root's value and never see the engine's updates. */
  --flame-rgb: 214 150 66;

  /* z-scale extension. app.css documents --z-base:0 … --z-toast:1000; the four
     atmosphere layers live inside #editor-pane's own stacking context, so they
     never interact with the global scale. */
  --z-atmo-light:   1;
  --z-atmo-ink:     2;
  --z-atmo-caret:   3;
  --z-atmo-chrome:  4;   /* editor head / toolbar / backlinks / footer / whisper */
  --z-atmo-empty:   5;   /* the no-note empty state outranks the atmosphere */

}

html[data-theme="hearthlight"] {
  /* --ink is warm cream here, so mixing toward it would LIGHTEN every derived
     depth. Anchor on the sunken indigo instead and both themes stay in family. */
  --scene-dark: var(--bg-sunken);
}

/* ==========================================================================
   LAYER GEOMETRY
   The four layers are empty, pointer-events:none siblings of .editor__scroll.
   #editor-pane is isolated so the Daylight multiply blend can never reach the
   app background, and the pane's own chrome is lifted above the layers so the
   tint only ever falls on the writing surface.
   ========================================================================== */
#editor-pane { isolation: isolate; }

#light-layer,
#ink-layer,
#caret-layer,
#mote-layer {
  position: absolute;
  inset: 0;
  pointer-events: none;
  overflow: hidden;
  border-radius: inherit;
}
#light-layer  { z-index: var(--z-atmo-light); }
#ink-layer    { z-index: var(--z-atmo-ink); }
#mote-layer   { z-index: var(--z-atmo-ink); }
#caret-layer  { z-index: var(--z-atmo-caret); }

/* the editor's own furniture sits above the atmosphere, so its text is never
   composited with the tint (see the audit: these tiers keep their base ratios) */
#editor-pane > .editor__head,
#editor-pane > #toolbar,
#editor-pane > #backlinks,
#editor-pane > #editor-footer {
  position: relative;
  z-index: var(--z-atmo-chrome);
}
/* #whisper is no longer in this list because it is no longer a child of the
   pane: it lives INSIDE #editor-footer (see §7), which is positioned and
   z-indexed here and therefore forms the stacking context the whisper paints
   in. The proverb is still above every atmosphere layer, untinted. */
#editor-pane > #editor-empty { z-index: var(--z-atmo-empty); }

/* ==========================================================================
   1. THE LIGHT  (#light-layer)
   A single radial source at --light-x / --light-y, which js/atmosphere.js
   writes on #editor-pane (px values) along with --flame (0..1). Defaults here
   keep the layer correct before the engine ever runs.
   ========================================================================== */
#light-layer {
  /* --light-x / --light-y are written by js/atmosphere.js on #editor-pane
     (the pane that contains this layer) — declaring defaults HERE would
     shadow the engine's values, since a custom property set on the same
     element the var() lives on always wins over one inherited from a child's
     ancestor. Every use site below supplies its own fallback instead. */
  --tint-day: calc(0.10 + 0.14 * var(--flame, 0.35));      /* cap 0.24 */
  background:
    radial-gradient(46vw 42vw at var(--light-x, 85%) var(--light-y, 12%),
      color-mix(in srgb, var(--flame-tint) calc(var(--tint-day) * 100%), transparent),
      color-mix(in srgb, var(--flame-tint) calc(var(--tint-day) * 45%), transparent) 46%,
      transparent 74%);
  mix-blend-mode: multiply;
  animation: atmo-breathe 8s var(--ease-in-out) infinite;
  transform-origin: var(--light-x, 85%) var(--light-y, 12%);
}

/* the warm edge-shadow that makes the page feel lit rather than printed */
#light-layer::after {
  content: "";
  position: absolute;
  inset: 0;
  background:
    radial-gradient(120% 110% at var(--light-x, 85%) var(--light-y, 12%),
      transparent 38%,
      color-mix(in srgb, var(--scene-dark) 26%, transparent) 100%);
}

/* Hearthlight: the candle lights the room, not the text. The layer drops below
   the in-flow page (negative z inside the isolated pane), trades multiply for
   a plain additive-looking warm wash, and the vignette deepens to indigo. */
html[data-theme="hearthlight"] #light-layer {
  z-index: -1;
  mix-blend-mode: normal;
  --tint-hearth: calc(0.06 + 0.18 * var(--flame, 0.35));   /* cap 0.24 */
  background:
    radial-gradient(52vw 46vw at var(--light-x, 85%) var(--light-y, 12%),
      color-mix(in srgb, var(--flame-tint) calc(var(--tint-hearth) * 100%), transparent),
      color-mix(in srgb, var(--flame-tint) calc(var(--tint-hearth) * 40%), transparent) 44%,
      transparent 76%);
}
html[data-theme="hearthlight"] #light-layer::after {
  background:
    radial-gradient(118% 104% at var(--light-x, 85%) var(--light-y, 12%),
      transparent 30%,
      color-mix(in srgb, var(--bg-sunken) 35%, transparent) 100%);
}

/* ---------- segment tint (spec §4): the light model leans warmer/cooler
   with the time of day. --flame-tint replaces the plain flame-rgb swatch in
   both gradients above; the alpha math (--tint-day / --tint-hearth, capped
   at 0.24 exactly as audited) is untouched — only the hue shifts, via
   color-mix off existing tag tokens, same pattern as the scene mixes above.

   KEPT DELIBERATELY, though inert today: nothing currently writes data-segment
   on #editor-pane (the Writing Desk that did was retired in round 4, spec §12),
   so these four rules colour nothing. They are the correct rules for the overlay
   light model if the segment is ever surfaced on the pane again, they cost one
   custom property, and they cannot misfire (no segment attribute, no effect).
   The hour of day is VISIBLE in round 4 as the realm time veil (css/realm.css). */
#editor-pane { --flame-tint: rgb(var(--flame-rgb)); }   /* day: unchanged amber */
#editor-pane[data-segment="dawn"] {
  --flame-tint: color-mix(in srgb, rgb(var(--flame-rgb)) 76%, var(--tag-rust) 24%);
}
#editor-pane[data-segment="dusk"] {
  --flame-tint: color-mix(in srgb, rgb(var(--flame-rgb)) 70%, var(--tag-violet) 30%);
}
#editor-pane[data-segment="night"] {
  --flame-tint: color-mix(in srgb, rgb(var(--flame-rgb)) 88%, var(--tag-rust) 12%);
}
/* night = candle-dominant: stays warmer (above) and, in Hearthlight only —
   where the glow paints the room, not the text, so the contrast audit above
   does not bound it — the vignette deepens slightly (26%->30% coverage,
   35%->40% alpha). Daylight's audited vignette is left exactly as measured. */
html[data-theme="hearthlight"] #editor-pane[data-segment="night"] #light-layer::after {
  background:
    radial-gradient(118% 104% at var(--light-x, 85%) var(--light-y, 12%),
      transparent 30%,
      color-mix(in srgb, var(--bg-sunken) 40%, transparent) 100%);
}

/* While the candle is lit the page KEEPS AN OPAQUE SURFACE, even in focus mode
   (app.css makes .prose transparent there). This is what makes the audit above
   complete: the only thing the Daylight tint is ever composited over is the
   page, and in Hearthlight nothing at all. Text never reads through the tinted
   pane background — where it would, --ink-faint on --bg falls to 4.02:1. */
#editor-pane.atmo-candle:not(.atmo-off) .prose { background: var(--bg-raised); }

/* 8s breath — transform + opacity only, and tiny on purpose. At 1.6% scale the
   eye reads it as the flame settling, never as the page moving. */
@keyframes atmo-breathe {
  0%, 100% { transform: scale(1);     opacity: 0.94; }
  50%      { transform: scale(1.016); opacity: 1; }
}

/* ==========================================================================
   2. INK BLOOM  (.bloom in #ink-layer)
   One halo per completed word: spreads to 1.15x, blurs, and is gone in 700ms.
   The engine positions the box from a Range rect and removes it on animationend.
   ========================================================================== */
.bloom {
  position: absolute;
  pointer-events: none;
  will-change: transform, opacity, filter;
  animation: ink-bloom 700ms var(--ease-out) both;
}
.bloom::before {
  content: "";
  position: absolute;
  inset: -0.35em -0.5em;
  border-radius: 999px;
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--ink) 22%, transparent),
    transparent 76%);
}
html[data-theme="hearthlight"] .bloom::before {
  background: radial-gradient(closest-side,
    color-mix(in srgb, var(--accent) 30%, transparent),
    transparent 78%);
}
@keyframes ink-bloom {
  from { transform: scale(0.92); filter: blur(0);   opacity: 0.7; }
  to   { transform: scale(1.15); filter: blur(3px); opacity: 0; }
}

/* ---------- milestone firefly: lifts from the caret and leaves the page ----- */
.firefly-rise {
  position: absolute;
  width: 5px;
  height: 5px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--accent-soft), transparent 72%);
  box-shadow: 0 0 10px 2px var(--accent-glow);
  pointer-events: none;
  animation: firefly-rise 2.6s var(--ease-in-out) both;
}
@keyframes firefly-rise {
  0%   { transform: translate(0, 0) scale(0.7);          opacity: 0; }
  12%  { opacity: 0.95; }
  40%  { transform: translate(14px, -52px) scale(1.08); }
  70%  { transform: translate(-8px, -100px) scale(1);    opacity: 0.8; }
  100% { transform: translate(18px, -140px) scale(0.72); opacity: 0; }
}

/* ---------- streak constellation: N stars near the note title, ~3s --------- */
.constellation {
  position: absolute;
  top: 0;
  right: var(--space-5);
  width: 180px;
  height: 64px;
  pointer-events: none;
  color: var(--accent-soft);
  animation: constellation-in 3s var(--ease-soft) both;
}
.constellation .star {
  fill: currentColor;
  opacity: 0;
  animation: star-twinkle 1.6s var(--ease-in-out) infinite;
}
.constellation .star:nth-child(2)  { animation-delay: 140ms; }
.constellation .star:nth-child(3)  { animation-delay: 280ms; }
.constellation .star:nth-child(4)  { animation-delay: 420ms; }
.constellation .star:nth-child(5)  { animation-delay: 560ms; }
.constellation .star:nth-child(6)  { animation-delay: 700ms; }
.constellation .star:nth-child(7)  { animation-delay: 840ms; }
.constellation .star:nth-child(8)  { animation-delay: 980ms; }
.constellation .star:nth-child(9)  { animation-delay: 1120ms; }
.constellation .star:nth-child(10) { animation-delay: 1260ms; }
.constellation .star:nth-child(11) { animation-delay: 1400ms; }
.constellation .star:nth-child(12) { animation-delay: 1540ms; }
@keyframes star-twinkle {
  0%, 100% { opacity: 0.25; transform: scale(0.85); }
  50%      { opacity: 1;    transform: scale(1.1); }
}
@keyframes constellation-in {
  0%, 100% { opacity: 0; }
  12%, 76% { opacity: 1; }
}

/* ==========================================================================
   3. THE QUILL NIB  (#caret-layer .quill-caret)
   A 2px nib the height of one line, lit from the same amber. Pulses only while
   idle; dead steady while writing, because a blinking caret under the hand is
   noise. The native caret is hidden ONLY while the engine is tracking
   successfully (it removes .has-quill-caret the moment measurement fails).
   ========================================================================== */
#caret-layer .quill-caret {
  position: absolute;
  width: 2px;
  height: 1.2em;
  border-radius: 1px;
  background: linear-gradient(to bottom,
    var(--accent), color-mix(in srgb, var(--accent) 62%, transparent));
  box-shadow: 0 0 6px 1px var(--accent-glow);
  will-change: transform;
}
#caret-layer .quill-caret.is-idle { animation: caret-pulse 1.6s var(--ease-in-out) infinite; }
#caret-layer .quill-caret.is-writing { animation: none; opacity: 1; }
@keyframes caret-pulse {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.38; }
}
#editor-pane.has-quill-caret #note-editor { caret-color: transparent; }

/* ==========================================================================
   4. THE ILLUMINATED INITIAL
   Pure CSS, so it costs nothing and prints. Only ever the first letter of the
   first paragraph-ish block: the selector cannot reach a heading, list or
   blockquote, which is exactly the manuscript rule.
   ========================================================================== */
#editor-pane.atmo-initials:not(.atmo-off) .prose > :is(p, div):first-child::first-letter {
  float: left;
  margin: 0.02em 0.08em 0 0;
  padding-right: 0.04em;
  font-family: var(--font-display);
  font-weight: 600;
  font-size: 3.2em;          /* ~3 lines at the prose line-height */
  line-height: 0.84;
  color: var(--accent);
  text-shadow:
    0 0 1px color-mix(in srgb, var(--accent-soft) 70%, transparent),
    0 0 14px var(--accent-glow);
}

/* hairline-and-diamond ornament under h1 / h2 — the gap in the rule is cut by
   the gradient itself, so the glyph sits in clear air */
#editor-pane.atmo-paper:not(.atmo-off) .prose h1::after,
#editor-pane.atmo-paper:not(.atmo-off) .prose h2::after {
  content: "\2756";   /* ❖ — the same ornament .prose hr uses */
  display: block;
  width: min(100%, var(--measure));
  margin: 0.46em 0 0;
  font-family: var(--font-body);
  font-size: 0.34em;
  font-weight: 400;
  line-height: 1;
  text-align: center;
  color: var(--accent);
  background-image: linear-gradient(to right,
    transparent 0,
    var(--line) 6%,
    var(--line) 45%,
    transparent 46%,
    transparent 54%,
    var(--line) 55%,
    var(--line) 94%,
    transparent 100%);
  background-size: 100% 1px;
  background-position: 0 50%;
  background-repeat: no-repeat;
}

/* ---------- top-of-page ornament in hush (spec §4) -------------------------
   Same hairline-and-diamond technique as the h1/h2 ornament above, once at
   the top of the page instead of after every heading. Not an atmosphere
   effect (no atmo-* gate) — it is page furniture, like the ornament itself
   already is above. --ink-faint keeps it the quietest mark on the page.
   (Round 4: focus mode became hush; in world mode there is no page to ornament,
   so css/realm.css gives .prose no surface and this mark simply never shows.) */
.app.focus-mode .prose::before {
  content: "\2756";
  display: block;
  width: min(180px, 40%);
  margin: 0 auto var(--space-4);
  font-family: var(--font-body);
  font-size: 0.6rem;
  font-weight: 400;
  line-height: 1;
  text-align: center;
  color: var(--ink-faint);
  background-image: linear-gradient(to right,
    transparent 0,
    var(--line) 6%,
    var(--line) 45%,
    transparent 46%,
    transparent 54%,
    var(--line) 55%,
    var(--line) 94%,
    transparent 100%);
  background-size: 100% 1px;
  background-position: 0 50%;
  background-repeat: no-repeat;
}

/* ==========================================================================
   5. PAPER — page lift, inner vignette, deckled edge
   ========================================================================== */
#editor-pane.atmo-paper:not(.atmo-off) .prose {
  box-shadow:
    0 1px 1px color-mix(in srgb, var(--scene-dark) 8%, transparent),
    0 6px 16px color-mix(in srgb, var(--scene-dark) 9%, transparent),
    0 22px 48px color-mix(in srgb, var(--scene-dark) 10%, transparent),
    inset 0 0 34px color-mix(in srgb, var(--scene-dark) 5%, transparent),
    inset 0 1px 0 color-mix(in srgb, var(--bg-raised) 80%, transparent);
}
/* REVIEW FIX (Important 2) — DELETED, deliberately.
   There used to be a rule here that zeroed the page shadow in focus mode when
   Candlelight was off:
     .app.focus-mode #editor-pane.atmo-paper:not(.atmo-off):not(.atmo-candle)
       .prose { box-shadow: none }
   Its premise ("the surface under the page may be transparent, so the shadow
   would smear") is never true: #editor-pane.atmo-candle .prose keeps an opaque
   --bg-raised surface in every mode that has a page at all. What the rule did
   instead was win on specificity and silently delete the page's shadow for
   anyone who turned Candlelight off, leaving it pasted flat. */
html[data-theme="hearthlight"] #editor-pane.atmo-paper:not(.atmo-off) .prose {
  /* in the dark theme the page is the lit object: a warm halo sells the candle
     without putting a single pixel of tint over the text */
  box-shadow:
    0 1px 2px color-mix(in srgb, var(--bg-sunken) 52%, transparent),
    0 10px 26px color-mix(in srgb, var(--bg-sunken) 58%, transparent),
    0 28px 64px color-mix(in srgb, var(--bg-sunken) 62%, transparent),
    0 0 42px var(--accent-glow),
    inset 0 0 40px color-mix(in srgb, var(--bg-sunken) 30%, transparent);
}

/* ==========================================================================
   6. DUST MOTES  (#mote-layer .mote)
   CSS-only. Six in Daylight, ten in Hearthlight. Every drift is biased up and
   to the right, toward the light.
   ========================================================================== */
.mote {
  position: absolute;
  width: 4px;
  height: 4px;
  border-radius: 50%;
  background: radial-gradient(circle, var(--accent-soft), transparent 70%);
  opacity: 0;
  animation: mote-drift 24s linear infinite;
}
.mote:nth-child(n + 7) { display: none; }          /* 6 in Daylight */
html[data-theme="hearthlight"] .mote:nth-child(n + 7) { display: block; }

.mote:nth-child(1)  { left: 12%; top: 78%; width: 3px; height: 3px; animation-duration: 26s; animation-delay:  -2s; }
.mote:nth-child(2)  { left: 28%; top: 64%;                          animation-duration: 21s; animation-delay:  -9s; }
.mote:nth-child(3)  { left: 44%; top: 86%; width: 3px; height: 3px; animation-duration: 29s; animation-delay:  -4s; }
.mote:nth-child(4)  { left: 58%; top: 52%;                          animation-duration: 19s; animation-delay: -14s; }
.mote:nth-child(5)  { left: 70%; top: 74%; width: 3px; height: 3px; animation-duration: 30s; animation-delay:  -7s; }
.mote:nth-child(6)  { left: 84%; top: 60%;                          animation-duration: 23s; animation-delay: -18s; }
.mote:nth-child(7)  { left: 20%; top: 44%; width: 3px; height: 3px; animation-duration: 27s; animation-delay: -11s; }
.mote:nth-child(8)  { left: 36%; top: 30%;                          animation-duration: 18s; animation-delay:  -1s; }
.mote:nth-child(9)  { left: 64%; top: 36%; width: 3px; height: 3px; animation-duration: 25s; animation-delay: -16s; }
.mote:nth-child(10) { left: 90%; top: 28%;                          animation-duration: 22s; animation-delay:  -6s; }

@keyframes mote-drift {
  0%   { opacity: 0;    transform: translate3d(0, 0, 0) scale(0.8); }
  12%  { opacity: 0.55; }
  50%  { transform: translate3d(34px, -108px, 0) scale(1.05); }
  82%  { opacity: 0.42; }
  100% { opacity: 0;    transform: translate3d(64px, -208px, 0) scale(0.78); }
}

/* ==========================================================================
   7. THE WHISPER
   A proverb at the foot of the page after a long pause. Italic display serif,
   faintest text tier, centred. Line-height and the bottom pad give the italic
   descenders (g, y, p) room — a clipped descender would undo the whole effect.

   REVIEW FIX (Important 1) — IT MUST NOT REFLOW THE WRITING VIEWPORT.
   #editor-pane is a flex column (head / toolbar / .editor__scroll / backlinks /
   footer). As an in-flow sibling between #backlinks and the footer, the whisper
   added ~60px to that column every time it appeared and took it away again on
   the next keystroke — so .editor__scroll resized, and the page jumped under
   the writer's hands at the exact moment they started typing again.
   So it lives INSIDE #editor-footer (app.html) and is absolutely positioned
   just above it: out of flow, zero layout cost, same visual place. The footer
   gets position:relative for it (it already had it, from the chrome z-index
   rule in LAYER GEOMETRY above — restated here so the dependency is explicit).
   Ids and classes are unchanged, so js/atmosphere.js needs no change at all.
   ========================================================================== */
#editor-footer { position: relative; }
.whisper {
  position: absolute;
  left: 0;
  right: 0;
  bottom: 100%;            /* sits on top of the footer, not in the column */
  pointer-events: none;    /* it is atmosphere: never a click target */
  padding: var(--space-2) var(--space-5) var(--space-3);
  text-align: center;
  opacity: 0;
  transition: opacity 900ms var(--ease-soft);
}
.whisper[hidden] { display: none; }
.whisper.is-visible { opacity: 1; }
.whisper__text {
  margin: 0;
  font-family: var(--font-display);
  font-style: italic;
  font-size: 1.06rem;
  line-height: 1.5;
  color: var(--ink-faint);
  text-wrap: balance;
}
.whisper__source {
  margin: 2px 0 0;
  font-family: var(--font-ui);
  font-size: 0.72rem;
  letter-spacing: 0.06em;
  text-transform: lowercase;
  color: var(--ink-faint);
  opacity: 0.8;
}

/* ==========================================================================
   8. THE ATMOSPHERE POPOVER
   Reuses the .popover / .segmented shells from app.css; only what is new to
   this panel lives here.
   ========================================================================== */
.popover--atmo {
  width: 304px;
  max-height: calc(100dvh - var(--chrome-h) - var(--space-5));
  overflow-y: auto;
  overscroll-behavior: contain;
}
.popover--atmo .popover__arrow { right: 208px; }   /* points at #btn-atmosphere */

/* ---------- master switch: a checkbox wearing a toggle ---------- */
.atmo-master {
  display: flex;
  align-items: center;
  gap: var(--space-3);
  margin: 0 0 var(--space-3);
  padding-bottom: var(--space-3);
  border-bottom: 1px solid var(--line-soft);
  font-family: var(--font-ui);
  font-size: 0.92rem;
  color: var(--ink);
  cursor: pointer;
}
.atmo-switch {
  position: absolute;
  width: 1px;
  height: 1px;
  opacity: 0;
  pointer-events: none;
}
.atmo-switch__track {
  position: relative;
  flex: none;
  width: 38px;
  height: 22px;
  border-radius: 999px;
  background: var(--bg-sunken);
  box-shadow: inset 0 0 0 1px var(--line);
  transition: background-color var(--dur-quick) var(--ease-out),
              box-shadow var(--dur-quick) var(--ease-out);
}
.atmo-switch__track::after {
  content: "";
  position: absolute;
  top: 3px;
  left: 3px;
  width: 16px;
  height: 16px;
  border-radius: 50%;
  background: var(--bg-raised);
  box-shadow: var(--shadow-soft);
  transition: transform var(--dur-quick) var(--ease-out),
              background-color var(--dur-quick) var(--ease-out);
}
.atmo-switch:checked + .atmo-switch__track {
  background: color-mix(in srgb, var(--accent) 34%, var(--bg-sunken));
  box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--accent) 46%, var(--line));
}
.atmo-switch:checked + .atmo-switch__track::after {
  transform: translateX(16px);
  background: var(--accent-soft);
}
.atmo-switch:focus-visible + .atmo-switch__track {
  outline: 2px solid var(--accent);
  outline-offset: 2px;
}
/* the label says what the switch will mean: "live" while on, "quiet" while off.
   display:none keeps the inactive half out of the accessible name too. */
.atmo-master__off { display: none; }
.atmo-switch:not(:checked) ~ .atmo-master__on  { display: none; }
.atmo-switch:not(:checked) ~ .atmo-master__off { display: inline; color: var(--ink-soft); }

/* ---------- one row per effect ---------- */
.atmo-list {
  margin: 0 0 var(--space-4);
  padding: 0;
  list-style: none;
  display: grid;
  gap: 2px;
}
.atmo-row {
  display: flex;
  align-items: flex-start;
  gap: var(--space-3);
  padding: var(--space-2) var(--space-2) var(--space-2) 0;
  border-radius: var(--radius-sm);
  cursor: pointer;
  transition: background-color var(--dur-quick) var(--ease-out);
}
@media (hover: hover) and (pointer: fine) {
  .atmo-row:hover { background: color-mix(in srgb, var(--accent) 7%, transparent); }
}
.atmo-check {
  flex: none;
  appearance: none;
  -webkit-appearance: none;
  width: 16px;
  height: 16px;
  margin: 2px 0 0 var(--space-2);
  border: 1.5px solid var(--ink-faint);
  border-radius: 4px;
  background: transparent;
  cursor: pointer;
  transition: background-color var(--dur-quick) var(--ease-out),
              border-color var(--dur-quick) var(--ease-out);
}
.atmo-check:checked {
  background: var(--accent);
  border-color: var(--accent);
}
.atmo-check:checked::after {
  content: "";
  display: block;
  width: 8px;
  height: 4px;
  margin: 3px auto 0;
  border-left: 1.8px solid var(--bg-raised);
  border-bottom: 1.8px solid var(--bg-raised);
  transform: rotate(-45deg);
}
.atmo-row__text { display: grid; gap: 1px; min-width: 0; }
.atmo-row__name {
  font-family: var(--font-ui);
  font-size: 0.86rem;
  font-weight: 500;
  color: var(--ink);
}
.atmo-row__desc {
  font-family: var(--font-body);
  font-size: 0.8rem;
  line-height: 1.4;
  color: var(--ink-faint);
}
.atmo-row:has(.atmo-check:not(:checked)) .atmo-row__name { color: var(--ink-soft); }

/* The five-up "Desk window" segmented control and its .atmo-group label were
   retired with the Writing Desk (round-4 spec §12). The hour of day is the
   world's own now; it is not a setting. */

/* when the master switch is off the per-effect rows read as inactive */
.popover--atmo:has(.atmo-switch:not(:checked)) .atmo-list {
  opacity: 0.52;
}

/* ==========================================================================
   10. GATING
   Per-effect classes on #editor-pane (written by the engine from
   settings.atmosphere). The containers stay in the DOM and stay empty; only
   the visible parts are gated, so a toggle can never leave a stray node lit.
   ========================================================================== */
#editor-pane:not(.atmo-candle) #light-layer { display: none; }
#editor-pane:not(.atmo-motes)  #mote-layer  { display: none; }
#editor-pane:not(.atmo-ink)    .bloom { display: none; }
#editor-pane:not(.atmo-caret)  .quill-caret { display: none; }
#editor-pane:not(.atmo-milestones) .firefly-rise,
#editor-pane:not(.atmo-milestones) .constellation { display: none; }
#editor-pane:not(.atmo-whisper) #whisper { display: none; }

/* master off: the page goes quiet. Paper, the drop cap and the candle
   background also carry their own `:not(.atmo-off)` (self-sufficient even if
   a per-effect class is ever left on by mistake), so the containers hidden
   here are the only ones that still depend solely on the engine dropping the
   per-effect class. */
#editor-pane.atmo-off #light-layer,
#editor-pane.atmo-off #ink-layer,
#editor-pane.atmo-off #caret-layer,
#editor-pane.atmo-off #mote-layer,
#editor-pane.atmo-off #whisper { display: none !important; }
#editor-pane.atmo-off #note-editor { caret-color: auto; }

/* ==========================================================================
   11. REDUCED MOTION
   Not "fewer pixels" — "no movement". The warm tint, the paper, the drop cap
   and the ornament all stay, because they are static and they are the part
   that carries the mood. Everything that moves stops or goes away.
   ========================================================================== */
@media (prefers-reduced-motion: reduce) {
  #light-layer {
    animation: none;
    transform: none;
    opacity: 1;
  }
  .bloom,
  .firefly-rise,
  .constellation { display: none; }
  /* .mote needs !important: html[data-theme="hearthlight"] .mote:nth-child(n+7)
     carries specificity (0,3,1) (type + attribute + class + pseudo-class) —
     higher than a plain `.mote { display: none }` at (0,1,0) could ever beat,
     media query or no. !important is the only thing that reliably wins here
     regardless of theme or nth-child count. */
  .mote {
    display: none !important;
    animation: none !important;
  }

  #caret-layer .quill-caret { animation: none; opacity: 1; }

  .whisper { transition: none; }
  .atmo-switch__track,
  .atmo-switch__track::after,
  .atmo-row,
  .atmo-check {
    transition: none;
  }
}
