/* Timer — slice 7 of 9 of the former components.css.

   ⚠️ LOAD ORDER IS PART OF THESE FILES. They are CONTIGUOUS slices of one
   6841-line sheet, cut at section banners and never re-sorted, so concatenating
   them in the order index.html lists them reproduces the original byte for byte
   — which is the proof the cascade did not shift. Moving a rule to a different
   slice throws that guarantee away. index.html and sw.js's SHELL_ASSETS must
   list all 9 in the same order as each other.

   Contents: Boot splash, Study Timer + mood picker + pulsing lattice, animated avatars. */

/* ── Boot splash — animated WarmUp mark shown until app.js mounts the first view.
   The WHITE star spins at an UNEVEN pace (bursts then hangs) inside a gently
   breathing AMBER hex — matching the flower's centre tile; the wordmark pulses.
   Fixed + full-screen so it's centred to the whole viewport. Respects
   reduced-motion. */
.wu-boot {
  position: fixed;
  inset: 0;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  background: var(--colour-bg);
  z-index: 1;
}
.wu-boot-logo {
  width: clamp(84px, 18vh, 132px);
  height: auto;
  filter: drop-shadow(0 6px 14px rgba(122, 75, 0, 0.22));
}
.wu-boot-hex {
  fill: var(--colour-brand, #ffb74d);
  transform-box: view-box;
  transform-origin: 54px 54px;
  animation: wu-boot-breathe 2.4s ease-in-out infinite;
}
.wu-boot-star {
  fill: #fff;
  transform-box: view-box;
  transform-origin: 54px 53px;
  animation: wu-boot-spin 2.4s ease-in-out infinite;
}
.wu-boot-text {
  margin: 0;
  font-family: var(--font-display);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--colour-brand-deep, #fb8c00);
  animation: wu-boot-fade 2.4s ease-in-out infinite;
}
/* Uneven angular velocity: quick to 150°, hang near the half-turn, quick again,
   settle — the ease-in-out per segment makes it feel playful, not mechanical. */
@keyframes wu-boot-spin {
  0%   { transform: rotate(0deg); }
  20%  { transform: rotate(150deg); }
  45%  { transform: rotate(185deg); }
  70%  { transform: rotate(330deg); }
  100% { transform: rotate(360deg); }
}
@keyframes wu-boot-breathe {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.05); }
}
@keyframes wu-boot-fade {
  0%, 100% { opacity: 0.55; }
  50%      { opacity: 1; }
}
@media (prefers-reduced-motion: reduce) {
  .wu-boot-hex,
  .wu-boot-star,
  .wu-boot-text {
    animation: none;
  }
}

