/* ── Cloud of Ideas ─────────────────────────────────────────── */

:root {
  --sky-top:   #00CFFF;
  --sky-mid:   #D4AACC;
  --sky-bot:   #FFF9F0;
  --sky-glow:  #ffffff;
  --sky-glow-opacity: 1;
  --sky-stars-opacity: 0;
  --sky-text:  #3a4060;
  --sky-soft:  rgba(58, 64, 96, 0.62);
  --sky-warm:  #fde0d2;
  --sky-cool:  #d5e8f5;
  --cloud-shadow: 0 14px 28px -10px rgba(70, 80, 130, 0.18);
}

/* The .sky wrapper now hosts MorningSkyBackground (which draws the
   gradient + ambient clouds + horizon haze). No background of its own. */
.sky {
  position: fixed;
  inset: 0;
  overflow: hidden;
  isolation: isolate;
  /* Fixed, non-scrolling canvas — nothing here needs native touch panning
     or pinch-zoom, and leaving it default lets the browser's own horizontal
     swipe/back gesture race the page-nav swipe handler on .sky (App). */
  touch-action: none;
}

/* Warm sunrise haze — peach-gold glow blooms up from the horizon,
   keeping the bottom area warm and legible without darkening the sky.
   Fades to 0 for 22:00-03:00 (--sky-glow-opacity, set on .sky itself —
   see the effect in App keyed on the visible facet's page/previewHour,
   since this single shared overlay sits outside .pages and can't inherit
   either facet's own instance-scoped copy of the same variable name) —
   the button now has its own pill background/border and doesn't need
   this for legibility, and it fought the genuinely-dark night sky. */
.sky::after {
  content: '';
  position: absolute;
  inset: 0;
  z-index: 4;
  pointer-events: none;
  opacity: var(--sky-glow-opacity, 1);
  background: radial-gradient(ellipse 120% 60% at 50% 110%,
    rgba(255, 200, 140, 0.45) 0%,
    rgba(255, 220, 180, 0.20) 45%,
    transparent 70%);
}

/* ── exhibition display mode (A2) ─────────────────────────────
   ?display=1 adds .display-mode to .sky (see App's render). Hides every
   interactive affordance — visitors on the projector/kiosk aren't meant to
   tap anything; they submit from their own phone via a QR code pointing at
   the normal URL. Purely a CSS layer on top of the same markup, so nothing
   about the sky/field/arrival logic needs to know display mode exists. */
.display-mode .add-btn,
.display-mode .page-arrow,
.display-mode .music-toggle-wrap,
.display-mode .count,
.display-mode .legend {
  display: none !important;
}

/* ── ambient light motes (B4) ─────────────────────────────────
   z-index 2: above MorningSkyBackground (rendered at z-index 0 as a whole)
   and below .pages (z-index 3, the real note clouds) — "between the far
   cloud plane and the note clouds" per the design. Nested wrapper (.mote
   drifts, .mote-inner bobs) so the two transforms compose instead of one
   overwriting the other, same pattern as .cloud-inner/.cloud-breath. */
.motes-layer {
  position: absolute;
  inset: 0;
  z-index: 2;
  pointer-events: none;
  overflow: hidden;
}

.mote {
  position: absolute;
  animation: mote-drift var(--mote-dur, 60s) ease-in-out var(--mote-delay, 0s) infinite alternate;
  will-change: transform;
}

.mote-inner {
  border-radius: 50%;
  background: radial-gradient(circle, rgba(255,255,255,0.95) 0%, rgba(255,255,255,0) 72%);
  animation: mote-bob var(--bob-dur, 12s) ease-in-out var(--bob-delay, 0s) infinite alternate;
  will-change: transform;
}

@keyframes mote-drift {
  from { transform: translateX(0); }
  to   { transform: translateX(var(--mote-dist, 8vw)); }
}

@keyframes mote-bob {
  from { transform: translateY(-6px); }
  to   { transform: translateY(6px); }
}

/* ── brand heading (SEO / screen readers only) ─────────────── */

/* The sky intentionally has no visible title; this standard clip pattern
   keeps the h1 in the document for search engines and screen readers. */
.sr-only {
  position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px;
  overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0;
}

/* ── two sliding pages (one question type per facet) ───────── */

.pages {
  position: absolute;
  inset: 0;
  z-index: 3;
  display: flex;
  width: 200%;
  height: 100%;
  transition: transform 700ms cubic-bezier(0.22, 0.61, 0.36, 1);
}

.pages[data-page="learn"] {
  transform: translateX(-50%);
}

.page {
  position: relative;
  width: 50%;
  height: 100%;
}

/* The hidden facet freezes exactly where it was — resumes from the same
   spot when you slide back, instead of drifting on unseen. */
.page.offstage .cloud {
  animation-play-state: paused;
}

.page.offstage .ms-cloud {
  animation-play-state: paused;
}

.page.offstage .ms-star {
  animation-play-state: paused;
}

/* Clouds can be wider than their own page (up to 136vw on mobile) and
   routinely overhang past its edge as part of normal drift — harmless
   with one page, since past-the-edge used to mean nothing was there. With
   two pages sitting edge to edge, that overhang lands inside the OTHER
   page's space the moment this one slides offstage, bleeding a frozen
   cloud into view no matter how big a gutter you add between them.
   `visibility: hidden` suppresses painting outright regardless of any
   cloud's position/width, while still preserving .page's box so the
   200%-wide flex layout and its transform math are untouched. */
.page.offstage {
  visibility: hidden;
}

/* ── cloud field ───────────────────────────────────────────── */

.field {
  position: absolute;
  inset: 0;
  /* user-select:none on .cloud alone (below) stops a drag that STARTS on a
     cloud from selecting its own text, but browsers can still visually
     paint the blue drag-highlight across .cloud's text when the drag's
     anchor is outside it — an empty sky gap, anywhere between clouds — and
     merely sweeps through on the way to somewhere else. That's a rendering
     quirk in the selection gesture itself, not something a leaf element's
     own user-select can veto after the fact. Marking the whole canvas
     (clouds and the gaps between them) non-selectable stops the drag from
     ever engaging as a text-selection here at all. Composer/NoteView are
     siblings of .pages, not descendants of .field, so their text is
     unaffected.
     -webkit-user-select: none is required alongside the unprefixed
     property for Safari/WebKit specifically — the unprefixed property
     alone still lets Safari paint the blue drag-selection rectangle
     across clouds even though it correctly blocks the actual text
     selection; Chrome doesn't need the prefix for this. */
  user-select: none;
  -webkit-user-select: none;
}

.cloud {
  position: absolute;
  top: var(--lane);
  left: 100%;
  width: var(--w, 260px);
  cursor: pointer;
  touch-action: none;
  /* Kept alongside .field's now-broader rule above: this is what actually
     stops a drag that starts ON a cloud from selecting its own text (the
     .field rule above targets a drag that starts *outside* one instead).
     Inherits down to .cloud-content and everything in it; .cloud-img
     already had its own user-select:none for the same reason before this
     covered the whole tree. -webkit-user-select: none needed alongside
     for Safari — see the comment on .field above. */
  user-select: none;
  -webkit-user-select: none;
  animation: drift var(--dur, 160s) linear var(--delay, 0s) infinite,
             cloud-fadein 0.5s ease-out forwards;
  will-change: transform, left;
  z-index: 1;
  transform-origin: center 25%;
  /* The bounding box is much bigger than the puffy cloud silhouette inside
     it, so overlapping clouds' boxes routinely cover each other's visible
     body — whichever has the higher z-index (assigned by width, not by
     what's actually on top) wins hover/pointer hit-testing there, so the
     cloud the user is actually looking at can fail to pause. Checked all
     11 cloud PNGs' alpha channels: every one is fully transparent in a
     16%-square top-left/top-right corner, but several have opaque pixels
     reaching into the bottom corners — so only the top corners are safe
     to trim from the hit area without cutting into visible cloud pixels.
     The top edge and both top-corner diagonals are untouched from the
     original box-fitted shape, so that trim is pixel-identical to before.
     (The other three sides were briefly pushed 20% past the box to clear
     the since-removed tint picker's drop-shadow glow bleed on .cloud-img —
     no longer needed now that clouds carry no glow, so this is back to a
     plain box-fitted polygon.) */
  clip-path: polygon(16% 0%, 84% 0%, 100% 16%, 100% 100%, 0% 100%, 0% 16%);
}

.cloud:hover,
.cloud.is-paused {
  animation-play-state: paused;
  z-index: 9;
}

.cloud.dragging {
  cursor: grabbing;
  z-index: 99;
  animation-play-state: paused;
}

.cloud.poofing {
  animation: poof 600ms cubic-bezier(0.4, 0, 0.2, 1) forwards !important;
  pointer-events: none;
}

@keyframes poof {
  0%   { transform: scale(1.0) rotate(0deg);  opacity: 1;   }
  25%  { transform: scale(1.2) rotate(-6deg); opacity: 1;   }
  60%  { transform: scale(1.8) rotate(12deg); opacity: 0.5; }
  100% { transform: scale(2.5) rotate(18deg); opacity: 0;   }
}

/* "Snooker ball" pointer-enter nudge (#118) — its own wrapper, one level
   out from .cloud-inner/.cloud-breath below, for the same reason they're
   split from each other and from .cloud itself: one wrapper, one animated
   transform property, composed through normal DOM nesting rather than
   fighting over a shared transform. --nudge-x/--nudge-y are set from JS
   (onPointerEnter in the Cloud component) to the direction away from
   wherever the pointer touched the cloud; the .nudging class (re-added via
   a forced-reflow restart, same trick as the drift-restart helper) plays
   the animation once per hover-enter. */
.cloud-nudge {
  display: block;
  width: 100%;
}

.cloud-nudge.nudging {
  animation: cloud-nudge 650ms cubic-bezier(0.34, 1.56, 0.64, 1) 1;
}

@keyframes cloud-nudge {
  0%   { transform: translate(0, 0); }
  35%  { transform: translate(var(--nudge-x, 0), var(--nudge-y, 0)); }
  100% { transform: translate(0, 0); }
}