/* ── 🎧 Study Timer — full-screen calm timer (was "Focus time") ── */
.wu-focus-backdrop {
  position: fixed;
  inset: 0;
  z-index: 60;
  display: flex;
  align-items: center;
  justify-content: center;
  background: color-mix(in srgb, var(--colour-bg) 88%, #000);
}
.wu-focus-card {
  position: relative;
  background: var(--colour-surface);
  border-radius: var(--radius-card, 12px);
  padding: var(--space-lg);
  width: min(420px, 92vw);
  text-align: center;
  box-shadow: 0 12px 40px rgba(0, 0, 0, 0.18);
}
/* ── Mood picker on the Study Timer setup card (owner, 2026-08-29) ────────
   Sits BELOW the h:m wheel and above ▶ Start, so the card reads in the order
   the decisions are made: how long, then what it sounds like, then go. */
.wu-focus-moodrow {
  margin-top: var(--space-md);
  text-align: center;
}
.wu-focus-moodlab {
  margin: 0 0 var(--space-xs);
  color: var(--colour-text-secondary);
  font-size: var(--font-size-sm);
}
.wu-focus-moods {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  justify-content: center;
}
.wu-mchip-ic {
  display: block;
  font-size: 1.35rem;
  line-height: 1.1;
}
.wu-mchip-tx {
  display: block;
  font-size: 0.85rem;
  line-height: 1.2;
}
.wu-focus-mchip {
  /* Share the row EQUALLY, so the three read as one block and ▶ Start below
     lines up with their full span rather than floating at its own width.
     `flex-basis: 0` makes the three the same width regardless of label length
     (with `auto` they came out 131/151/122 px, which reads as sloppy rather
     than deliberate); the min-width then makes them WRAP on a narrow card
     instead of squeezing "☀️ Morning" past legibility. */
  flex: 1 1 0;
  min-width: 96px;
  min-height: 44px; /* tap-target floor, #88–#90 */
  padding: var(--space-xs) var(--space-md);
  border: 2px solid var(--colour-border);
  border-radius: 999px;
  background: var(--colour-surface);
  padding: var(--space-xs) var(--space-sm);
  font-family: var(--font-display);
  font-weight: 700;
  cursor: pointer;
}
/* Selected and PLAYING are deliberately different states: a child can choose a
   mood without it being the one currently making noise, and after they tap the
   playing chip a second time to silence it, the choice must visibly survive. */
.wu-focus-mchip.wu-on {
  border-color: var(--colour-brand-deep);
  background: color-mix(in srgb, var(--colour-brand) 22%, var(--colour-surface));
}
.wu-focus-mchip.wu-playing {
  animation: wu-mchip-pulse 2.4s ease-in-out infinite;
}
@keyframes wu-mchip-pulse {
  0%, 100% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--colour-brand) 55%, transparent); }
  50% { box-shadow: 0 0 0 6px color-mix(in srgb, var(--colour-brand) 0%, transparent); }
}
@media (prefers-reduced-motion: reduce) {
  .wu-focus-mchip.wu-playing {
    animation: none;
    box-shadow: 0 0 0 3px color-mix(in srgb, var(--colour-brand) 55%, transparent);
  }
}