/* Organic bob/breathe motion — two nested wrapper divs, each owning exactly
   one animated transform property, so they compose naturally through normal
   DOM nesting instead of fighting over .cloud's transform (which the drift
   animation, the drag/resize logic, and the press wobble all already own).
   Both wrappers are plain-flow blocks sized to their content (same box as
   .cloud itself), so nesting is transparent to the percentage insets that
   .cloud-content uses below. */
.cloud-inner {
  display: block;
  width: 100%;
  animation: cloud-bob var(--bob-dur, 18s) ease-in-out var(--bob-delay, 0s) infinite alternate;
  will-change: transform;
}

.cloud-breath {
  display: block;
  width: 100%;
  animation: cloud-breath var(--breath-dur, 24s) ease-in-out var(--breath-delay, 0s) infinite alternate;
  will-change: transform;
}

/* Amplitude is a percentage of the cloud's own height, so it stays small and
   proportional at every size — well clear of the 16%-corner clip-path on
   .cloud (see the comment there), which only trims the outer hit area and
   would otherwise crop opaque pixels drifting into it. */
@keyframes cloud-bob {
  from { transform: translateY(-1.6%); }
  to   { transform: translateY(1.6%); }
}

@keyframes cloud-breath {
  from { transform: scale(1); }
  to   { transform: scale(1.012); }
}

.cloud-img {
  display: block;
  width: 100%;
  height: auto;
  pointer-events: none;
  user-select: none;
  -webkit-user-select: none;
}

/* The exact inset is set inline per cloud variant (CLOUD_TEXT_INSETS in
   cloud-of-ideas.jsx, traced from the Figma cloud01–cloud11 frames). The inset
   here is only a fallback. */
.cloud-content {
  position: absolute;
  inset: 46% 20% 10% 20%;
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  text-align: center;
  gap: 4px;
  pointer-events: none;
  color: var(--sky-text);
}

/* Empty-cloud variant: the question label reads better near the top of the
   text box rather than centered as a group with "I want to share" — a real
   note keeps the default centered group (its .answer can run several
   lines, so centering the whole block still reads balanced). */
.cloud-content--deco {
  justify-content: flex-start;
  padding-top: 6%;
  gap: 20px;
}

.cloud-content .q-label {
  font-size: clamp(5px, calc(var(--w, 26vw) * 0.030), 11px);
  font-weight: 700;
  letter-spacing: 0.10em;
  text-transform: uppercase;
  opacity: 0.42;
  line-height: 1.2;
  margin-bottom: 2px;
  flex-shrink: 0;
  /* Single line by default so it never pushes the answer out of the cloud body */
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
}


.cloud-content .answer {
  /* Scale with the cloud itself (--w is the cloud width in vw), so text
     stays proportional to its drawn text box at any cloud size. */
  font-size: clamp(9px, calc(var(--w, 26vw) * 0.050), 22px);
  line-height: 1.32;
  font-weight: 400;
  margin: 0;
  max-width: 96%;
  text-wrap: pretty;
  opacity: 0.78;
  transition: opacity 280ms ease;
  /* clamp the preview to the lines that fit this cloud's drawn text box
     (--clamp comes from CLOUD_TEXT_AREAS in cloud-of-ideas.jsx). flex-shrink 0
     keeps the clamped block at natural height — a couple of px of
     centred spill beats a mid-line cut at the box edge. */
  flex-shrink: 0;
  display: -webkit-box;
  -webkit-line-clamp: var(--clamp, 4);
  -webkit-box-orient: vertical;
  overflow: hidden;
}

.cloud:hover .cloud-content .answer {
  opacity: 1;
}

.cloud-content .meta {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: 8px;
  font-family: 'Caveat', cursive;
  font-size: clamp(10px, calc(var(--w, 26vw) * 0.038), 18px);
  color: var(--sky-text);
  opacity: 0.72;
  margin-top: 4px;
  flex-shrink: 0;
  line-height: 1;
}

.cloud-content .meta .dot {
  width: 3px; height: 3px; border-radius: 50%;
  background: currentColor; opacity: 0.5;
}

/* Faint brand mark sitting behind the "I want to share" invitation inside
   every empty (decorative) cloud. Own layer, not part of .deco-hint's own
   `background` — that property is already spoken for as the animated
   gradient the text-fill clips to. Painted first in the DOM so .deco-hint
   naturally stacks on top with no z-index needed. */
.deco-logo-watermark {
  position: absolute;
  inset: 40% 20% 30% 20%; /* fallback — real value set inline, see .deco-hint below */
  background-image: url('assets/logo.svg');
  background-repeat: no-repeat;
  background-position: center;
  background-size: contain;
  /* Scaled down from the box's full "contain" fit, not a smaller box — a
     transform keeps the mark centered and its aspect ratio intact
     regardless of how each cloud variant's inset is shaped. */
  transform: scale(0.7);
  opacity: 0.07;
  pointer-events: none;
}

/* Standing invitation inside every empty (decorative) cloud — the empty
   slot in the sky asking for your idea, under its facet's own question
   label (.cloud-content .q-label, same markup a real note uses — see the
   Cloud component). Same font as the "I want to share" button, and the
   same animated rainbow — applied as a text fill via background-clip
   instead of the button's stroke — so the colour itself invites the
   click. Always visible (softened to 80% opacity) rather than hover-only,
   since touch devices have no hover to reveal it. A plain flex child of
   .cloud-content now (not independently positioned) — that parent already
   places and centers it the same way it places a real note's .answer. */
.deco-hint {
  text-align: center;
  font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
  font-weight: 400;
  font-size: clamp(13px, calc(var(--w, 26vw) * 0.045), 20px);
  background: linear-gradient(270deg, #f43f5e, #c026d3, #7c3aed, #2563eb, #0891b2, #f43f5e);
  background-size: 400% 400%;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
  color: transparent;
  animation: btn-rainbow 2.4s ease infinite;
  opacity: 0.8;
  pointer-events: none;
}

@keyframes drift {
  from { left: 110%; }
  to   { left: calc(-1 * var(--exit-margin, 32%)); }
}

/* Direction is a per-lane property (see laneDirection in cloud-of-ideas.jsx),
   hashed independently of which question the lane belongs to — any lane on
   any page can end up drifting either way. */
.cloud.ltr {
  animation: drift-ltr var(--dur, 160s) linear var(--delay, 0s) infinite,
             cloud-fadein 0.5s ease-out forwards;
}

@keyframes drift-ltr {
  from { left: calc(-1 * var(--exit-margin, 32%)); }
  to   { left: 110%; }
}

@keyframes cloud-fadein {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* ── resonance (B3): the sky notices a new idea ────────────────────────
   .field gets a short-lived .resonating class from App.onSubmit (both the
   local submitter's own rising moment and, later, other visitors' live
   arrivals). Every cloud gets one non-repeating opacity ripple, staggered
   by its own --res-delay. This redeclares the full `animation` shorthand
   (can't just add a third animation via a separate rule — shorthand doesn't
   merge) following the same pattern as .cloud.poofing below. */
.field.resonating .cloud {
  animation: drift var(--dur) linear var(--delay) infinite,
             cloud-fadein 0.5s ease-out forwards,
             cloud-resonance 900ms ease-in-out var(--res-delay, 0s) 1;
}
.field.resonating .cloud.ltr {
  animation: drift-ltr var(--dur) linear var(--delay) infinite,
             cloud-fadein 0.5s ease-out forwards,
             cloud-resonance 900ms ease-in-out var(--res-delay, 0s) 1;
}

@keyframes cloud-resonance {
  0%   { opacity: 1; }
  40%  { opacity: 0.6; }
  100% { opacity: 1; }
}

/* Warm gold wash, same static-background/animated-opacity pattern as
   .rising-scrim below — pulses once over the sky when a new idea arrives. */
.resonance-pulse {
  position: fixed;
  inset: 0;
  z-index: 6;
  pointer-events: none;
  background: radial-gradient(120% 80% at 50% 30%,
    rgba(255, 205, 120, 0.35) 0%,
    rgba(255, 205, 120, 0.14) 45%,
    transparent 75%);
  opacity: 0;
  animation: resonance-pulse-fade 2500ms ease-out forwards;
}

@keyframes resonance-pulse-fade {
  0%   { opacity: 0; }
  18%  { opacity: 1; }
  60%  { opacity: 0.5; }
  100% { opacity: 0; }
}

/* ── rising cloud (newly submitted note) ───────────────────── */

/* All drifting clouds pause while the new cloud's entry animation plays */
.field.paused .cloud {
  animation-play-state: paused;
}

/* Soft fog layer that dims the sky while the new cloud rises */
.rising-scrim {
  position: fixed;
  inset: 0;
  z-index: 7;
  background: rgba(240, 245, 255, 0.52);
  -webkit-backdrop-filter: blur(6px) saturate(80%);
  backdrop-filter: blur(6px) saturate(80%);
  pointer-events: none;
  animation: rising-scrim-fade 3500ms ease forwards;
}

@keyframes rising-scrim-fade {
  0%        { opacity: 0; }
  14%       { opacity: 1; }
  72%       { opacity: 1; }
  100%      { opacity: 0; }
}

/* The wrapper rises from off-screen to the upper-centre of the sky */
.rising {
  position: absolute;
  top: 14%;
  left: 50%;
  transform: translate(-50%, 90vh) scale(0.35);
  z-index: 8;
  opacity: 0;
  pointer-events: none;
  animation: rise 3500ms cubic-bezier(0.22, 0.9, 0.36, 1) forwards;
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 14px;
}

@keyframes rise {
  0%   { transform: translate(-50%, 90vh) scale(0.35); opacity: 0; }
  16%  { opacity: 1; }
  65%  { transform: translate(-50%, 0) scale(1.04); opacity: 1; }
  100% { transform: translate(-50%, 0) scale(1); opacity: 0.88; }
}

/* Cloud image + text overlay */
.rising-cloud {
  position: relative;
  width: min(560px, 60vw);
  --w: min(560px, 60vw);
}

/* Caption rides inside the wrapper — inherits the rise movement */
.rising-caption {
  font-family: 'Caveat', cursive;
  font-size: 22px;
  color: var(--sky-text);
  opacity: 0;
  animation: caption-fade 3500ms ease-out forwards;
  pointer-events: none;
  text-align: center;
  white-space: nowrap;
}

@keyframes caption-fade {
  0%, 12%  { opacity: 0; }
  25%, 72% { opacity: 0.88; }
  100%     { opacity: 0; }
}

/* ── add-note button ───────────────────────────────────────── */

.add-btn {
  position: fixed;
  left: 50%;
  bottom: clamp(20px, 4vh, 37px);
  transform: translateX(-50%);
  z-index: 10;
  appearance: none;
  border: 0;
  padding: 2px;
  border-radius: 26px;
  background: linear-gradient(270deg, #f43f5e, #c026d3, #7c3aed, #2563eb, #0891b2, #f43f5e);
  background-size: 400% 400%;
  animation: btn-rainbow 2.4s ease infinite;
  color: #222;
  font-weight: 700;
  font-size: 15px;
  letter-spacing: 0.01em;
  display: inline-flex;
  align-items: center;
  cursor: pointer;
  box-shadow: 0 6px 20px 0 rgba(32, 22, 6, 0.08);
  transition: transform 220ms cubic-bezier(0.22, 0.9, 0.36, 1),
              box-shadow 220ms ease;
}

.add-btn-label {
  position: relative;
  z-index: 1;
  padding: 9px 21px;
  border-radius: 24px;
  white-space: nowrap;
  background: rgba(254, 254, 254, 0.88);
  -webkit-backdrop-filter: blur(20px) saturate(140%);
  backdrop-filter: blur(20px) saturate(140%);
}

.add-btn:hover {
  transform: translateX(-50%) translateY(-2px);
  box-shadow: 0 10px 28px 0 rgba(32, 22, 6, 0.12);
}

.add-btn:hover .add-btn-label {
  background: rgba(254, 254, 254, 0.96);
}

.add-btn:active { transform: translateX(-50%) translateY(0); }

/* ── page navigation arrows (slide between the two facets) ─── */

/* Small rounded-rectangle button hugging the edge — no caption (the arrow
   plus the edge position already says "more this way"; text just repeated
   the aria-label). Sized/styled after Apple Music's carousel arrow (a
   compact dark-glass capsule, taller than wide with softly rounded
   corners — not a full circle, not the tall edge-to-edge pill this used
   to be), now that swipe is the primary way to move between facets and
   the arrow is a secondary affordance rather than the main touch target
   it used to be. */
.page-arrow {
  position: fixed;
  top: 50%;
  transform: translateY(-50%);
  z-index: 10;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  width: clamp(34px, 4.5vw, 42px);
  height: clamp(64px, 9vh, 84px);
  appearance: none;
  border: 0;
  cursor: pointer;
  background: rgba(20, 22, 30, 0.38);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
  border-radius: 20px;
  box-shadow: 0 4px 14px -6px rgba(20, 22, 30, 0.35);
  transition: transform 220ms cubic-bezier(0.22, 0.9, 0.36, 1), background 220ms ease;
}

.page-arrow:hover {
  background: rgba(20, 22, 30, 0.55);
}

.page-arrow-right { right: clamp(10px, 2.5vw, 20px); }
.page-arrow-right:hover { transform: translateY(-50%) translateX(4px); }

.page-arrow-left { left: clamp(10px, 2.5vw, 20px); }
.page-arrow-left:hover { transform: translateY(-50%) translateX(-4px); }

.page-arrow-icon {
  width: clamp(14px, 2.2vw, 18px);
  height: clamp(14px, 2.2vw, 18px);
  flex-shrink: 0;
  color: #ffffff;
}

/* ── background music toggle ─────────────────────────────────── */

.music-toggle-wrap {
  position: fixed;
  top: clamp(2px, 1vw, 10px);
  right: clamp(2px, 1vw, 10px);
  z-index: 10;
  display: flex;
  align-items: center;
  gap: 8px;
}

.music-toggle {
  width: 44px;
  height: 44px;
  border-radius: 50%;
  border: 0;
  padding: 0;
  appearance: none;
  cursor: pointer;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  flex: none;
  background: transparent;
  box-shadow: none;
  opacity: 0.45;
  transition: opacity 220ms ease, transform 220ms cubic-bezier(0.22, 0.9, 0.36, 1);
}

.music-toggle:hover {
  opacity: 0.8;
  transform: translateY(-2px);
}

.music-toggle svg {
  width: 20px;
  height: 20px;
  color: rgba(255, 255, 255, 0.85);
  filter: drop-shadow(0 1px 3px rgba(60, 70, 110, 0.25));
}

/* Courtesy credit (not a license requirement — see MusicToggle's comment):
   always visible (hover reveal doesn't work on touch devices) but kept
   small and low-opacity so it doesn't compete with the icon for attention. */
.music-credit {
  font-family: 'Caveat', cursive;
  font-size: 13px;
  color: rgba(255, 255, 255, 0.65);
  text-shadow: 0 1px 3px rgba(60, 70, 110, 0.35);
  white-space: nowrap;
  pointer-events: none;
}

/* ── legend ────────────────────────────────────────────────── */

.legend {
  position: fixed;
  left: clamp(16px, 2.5vw, 32px);
  bottom: clamp(20px, 4vh, 36px);
  z-index: 10;
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px 14px;
  border-radius: 14px;
  background: rgba(255, 255, 255, 0.45);
  -webkit-backdrop-filter: blur(14px) saturate(140%);
  backdrop-filter: blur(14px) saturate(140%);
  border: 0.5px solid rgba(255, 255, 255, 0.6);
  box-shadow: 0 6px 20px -8px rgba(60, 70, 110, 0.18);
  font-size: 11.5px;
  color: var(--sky-soft);
  letter-spacing: 0.02em;
  max-width: min(280px, 40vw);
}

.legend-title {
  font-size: 9.5px;
  letter-spacing: 0.18em;
  text-transform: uppercase;
  color: var(--sky-soft);
  opacity: 0.78;
  margin-bottom: 2px;
  font-weight: 600;
}

.legend-row {
  display: flex;
  align-items: center;
  gap: 9px;
  line-height: 1.25;
  color: var(--sky-text);
  font-weight: 500;
}

.legend-swatch {
  width: 18px; height: 12px;
  border-radius: 6px;
  background: var(--swatch);
  box-shadow: 0 0 0 0.5px rgba(0,0,0,0.04), 0 1px 2px rgba(60,70,110,0.08);
  flex-shrink: 0;
}

/* count chip in the corner */
.count {
  display: none;
}

.count b {
  font-family: 'Caveat', cursive;
  font-weight: 500;
  font-size: 20px;
  color: var(--sky-text);
  letter-spacing: 0;
}

/* ── modal: composer and note view ─────────────────────────── */

.scrim {
  position: fixed;
  inset: 0;
  z-index: 20;
  background: radial-gradient(120% 80% at 50% 60%,
              rgba(255, 245, 235, 0.55) 0%,
              rgba(180, 195, 220, 0.5) 60%,
              rgba(120, 135, 175, 0.52) 100%);
  -webkit-backdrop-filter: blur(16px);
  backdrop-filter: blur(16px);
  animation: scrim-in 280ms ease-out;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 24px;
  overflow: hidden;
}

@keyframes scrim-in { from { opacity: 0; } to { opacity: 1; } }

/* ── composer sheet (3-step wizard) ───────────────────── */

.sheet {
  position: relative; /* anchors .pen-widget (#119) */
  width: min(664px, calc(100% - 48px));
  background: rgba(254, 254, 254, 0.5);
  -webkit-backdrop-filter: blur(28px) saturate(140%);
  backdrop-filter: blur(28px) saturate(140%);
  border: 0.5px solid rgba(254, 254, 254, 0.9);
  border-radius: 24px;
  padding: 32px;
  display: flex;
  flex-direction: column;
  min-height: 380px;
  color: #222;
  animation: sheet-in 380ms cubic-bezier(0.22, 0.9, 0.36, 1);
}

@keyframes sheet-in {
  from { transform: translateY(14px) scale(0.96); opacity: 0; }
  to   { transform: translateY(0) scale(1); opacity: 1; }
}

.step-body {
  display: flex;
  flex-direction: column;
  gap: 12px;
  flex: 1;
  min-height: 0;
}

.step-question {
  font-size: 15px;
  font-weight: 700;
  color: #222;
  margin: 0;
  line-height: 1.4;
  text-align: center;
}

.step-area {
  width: 100%;
  flex: 1;
  min-height: 120px;
  background: #fefefe;
  border: none;
  border-radius: 13px;
  padding: 16px;
  font-family: inherit;
  font-size: 15px;
  color: #222;
  resize: none;
  outline: none;
  box-sizing: border-box;
  line-height: 1.5;
}

.step-area::placeholder { color: #aaa; }

.step-fields {
  display: flex;
  flex-direction: column;
  gap: 14px;
  flex: 1;
}

.step-field {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.step-label {
  font-size: 15px;
  font-weight: 400;
  color: #222;
  margin: 0;
}

.step-input {
  width: 100%;
  background: #fefefe;
  border: none;
  border-radius: 13px;
  padding: 17px 16px;
  font-family: inherit;
  font-size: 15px;
  color: #222;
  outline: none;
  box-sizing: border-box;
}

.step-input::placeholder { color: #888; }

.step-warning {
  margin: 0;
  padding: 8px 12px;
  border-radius: 10px;
  /* Neutral, not --sky-warm/--sky-cool — those read as brand colors. */
  background: rgba(58, 64, 96, 0.08);
  color: var(--sky-text);
  font-size: 13px;
  font-weight: 500;
  line-height: 1.4;
}

/* Decorative pen + ink trail (#119), step 1 of the composer only. Tucked
   inside the sheet's top-right corner, over the textarea, at every
   viewport width — it used to float outside the sheet on desktop, but
   that read as disconnected from the actual fill area; sitting inside,
   near where the visitor is writing, is more relevant regardless of
   screen size. Only the size changes at narrower widths (see the media
   query below), not this positioning model. Positioned relative to
   .sheet itself. */
.pen-widget {
  position: absolute;
  top: 8px;
  right: 10px;
  width: 60px;
  /* Right-align both children instead of the default left-aligned block
     stack — the ink trail is wider than the pen icon, so right-aligning
     keeps its bulk under/left of the nib (which sits at the icon's own
     bottom-left) instead of spilling out to the right of the pen. */
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  pointer-events: none;
}

.pen-icon {
  display: block;
  width: 34px;
  height: 34px;
  transform-origin: 65% 75%; /* roughly the nib, so the scribble wiggle pivots near the writing tip */
  animation: pen-wiggle 7s ease-in-out infinite;
}

.pen-ink {
  display: block;
  width: 50px;
  height: 17px;
  margin-top: 2px;
}

.pen-ink path {
  stroke-dasharray: 100;
  animation: pen-ink-draw 7s ease-in-out infinite;
}

/* Active for the first ~2.5s of the 7s cycle (0-35%), still for the rest
   (~4.5s) — "move for 2-3 sec, pause for 4-5 sec" per the confirmed spec.
   A small scribbling wiggle, not a big swoop. */
@keyframes pen-wiggle {
  0%          { transform: rotate(0deg)   translate(0, 0); }
  4%          { transform: rotate(-6deg)  translate(-1px, 1px); }
  8%          { transform: rotate(5deg)   translate(1px, -1px); }
  12%         { transform: rotate(-6deg)  translate(-1px, 1px); }
  16%         { transform: rotate(5deg)   translate(1px, -1px); }
  20%         { transform: rotate(-4deg)  translate(-1px, 0); }
  24%         { transform: rotate(4deg)   translate(1px, 0); }
  28%         { transform: rotate(-3deg)  translate(0, 0); }
  32%, 100%   { transform: rotate(0deg)   translate(0, 0); }
}

/* Draws (stroke-dashoffset 100->0, pathLength="100" set on the <path> in
   JSX so this doesn't depend on the actual curve's geometry) over the same
   active window, holds a moment, then fades out during the pause — a fresh
   squiggle every cycle rather than one that accumulates. Doesn't trace the
   pen's actual tip position; a separate decorative curve, not a literal
   pen-follows-ink simulation (confirmed acceptable). */
@keyframes pen-ink-draw {
  0%   { opacity: 0; stroke-dashoffset: 100; }
  2%   { opacity: 1; }
  35%  { opacity: 1; stroke-dashoffset: 0; }
  50%  { opacity: 1; stroke-dashoffset: 0; }
  62%  { opacity: 0; }
  100% { opacity: 0; stroke-dashoffset: 100; }
}

/* Desktop only — pulled in from the sheet's corner (was flush against the
   edge) and the pen icon sized down a step; the ink trail keeps its size
   from the base rule above. */
@media (min-width: 861px) {
  .pen-widget {
    top: 14px;
    right: 20px;
  }
  .pen-icon { width: 26px; height: 26px; }
}

@media (max-width: 860px) {
  .pen-widget {
    top: 12px;
    width: 44px;
  }
  .pen-icon { width: 20px; height: 20px; }
  .pen-ink  { width: 31px; height: 11px; }
}

.step-consent {
  display: flex;
  align-items: flex-start;
  gap: 10px;
  font-size: 13px;
  color: #444;
  cursor: pointer;
  line-height: 1.4;
}

.step-consent input[type="checkbox"] {
  margin-top: 2px;
  flex-shrink: 0;
  accent-color: #7ba7bc;
  width: 16px;
  height: 16px;
  cursor: pointer;
}

.step-footer {
  display: flex;
  align-items: center;
  margin-top: 24px;
  flex-shrink: 0;
}

.step-counter {
  flex: 1;
  text-align: center;
  font-size: 15px;
  color: #222;
  font-weight: 400;
  pointer-events: none;
}

.step-btn {
  appearance: none;
  border: none;
  border-radius: 24px;
  padding: 12px 16px;
  flex: 1;
  font-family: inherit;
  font-size: 16px;
  font-weight: 600;
  cursor: pointer;
  transition: opacity 160ms ease, transform 160ms ease;
}

.step-btn.ghost {
  background: transparent;
  color: #222;
  text-align: left;
}

.step-btn.ghost:hover { opacity: 0.6; }

.step-btn.primary {
  background: #222;
  color: #fefefe;
  position: relative;
  overflow: hidden;
  isolation: isolate;
}

.step-btn.primary::before {
  content: '';
  position: absolute;
  inset: 0;
  z-index: -1;
  background: linear-gradient(270deg, #f43f5e, #c026d3, #7c3aed, #2563eb, #0891b2, #f43f5e);
  background-size: 400% 400%;
  animation: btn-rainbow 2.4s ease infinite;
  opacity: 0;
  transition: opacity 380ms ease;
}

@keyframes btn-rainbow {
  0%   { background-position: 0% 50%; }
  50%  { background-position: 100% 50%; }
  100% { background-position: 0% 50%; }
}

.step-btn.primary:hover:not(:disabled) { opacity: 1; }
.step-btn.primary:hover:not(:disabled)::before { opacity: 1; }
.step-btn.primary:disabled { opacity: 0.3; cursor: not-allowed; }

/* ── note-view modal (when you click a cloud) ──────────────── */

.note-view {
  width: min(660px, 92vw);
  position: relative;
  text-align: center;
  animation: sheet-in 420ms cubic-bezier(0.22, 0.9, 0.36, 1);
}

.note-view .cloud-art {
  position: relative;
  width: 100%;
  margin: 0 auto;
  filter: drop-shadow(0 24px 40px -14px rgba(50,60,100,0.3));
}

/* Content block inside the enlarged cloud.
   Label always at top, meta always at bottom, answer fills the middle. */
.note-view .nv-content {
  position: absolute;
  display: flex;
  flex-direction: column;
  justify-content: flex-start;
  align-items: center;
  text-align: center;
  gap: 4px;
  color: var(--sky-text);
  overflow: hidden;
}

.note-view .nv-label {
  font-size: 11px;
  font-weight: 700;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  opacity: 0.45;
  line-height: 1.2;
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
  max-width: 100%;
  flex-shrink: 0;
}

.note-view .nv-answer {
  flex: 1;
  min-height: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  margin: 0;
  font-size: clamp(15px, 1.8vw, 20px);
  font-weight: 500;
  line-height: 1.4;
  text-wrap: pretty;
  overflow: hidden;
  text-align: center;
}

.note-view .nv-meta {
  display: flex;
  align-items: center;
  gap: 8px;
  font-family: 'Caveat', cursive;
  font-size: clamp(15px, 1.6vw, 18px);
  opacity: 0.72;
  line-height: 1;
  flex-shrink: 0;
}

.note-view .nv-meta .dot {
  width: 3px; height: 3px; border-radius: 50%;
  background: currentColor; opacity: 0.5;
}

.note-view .x {
  position: absolute;
  top: -6px; right: -6px;
  width: 34px; height: 34px;
  border-radius: 50%;
  border: 0;
  background: rgba(255,255,255,0.95);
  color: var(--sky-text);
  font-size: 17px;
  cursor: pointer;
  display: inline-flex;
  align-items: center; justify-content: center;
  box-shadow: 0 8px 22px -8px rgba(58, 64, 96, 0.35);
}

/* ── responsive ────────────────────────────────────────────── */

/* Tablet ≤ 1000px */
@media (max-width: 1000px) {
  .scrim  { padding: 20px; }
  .sheet  { padding: 26px; min-height: 340px; }
  .step-area { min-height: 140px; }
  .cloud { scale: 0.78; }
}

/* Mobile ≤ 480px */
@media (max-width: 480px) {
  /* masthead — clamp() already scales but lift the floor */
  .masthead h1 { font-size: clamp(28px, 8.5vw, 38px); }
  .masthead p  { font-size: 11px; letter-spacing: 0.10em; }

  /* add button — larger touch target, safe-area aware */
  .add-btn {
    bottom: max(20px, calc(env(safe-area-inset-bottom, 0px) + 16px));
  }
  .add-btn-label {
    font-size: 14px;
    padding: 10px 19px;
  }

  /* composer modal */
  .scrim { padding: 12px 12px max(12px, env(safe-area-inset-bottom, 0px)); }
  .sheet {
    width: 100%;
    padding: 20px 16px;
    border-radius: 20px;
    min-height: 300px;
  }
  .step-question { font-size: 14px; }
  .step-area     { min-height: 110px; font-size: 14px; }
  .step-label    { font-size: 14px; }
  .step-input    { font-size: 14px; padding: 13px 14px; }
  .step-btn      { font-size: 14px; padding: 10px 12px; }
  .step-footer   { margin-top: 16px; }

  /* note-view modal — oversized cloud so text is readable; edges crop off-screen */
  .note-view { width: 140vw; flex-shrink: 0; }
  .note-view .nv-label  { font-size: 10px; }
  .note-view .nv-answer { font-size: 15px; }
  .note-view .nv-meta   { font-size: 14px; }
  .note-view .x { top: -4px; right: 15vw; width: 30px; height: 30px; font-size: 15px; }

  /* rising animation — stretch cloud to near full-width on mobile */
  .rising { top: 10%; }
  .rising-cloud { width: 140vw; --w: 140vw; flex-shrink: 0; }

  /* suppress info panels — already hidden in defaults */
  .count  { display: none; }
  .legend { display: none; }

  .cloud { scale: 0.72; }
  .cloud-content { scale: 0.90; }
  .cloud-content .q-label { font-size: clamp(4px,  calc(var(--w, 26vw) * 0.024), 9px); }
  .cloud-content .answer  { font-size: clamp(7px,  calc(var(--w, 26vw) * 0.040), 18px); }
  .cloud-content .meta    { font-size: clamp(8px,  calc(var(--w, 26vw) * 0.030), 14px); }
}

@media (prefers-reduced-motion: reduce) {
  .cloud, .rising, .rising-caption,
  .cloud-inner, .cloud-breath, .cloud-nudge,
  .resonance-pulse,
  .mote, .mote-inner { animation-duration: 0.001ms !important; }
  .cloud { left: 50% !important; transform: translateX(-50%); }
  .pages { transition-duration: 0.001ms !important; }
  /* No sensible static frame for a continuous scribble-and-fade loop —
     same reasoning as the random sky events — so hide it entirely rather
     than freeze it mid-cycle. */
  .pen-widget { display: none; }
}