.wu-focus-choices {
  display: flex;
  flex-wrap: wrap;
  gap: var(--space-sm);
  justify-content: center;
  margin-top: var(--space-sm);
}
.wu-focus-choice {
  /* Full width, matching the mood row above it (owner, 2026-08-29). The card is
     now one column of equal-width blocks: wheel, moods, start.

     SOLID, not the tinted-accent treatment `.wu-results-done` uses — and that
     is a contrast decision, not a taste one. Measured on this palette, EVERY
     accent-as-text-on-its-own-22%-tint fails AA badly (success 2.41, accent
     2.27, brand-deep 1.81), and white on solid brand amber is 2.37. White on
     a DARKENED green is the only option here that both stands out and is
     legible; #2E7D32 gives 5.3:1, clearing AA for normal text outright rather
     than relying on the large-text allowance. Green also reads as "go", which
     is what this button does. See #121 for the outstanding palette items. */
  flex: 1 1 100%;
  min-height: 48px;
  padding: var(--space-sm) var(--space-md);
  border: 2px solid #2E7D32;
  border-radius: var(--radius-button, 8px);
  background: #2E7D32;
  color: #fff;
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1.1rem;
  cursor: pointer;
}
.wu-focus-choice:active { background: #256428; border-color: #256428; }
.wu-focus-choice:hover {
  border-color: var(--colour-brand);
}

/* Custom-duration HH:MM wheel — looping scroll-snap columns, no browser
   prompt. Viewport = 5 rows (selected ± two, for the spin effect); vertical
   padding = two item heights so the snap centre is the middle row; item
   height must match WHEEL_ITEM_H in focus.js. Edges fade via mask. */
.wu-wheel-row {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: var(--space-sm);
  margin: var(--space-md) 0;
}

.wu-wheel-col {
  display: flex;
  align-items: center;
  gap: 4px;
}

.wu-wheel-unit {
  font-family: var(--font-display);
  font-size: var(--font-size-sm);
  font-weight: var(--font-weight-bold);
  color: var(--colour-text-secondary);
}

.wu-wheel {
  box-sizing: border-box; /* 220px INCLUDES the 2×88px padding → exactly 5 rows visible */
  height: 220px;
  width: 84px;
  overflow-y: auto;
  scroll-snap-type: y mandatory;
  padding: 88px 0;
  border: 1px solid var(--colour-border);
  border-radius: var(--radius-card);
  background: var(--colour-surface);
  scrollbar-width: none;
  -webkit-mask-image: linear-gradient(transparent, #000 28%, #000 72%, transparent);
  mask-image: linear-gradient(transparent, #000 28%, #000 72%, transparent);
}

.wu-wheel::-webkit-scrollbar {
  display: none;
}

.wu-wheel-item {
  height: 44px;
  display: flex;
  align-items: center;
  justify-content: center;
  scroll-snap-align: center;
  font-family: var(--font-mono);
  font-size: 1.4rem;
  font-weight: 700;
  color: var(--colour-text-secondary);
  opacity: 0.4;
}

/* Centre row = the selection: black, slightly larger, level with the h/m unit */
.wu-wheel-item.wu-wheel-on {
  color: var(--colour-text-primary);
  font-size: 1.7rem;
  opacity: 1;
}

.wu-wheel-sep {
  font-family: var(--font-mono);
  font-size: 1.8rem;
  font-weight: 700;
  color: var(--colour-text-secondary);
}
.wu-focus-stage {
  position: relative;
  width: 100%;
  height: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: var(--space-md);
  overflow: hidden;
  background: var(--colour-bg);
}
/* ── Background hex lattice, pulsing with the music (owner, 2026-08-29) ─────
   The same clip-path + `--colour-grid-line` language as `.wu-ghost-hex` behind
   every flower, so the timer looks like WarmUp rather than a generic pattern.
   Sits furthest back, behind the drifting glyphs and the clock. */
.wu-focus-lattice {
  position: absolute;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  z-index: 0;
  /* THE BACKGROUND is what reacts to the music (owner, v370). The chord change
     walks this hue; the per-cell breathing above carries the tempo. Both live
     on the lattice now, so the rising icons can stay solid and still. */
  --focus-chord: 0;
  filter: hue-rotate(calc(var(--focus-chord) * 16deg));
  transition: filter 2.4s ease;
}
@media (prefers-reduced-motion: reduce) {
  .wu-focus-lattice { transition: none; filter: none; }
}
.wu-focus-cell {
  position: absolute;
  /* width/height and --f are written inline by buildLattice() from the measured
     pitch — see the note there. Declaring them from var(--hex-w) here as well
     would be a second source of truth for the one thing that must not drift. */
  opacity: 0.26;
  /* Regular breathing, at the music's tempo: --pulse is two beats of the current
     mood, set on the lattice by runTimer. The NEGATIVE delay scaled by --f
     (1 at the centre, 0 at the corners) starts central cells further into the
     cycle so they peak first — the breath and the outward travel are one
     motion, and it costs no JS per beat. */
  animation: wu-lattice-breathe var(--pulse, 1.6s) ease-in-out infinite;
  animation-delay: calc(var(--pulse, 1.6s) * -0.5 * var(--f, 0));
}
@keyframes wu-lattice-breathe {
  0%, 100% { opacity: 0.22; }
  50% { opacity: 0.62; }
}
.wu-focus-cell::before {
  content: "";
  position: absolute;
  inset: 0;
  clip-path: polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
  /* WARM, not the neutral grid grey (owner, v370: "the colours are too
     neutral"). Mixed toward the brand amber so the lattice belongs to the same
     palette as the icons rising over it, while staying quiet enough to read as
     background rather than content. */
  background: color-mix(in srgb, var(--colour-brand) 52%, var(--colour-grid-line));
}
@media (prefers-reduced-motion: reduce) {
  .wu-focus-cell { animation: none; opacity: 0.3; }
}

/* ⏸ Pause freezes the PICTURE as well as the sound (owner, 2026-08-29).
   Suspending the AudioContext stops the music and the chord cues, but the
   lattice and the drifting icons are pure CSS and carried on — a background
   still breathing to music that has stopped reads as the pause not working.
   `animation-play-state` holds them mid-cycle rather than resetting, so
   ▶ Resume continues from the same phase, exactly as the audio does. */
.wu-focus-frozen .wu-focus-cell,
.wu-focus-frozen .wu-focus-drift {
  animation-play-state: paused;
}

.wu-focus-sky {
  position: absolute;
  inset: 0;
  pointer-events: none;
  /* The rising icons hold STILL colour-wise and at full strength. Every
     reaction to the music lives on the lattice behind them (owner, v370: "it's
     the background that should change"), so this layer only carries the drift. */
  opacity: 1;
}


.wu-focus-drift {
  position: absolute;
  bottom: -100px;
  /* SOLID (owner, v370: "the latter should not be transparent, the animation is
     sufficient"). They were 38% brand over transparent, which on warm paper is
     barely there — the RISING is what makes them ambient, not the fading, and
     washing them out only cost legibility. Colour is stable here too: it is the
     BACKGROUND that changes with the music, not the icons.

     The DEEP amber, not the light one: the lattice behind is now mixed toward
     the same brand colour, so the plain `--colour-brand` put amber icons on an
     amber field — cohesive but measured at 1.57:1 against it, i.e. barely
     separated. The deeper tone keeps the palette and gives the icons somewhere
     to sit. This is the "balance between the background effect and the rising
     icons" the owner asked for. */
  color: var(--colour-brand-deep);
  animation: wu-focus-rise linear infinite;
}
/* The child's avatar drifting along — rides the profile-colour disc (inline
   background from ambience()) like the HUD chip, so it's unmistakably this
   child's even when the avatar is just an initial letter. */
.wu-focus-drift-av {
  opacity: 1;
  width: 1.5em;
  height: 1.5em;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-radius: 50%;
  color: #fff;
  font-family: var(--font-display);
  font-weight: var(--font-weight-bold);
}
@keyframes wu-focus-rise {
  from { transform: translateY(0) rotate(0deg); }
  to   { transform: translateY(-120vh) rotate(160deg); }
}
.wu-focus-clock {
  font-family: var(--font-mono, "JetBrains Mono", monospace);
  font-size: clamp(4rem, 16vw, 9rem);
  font-weight: 600;
  color: var(--colour-text-primary);
  z-index: 1;
}
.wu-focus-donemark {
  font-size: 4rem;
  z-index: 1;
}
.wu-focus-note {
  margin: 0;
  color: var(--colour-text-secondary);
  z-index: 1;
}
.wu-focus-actions {
  display: flex;
  gap: var(--space-md);
  z-index: 1;
}
.wu-focus-btn2 {
  padding: var(--space-sm) var(--space-lg);
  border: 2px solid var(--colour-border);
  border-radius: var(--radius-button, 8px);
  background: var(--colour-surface);
  font-family: var(--font-display);
  font-weight: 700;
  font-size: 1rem;
  cursor: pointer;
}
.wu-focus-stop {
  color: var(--colour-danger);
}
@media (prefers-reduced-motion: reduce) {
  .wu-focus-drift { animation: none; }
}

/* ── Animated premium avatars (8⭐ tier) ──
   This lands on the inner FACE span, never on the avatar disc — a disc carrying
   it leaves its own layout box and the ring escapes the frame (see
   paintAvatarFace in avatar-kit.js). `.wu-av-face` is present in both states and
   matches the animated box model exactly, so turning the bob on and off cannot
   move anything. */
.wu-av-face {
  display: inline-block;
  line-height: 1;
}
.wu-av-anim {
  display: inline-block;
  animation: wu-av-bob 2.2s ease-in-out infinite;
}
@keyframes wu-av-bob {
  0%, 100% { transform: translateY(0) rotate(-4deg); }
  50%      { transform: translateY(-9%) rotate(4deg); }
}
@media (prefers-reduced-motion: reduce) {
  .wu-av-anim { animation: none; }
}

/* 🏆 Gold frame (3⭐, the top tier) shimmers. The 8⭐ legendary FACES already
   bob, so "the top tier moves" is an established convention here rather than
   a new idea — and it is what makes gold unmistakable at 24px, where a static
   amber ring on an amber disc was not (owner: "unclear what frame 5 does").
   The keyframes carry the FULL box-shadow because an animation outranks the
   inline style avatarStyle() writes; the two must therefore agree. */
@keyframes wu-av-shine {
  0%, 100% { box-shadow: 0 0 0 2px #fff, 0 0 0 5px #FFC107, 0 0 0 6px #FB8C00; }
  50%      { box-shadow: 0 0 0 2px #fff, 0 0 0 5px #FFF3C4, 0 0 0 6px #FFC107; }
}
@media (prefers-reduced-motion: reduce) {
  [style*="wu-av-shine"] { animation: none !important; }
}

