/* ─────────────────────────────────────────────────────────────────────────
   Bhuri Bai Journey — design tokens + module styles
   All sizes are authored against a 1920×1080 stage; JS scales to viewport.
   Figma file: 56BUyvl507GbU932I5HjTk
   ───────────────────────────────────────────────────────────────────────── */

/* Brand display face. Two weights map to every weight used in the page:
   medium → 500, bold → 700. Nunito (Google Fonts) remains in every stack
   as a swap-time fallback so text never disappears while the OTFs load. */
@font-face{
  font-family:'Gotham Rounded';
  src:url('../fonts/gothamrnd_medium.otf') format('opentype');
  font-weight:500;
  font-style:normal;
  font-display:swap;
}
@font-face{
  font-family:'Gotham Rounded';
  src:url('../fonts/gothamrnd_bold.otf') format('opentype');
  font-weight:700;
  font-style:normal;
  font-display:swap;
}

:root{
  /* Draggables' hand cursors — the client's hand_open / hand_closed
     glyphs (Figma 571:1028 / 571:1025), same art the retired cursor.js
     follower showed for grab/grabbing: 44px, kohl-outlined white fill,
     palm-centred hotspot (22,22). Keyword fallbacks ride in the value. */
  --cur-grab:url("data:image/svg+xml,%3Csvg%20width%3D%2244%22%20height%3D%2244%22%20viewBox%3D%220%200%2032%2032%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20stroke%3D%22%2314141F%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20paint-order%3D%22stroke%22%3E%3Cpath%20d%3D%22M26.6502%2018.4C27.0835%2019.7%2027.1335%2021%2026.8001%2022.3L26.2502%2023.85C25.3835%2025.65%2024.0002%2026.8667%2022.1002%2027.5H22.0501L18.3001%2027.9C17.0335%2027.5333%2015.7668%2026.9667%2014.5001%2026.2H14.4501C13.2835%2025.5%2012.0168%2025.0333%2010.6501%2024.8L7.70015%2024.7H7.65015C7.05015%2024.8%206.51682%2024.65%206.05015%2024.25L6.00015%2024.2C5.43348%2023.7667%205.10015%2023.2167%205.00015%2022.55C4.93348%2021.8833%205.11682%2021.2833%205.55015%2020.75C5.91682%2020.2833%206.38348%2020.0167%206.95015%2019.95C7.88348%2019.85%208.81682%2019.85%209.75015%2019.95L10.4001%2019.8L10.8001%2019.3V18.65C9.73348%2015.4833%208.36682%2012.55%206.70015%209.85C6.30015%209.21667%206.15015%208.65%206.25015%208.15C6.41682%207.68334%206.81682%207.26667%207.45015%206.9C8.81682%206.1%209.96682%206.36667%2010.9001%207.7L12.0001%209.55L12.5501%209.95H13.2501L13.7501%209.45L13.8502%208.75L13.1501%206.15C12.7501%204.75%2013.2501%203.88334%2014.6501%203.55H14.7001C16.1335%203.15%2017.1501%203.68334%2017.7501%205.15L18.4502%207.5L18.8502%208.05L19.5001%208.2L20.1002%207.95L20.4001%207.35C20.6001%206.21667%2021.3335%205.68334%2022.6002%205.75C24.1002%205.85%2024.9002%206.68334%2025.0002%208.25V8.35C24.8668%2011.9167%2025.2335%2014.75%2026.1002%2016.85V16.8L26.6502%2018.4Z%22%20fill%3D%22white%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") 22 22, grab;
  --cur-grabbing:url("data:image/svg+xml,%3Csvg%20width%3D%2244%22%20height%3D%2244%22%20viewBox%3D%220%200%2032%2032%22%20fill%3D%22none%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cg%20stroke%3D%22%2314141F%22%20stroke-width%3D%221.9%22%20stroke-linecap%3D%22round%22%20stroke-linejoin%3D%22round%22%20paint-order%3D%22stroke%22%3E%3Cpath%20d%3D%22M20.8499%206.35001C22.7166%206.18334%2023.8499%207.03334%2024.2499%208.90001C24.3833%2010.4333%2024.6833%2011.7667%2025.1499%2012.9L25.7999%2014.5C26.2666%2015.9333%2026.3166%2017.3667%2025.9499%2018.8L25.3499%2020.45L24.8999%2021.3L24.8499%2021.35C23.7499%2023.35%2021.5999%2024.6%2018.3999%2025.1H18.3499C13.9833%2025.8333%2010.9833%2024.8333%209.34992%2022.1L9.24992%2022C8.74992%2021.5%208.29992%2020.9%207.89992%2020.2L7.69992%2020.05L7.59992%2020C5.69992%2019.2667%204.91659%2017.95%205.24992%2016.05C5.28325%2015.75%205.36659%2015.4833%205.49992%2015.25V15.2C5.96659%2014.5%206.58325%2014.05%207.34992%2013.85L7.54992%2013.7L7.69992%2013.45V13.2C6.96659%2011.2333%206.98325%209.90001%207.74992%209.20001L8.49992%208.65001C9.79992%207.88334%2010.9333%207.88334%2011.8999%208.65001L12.2499%208.75001L12.5499%208.65001L12.6999%208.35001C12.8999%207.11667%2013.7166%206.45001%2015.1499%206.35001C16.4166%206.25001%2017.3333%206.58334%2017.8999%207.35001L18.1499%207.55001H18.4499L18.6999%207.35001C19.1333%206.78334%2019.8499%206.45001%2020.8499%206.35001Z%22%20fill%3D%22white%22%2F%3E%3C%2Fg%3E%3C%2Fsvg%3E") 22 22, grabbing;
  /* palette */
  --aamras:   #FFA300;
  --aamras-d: #E58F00;
  --gulabi:   #D0006F;
  --ferozi:   #5CB8B2;
  --baingani: #500778;
  --kohl:     #14141F;
  --black:    #000000;
  --paper:    #FFFFFF;
  --stroke:   #E8E2D9;
  --bg-sec:   #F5F3F0;
  --lal:      #D50032;
  --ink:      #000000;
  --ink-mute: #6B6B6B;

  /* radii / shadows */
  --r-cta:   24rem;
  --r-card:  20rem;
  --r-mod:   24rem;
  --sh-cta:  0 4rem 32rem rgba(0,0,0,.16);
  --sh-card: 0 4rem 7rem rgba(0,0,0,.08);
  --sh-img:  0 8rem 32rem rgba(0,0,0,.12), 0 2rem 6rem rgba(0,0,0,.06);
  --sh-letter: 0 2rem 4rem rgba(0,0,0,.20);
  --sh-modal: 0 8rem 32rem rgba(0,0,0,.12), 0 2rem 6rem rgba(0,0,0,.06);
  --sh-close: 0 2rem 4rem rgba(0,0,0,.5);

  /* timing */
  --ease: cubic-bezier(.22,.61,.36,1);
  --ease-boing: cubic-bezier(.34,1.56,.64,1);
  --t-fast: 180ms;
  --t-med:  300ms;
}

*,*::before,*::after{box-sizing:border-box}
/* ── Fluid scale root ──────────────────────────────────────────────────
   The whole runtime is authored against a 1920×1080 design canvas. By
   setting the html font-size to the smaller of (viewport-width / 1920,
   viewport-height / 1080), 1rem equals exactly one design-pixel at every
   viewport size: 1rem resolves to 1px on a 1920×1080 monitor, ~0.71px
   on a 1366×768 laptop, ~1.33px on a 2560×1440 display. All distances
   throughout the
   runtime CSS are expressed in rem, so they scale uniformly without the
   GPU transform that the old fitStage() applied to the entire stage.
   The CMS editor (.cms-body) overrides this — its preview pane still
   wants a fixed-px canvas so the fitPreview() scale math stays valid. */
html{font-size:min(0.052083vw, 0.092593vh);}
html,body{margin:0;padding:0;height:100%;background:#0E0D0C;overflow:hidden;
  font-family:"Gotham Rounded","Nunito",system-ui,-apple-system,sans-serif;
  color:var(--ink);-webkit-font-smoothing:antialiased;
  -webkit-tap-highlight-color:transparent;   /* iPad Safari: no grey tap flash */
  /* Brand cursor — white-fill, black-stroke pointer with a soft drop-shadow
     baked into the SVG (feDropShadow). The viewBox is padded so the blur
     doesn't clip; the hotspot 3,3 lands the click point on the arrow tip
     after that padding. `default` falls back if the SVG fails to load. */
  cursor:url('../img/cursor.svg') 3 3, default;
}
/* Force the brand cursor on every <button>/<a> by inheriting from body.
   Author `cursor: inherit` is required because the browser user-agent
   stylesheet sets an explicit cursor on these elements, which would
   otherwise beat the inherited brand cursor. .image-frame's higher-
   specificity `cursor: zoom-in` still wins, so the zoom affordance stays. */
button,a{font:inherit;color:inherit;background:none;border:0;padding:0;text-decoration:none;cursor:inherit}
img,svg{display:block;max-width:100%}
[hidden]{display:none!important}
p{margin:0}

/* ─────────────────────────────────────────
   Stage = scaled 1920×1080 canvas
   ───────────────────────────────────────── */
.stage{position:fixed;inset:0;display:grid;place-items:center;
  background:radial-gradient(120% 120% at 50% 0%, #1a1815 0%, #0a0908 100%);
  /* Kiosk surface: drags must never start a text selection (Safari only
     honours the -webkit- form) or an iPad long-press save-image callout.
     Player-only by construction — the editor has no .stage; its preview
     pane staying selectable is harmless. */
  -webkit-user-select:none;user-select:none;-webkit-touch-callout:none;}
.screen-container{
  position:relative;width:1920rem;height:1080rem;
  transform-origin:center;
  background:var(--paper);overflow:hidden;
  box-shadow:0 12rem 60rem rgba(0,0,0,.45),0 2rem 0 rgba(255,255,255,.04) inset;
  perspective:2000rem;
  perspective-origin:50% 40%;
}
/* Sections rotate in 3D for the page-flip — backface-visibility: hidden lets
   them disappear cleanly when they pass the 90° threshold instead of showing
   a mirror image of their content.
   No transform-style:preserve-3d here — children have no 3D transforms of
   their own, so flattening each section into a single GPU texture is much
   cheaper than perpetually rendering it as a 3D rendering context. The
   flip rotates the section as a whole quad, which works either way.
   contain: layout paint lets the browser treat each section as an isolated
   render block, so child tweens don't invalidate sibling sections. */
.screen-container > section{
  will-change:transform,opacity;
  backface-visibility:hidden;
  contain:layout paint;
}

/* Input gate during the entrance choreography. Entrance from-tweens hide
   elements via opacity, which keeps them pointer-interactive — and since
   tap-to-answer, the first tap on a choice is FINAL. Without this a tap
   during the entrance (transition covers are pointer-events:none, so taps
   pass through them too) silently commits an answer the visitor never saw.
   Module enter() adds .is-entering at creation and removes it when its
   timeline completes; goBack's quick path skips enter() entirely, so
   back-navigation stays immediately interactive. */
.screen-container > section.is-entering{
  pointer-events:none;
}

/* Attention shimmer — a translucent diagonal sweep across primary CTAs.
   The button sits idle most of the cycle; the gradient sweeps across briefly,
   prompting the user to tap. Pure CSS keyframe so it survives section
   teardown / reattach without JS bookkeeping. */
.cta-play::after,
.complete-card .cta-discover::after{
  content:'';
  position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:linear-gradient(115deg,
    transparent 30%,
    rgba(255,255,255,.45) 50%,
    transparent 70%);
  transform:translateX(-130%);
  will-change:transform;
  animation:cta-shimmer 4.5s ease-in-out infinite;
}
.cta-play::after{
  /* Cover Play is the loudest attract-CTA — brighter sweep (.66 vs the
     shared .45) and a faster 3.6s cadence (vs 4.5s). The journey-end
     Discover button keeps the calmer shared default. */
  background:linear-gradient(115deg,
    transparent 30%,
    rgba(255,255,255,.66) 50%,
    transparent 70%);
  animation-duration:3.6s;
  animation-delay:2s;
}
.complete-card .cta-discover::after{animation-delay:3s}
@keyframes cta-shimmer{
  0%, 65%   { transform:translateX(-130%); }
  90%, 100% { transform:translateX( 130%); }
}

/* Quirky wiggle — fires right after the shimmer in the same 4.5s cycle.
   Same animation-delay as the shimmer keeps the two phase-locked: shimmer
   sweeps 65→90%, then the whole button shakes-and-pops in 86→100%. The
   delay also keeps the CSS animation from clobbering the GSAP entry tween,
   which finishes well before the first cycle starts. */
.cta-play,
.complete-card .cta-discover{
  will-change:transform;
  animation:cta-wiggle 4.5s ease-in-out infinite;
}
/* Faster 3.6s cadence for Play (the four attract animations stay phase-
   locked because they all share this duration + the 2s delay); Discover
   keeps the shared 4.5s. */
.cta-play{animation-delay:2s;animation-duration:3.6s}
.complete-card .cta-discover{animation-delay:3s}
@keyframes cta-wiggle{
  0%, 86%   { transform:translateY(0)    rotate(0deg)  scale(1); }
  88%       { transform:translateY(-2rem) rotate(-5deg) scale(1.04); }
  91%       { transform:translateY(-5rem) rotate( 7deg) scale(1.07); }
  94%       { transform:translateY(-2rem) rotate(-4deg) scale(1.04); }
  97%       { transform:translateY(-1rem) rotate( 2deg) scale(1.02); }
  100%      { transform:translateY(0)    rotate(0deg)  scale(1); }
}

/* Label glint — a warm amber shine sweeps left→right through the TEXT
   GLYPHS, then flashes the icon, on the armed Continue. (Removed from the
   cover Play button 2026-06-15 at user request: Play keeps only its white
   face shimmer + wiggle; its "Play" label and icon stay solid white.) The
   text fill is solid white/kohl at rest: the highlight band parks off-glyph
   at both ends of the cycle, so this is a moving highlight, not gradient-
   text styling. The label is always the first child of .cta-inner
   (cta-back puts its arrow first and is deliberately excluded).
   @supports-guarded — without background-clip:text the gradient would
   paint as a solid bar behind transparent text. */
@supports ((-webkit-background-clip: text) or (background-clip: text)){
  /* Continue is kohl-on-white: a saturated amber shine sweeping through
     the dark glyphs (linear, not ease-in-out, so one band crosses label
     and icon at constant speed without a stop-start at the handoff). */
  .cta-continue.is-active .cta-inner > span:first-child{
    background:linear-gradient(105deg,
      var(--kohl) 42%, #FFB733 47%, #E58F00 50%, #FFB733 53%, var(--kohl) 58%);
    background-size:250% 100%;
    background-position:115% 0;
    -webkit-background-clip:text;
            background-clip:text;
    color:transparent;
    animation:cta-text-glint 4.5s linear infinite;
  }
}
/* The armed Continue arrow takes the SAME band as its label — OVER the
   glyph, not behind it. The arrow span is masked to its SVG shape and
   painted with the same animated gradient the label uses (the old drop-
   shadow flash could only glow around an <img>'s alpha; it can't recolour
   the fill). The hidden img stays in the DOM as layout + no-mask fallback.
   Icon shares the label's clock (SAME delay) but runs cta-icon-sweep — a
   later, shorter window sized so the band crosses the small glyph at the
   same px/s the text sweep travels. WIDER band stops (30→70 vs the text's
   42→58) and a further-out parked position (122/−22) wash the whole glyph
   instead of catching a sliver. (Play's icon sweep was removed with its
   label glint — see above.) */
@supports ((-webkit-mask-image: url('')) or (mask-image: url(''))){
  /* All Continue arrows render as masked kohl ink (identical to the
     brightness(0) fallback at rest); only the ARMED button animates. */
  .cta-continue .arrow{
    -webkit-mask:url('../img/icon-arrow.svg') no-repeat center / contain;
            mask:url('../img/icon-arrow.svg') no-repeat center / contain;
    background:linear-gradient(105deg,
      var(--kohl) 30%, #FFB733 44%, #E58F00 50%, #FFB733 56%, var(--kohl) 70%);
    background-size:250% 100%;
    background-position:122% 0;
  }
  .cta-continue.is-active .cta-inner .arrow{
    animation:cta-icon-sweep 4.5s linear infinite;
  }
  /* Only the masked-gradient arrow hides its img; Play's icon img is
     visible (it IS the white play glyph, no mask over it anymore). */
  .cta-continue .arrow img{visibility:hidden}
}
/* Continue's label + arrow share ONE delay — continuity comes from the
   adjoining keyframe windows, not from staggered clocks. It first glints
   ~1.2s after arming so the activation flourish owns the opening beat.
   (Play's glint delays were removed with its label/icon sweep.) */
.cta-continue.is-active .cta-inner > span:first-child,
.cta-continue.is-active .cta-inner .arrow{animation-delay:1.2s}
@keyframes cta-text-glint{
  0%, 68%   { background-position:115% 0; }
  88%, 100% { background-position:-15% 0; }
}
/* Icon leg of the same sweep: opens at 80% — while the text band is still
   crossing its final letters — and runs to 90%, so the whole-glyph wash
   peaks (~85%) exactly as the label's sweep exits. Parked at 122/−22 to
   match the icon's wider band stops (at 115 the wide band would leak a
   sliver onto the glyph at rest). */
@keyframes cta-icon-sweep{
  0%, 80%   { background-position:122% 0; }
  90%, 100% { background-position:-22% 0; }
}

@media (prefers-reduced-motion: reduce){
  .cta-play::after,
  .complete-card .cta-discover::after{display:none}
  .cta-play,
  .complete-card .cta-discover{animation:none}
  .cta-continue::before{animation:none}
  .cta-continue.is-pulsing::after{animation:none}  /* content pulse — also JS-gated in content.js */
  /* Static colour glow, no orbit. */
  .cta-continue.is-active{animation:none}
  /* Glint off — the parked band renders as solid ink (white on Play,
     kohl arrows on Continue). !important so the hover/activating rules
     that COMPOSE the sweep into their animation lists (higher specificity,
     later in source) can't resurrect it under reduced motion. */
  .cta-play .cta-inner > span:first-child,
  .cta-continue.is-active .cta-inner > span:first-child,
  .cta-play .cta-inner .ico,
  .cta-continue.is-active .cta-inner .arrow{animation:none !important}
  .cta-play:hover,
  .cta-back:hover,
  .cta-continue:not(.is-inactive):hover,
  .complete-card .cta-discover:hover{animation:none;transform:translateY(-2rem) scale(1.03)}
  .cta-play:hover .ico,
  .cta-continue:hover .arrow{animation:none;transform:none}
  .cta-back:hover .arrow{animation:none;transform:scaleX(-1)}
  .fs-exit:hover{animation:none;transform:translateX(-50%) translateY(-2rem) scale(1.02)}
  .fs-exit:hover .ico{animation:none}
  .is-attention{animation:none !important}
  .choice.is-wrong,.choice.is-right,.choice.is-right .letter{animation:none !important}
  .choice:hover,.choice:hover .letter{animation:none !important}
  .choice.is-locked{animation:none !important}
  .module-personality .choice.is-chosen{animation:none !important}
}

/* Idle attention boing — added by app.js to the screen's primary CTA after
   ~30s with no input. A bigger, louder boing than the hover one; meant to
   pull a fresh visitor's eye on a kiosk where children rotate through.
   `!important` overrides the idle wiggles on cta-play / cta-discover. */
.is-attention{
  animation:attention-boing 900ms var(--ease-boing) !important;
  z-index:6;
}
@keyframes attention-boing{
  0%, 100% { transform:translateY(0)     rotate(0)     scale(1); }
  20%      { transform:translateY(4rem)   rotate(-3deg) scale(1.06,.92); }
  50%      { transform:translateY(-22rem) rotate(7deg)  scale(.92,1.10); }
  75%      { transform:translateY(-6rem)  rotate(-4deg) scale(1.05); }
}

/* Confetti — pinned full-stage canvas, drawn by BhuriConfetti
   (assets/js/confetti.js) via spawnConfetti() in journey-helpers.js.
   z-index above modules + overlays but below the loading screen so
   confetti can spill across journey-end, feedback modal, and slider
   commits without per-host clipping concerns. pointer-events:none so
   the canvas is invisible to interaction. */
.confetti-canvas{
  position:absolute;inset:0;width:100%;height:100%;
  pointer-events:none;z-index:100;
}

/* Lottie celebration plays (lottie-confetti.js) — same pinned z-plane
   as the canvas; each play box is centred on its burst origin.
   lottie-web sizes its own canvas; the !important pins it to the box
   under the CMS preview's scaled stage. */
.confetti-lottie-layer{
  position:absolute;inset:0;
  pointer-events:none;z-index:100;overflow:hidden;
}
.confetti-lottie-play{position:absolute;transform:translate(-50%,-50%);}
/* Full-stage plays — the cascade comp is 1920×1080, same shape as the
   stage, so it simply covers the layer. */
.confetti-lottie-play.is-stage{inset:0;width:100%;height:100%;transform:none;}
.confetti-lottie-play canvas{display:block;width:100%!important;height:100%!important;}

/* paper texture, rotated 180°, low opacity, mix-blend color over white */
.paper-bg{
  position:absolute;left:0;top:-130rem;width:1920rem;height:1280rem;
  transform:rotate(180deg);pointer-events:none;z-index:0;
}
/* paper-bg sits at z-index:0 over a white .screen-container, so multiply
   blends against white = identical to plain alpha. Drop the blend mode —
   mix-blend-mode forces an offscreen compositing pass every frame. */
.paper-bg img{width:100%;height:100%;object-fit:cover;opacity:.42}

/* ─────────────────────────────────────────
   Header + footer (used by content / mcq / slider modules)
   ───────────────────────────────────────── */
.activity-header{
  position:absolute;top:0;left:0;width:1920rem;height:64rem;
  z-index:5;
}
.btn-close{
  /* Single source of truth for close-button placement across every module
     (cover / content / mcq / slider). Whether it sits inside an
     .activity-header at flex-position 0 or as a direct child of the
     module section, both contexts have their top edge at section y=0,
     so the same top:16 lands the button identically on screen. z-index:2
     keeps it above any positioned flex item (e.g. the cover-frame). */
  position:absolute;top:16rem;left:48rem;width:52rem;height:52rem;
  background:#fff;border-radius:24rem;
  /* Kohl-ink hairline (2rem so the thin line keeps a saturated core
     after anti-aliasing — 1.5rem/.6 measured only 2.8:1 on cream when
     rendered). The white chip is 1.0–1.2:1 against light grounds (fails
     WCAG 1.4.11), so the soft shadow alone can't hold the edge on cream /
     white artwork; this border gives a shadow-independent edge regardless
     of the painting behind it. Where the chip sits on a colourful Bhil
     border, white-vs-colour already carries the edge. border-box keeps
     the chip 52×52 (content insets 2rem). Thickness matches .fs-exit. */
  border:2rem solid rgba(20,20,31,.7);
  filter:drop-shadow(0 3rem 8rem rgba(0,0,0,.22));
  display:grid;place-items:center;
  z-index:2;
  transition:transform var(--t-fast) var(--ease), filter var(--t-fast) var(--ease);
}
/* 2026-06-12: nav buttons flipped to black-on-white so chrome joins the
   white-card surface language instead of out-shouting the artwork. The X
   asset ships WHITE inside an <img> (fill is baked in — page CSS can't
   reach it), so the chip inverts it to ink with the same brightness(0)
   trick the Continue arrow already uses. ONE look everywhere — the
   brief dark-variant exception for fullscreen scenes was removed at
   user direction; the chip's drop-shadow carries the edge over light
   artwork. */
.btn-close img{width:21rem;height:21rem;object-fit:contain;filter:brightness(0)}
.btn-close:hover{transform:scale(1.1) rotate(90deg);filter:drop-shadow(0 8rem 20rem rgba(0,0,0,.28))}
.btn-close:active{transform:scale(.95) rotate(90deg)}

.header-title{
  /* Left-aligned, anchored just right of the close button with a 24rem gap
     (button spans left:48 → 48+52 = 100rem). The previous centre-then-shift
     trick (left:50% + translateX(calc(-50% - 671rem))) was calibrated for a
     short title and slid left into the close button once a title ran long. */
  position:absolute;left:124rem;top:20rem;
  font:700 20rem/1 "Gotham Rounded","Nunito",sans-serif;color:var(--kohl);
  white-space:nowrap;
}
.progress-wrap{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  display:flex;align-items:center;gap:14rem;padding:12rem 20rem;
}
.progress-current{
  font:700 20rem/1 "Gotham Rounded","Nunito",sans-serif;color:var(--aamras);
  white-space:nowrap;
}
.progress-divider{width:2rem;height:20rem;background:rgba(255,163,0,.32);border-radius:1rem}
.progress-dots{display:flex;align-items:center;gap:6rem;height:10rem}
.progress-dots .dot{
  width:10rem;height:10rem;border-radius:999rem;background:#E1DBCF;
  transition:background var(--t-fast) var(--ease), transform var(--t-fast) var(--ease);
}
.progress-dots .dot.is-done{background:var(--aamras)}
.progress-dots .dot.is-current{
  background:var(--gulabi);transform:scale(1.15);
  box-shadow:0 0 0 3rem rgba(208,0,111,.18);
}

/* Score chip — sits on the right side of the activity header, only when
   journey.scoring.enabled is true. Mirrors the progress-wrap visual weight
   so the two chips read as siblings. */
.score-chip{
  position:absolute;right:48rem;top:50%;transform:translateY(-50%);
  display:inline-flex;align-items:center;gap:8rem;
  background:rgba(255,163,0,.12);border:2rem solid var(--aamras);
  border-radius:999rem;padding:8rem 16rem;
}
.score-chip-label{
  font:700 14rem/1 "Gotham Rounded","Nunito",sans-serif;
  color:var(--aamras);letter-spacing:.6rem;text-transform:uppercase;
}
.score-chip-value{
  font:700 22rem/1 "Gotham Rounded","Nunito",sans-serif;color:var(--kohl);
  white-space:nowrap;
}

/* ─────────────────────────────────────────
   Activity countdown timer (CMS opt-in: timerEnabled / timerSeconds)
   The chip shares the header's right side with the score chip, so the two
   sit in one flex wrapper; alone, the score chip keeps its old geometry
   via the static override below. The centre-stage intro card is built by
   assets/js/activity-timer.js, which then flies the digits into the chip.
   ───────────────────────────────────────── */
.header-right{
  position:absolute;right:48rem;top:50%;transform:translateY(-50%);
  display:flex;align-items:center;gap:12rem;
}
.header-right .score-chip{position:static;top:auto;right:auto;transform:none}

.timer-chip{
  display:inline-flex;align-items:center;gap:8rem;
  background:rgba(255,163,0,.12);border:2rem solid var(--aamras);
  border-radius:999rem;padding:8rem 16rem;
}
/* Waiting = invisible but still holding its slot, so nothing in the header
   shifts when the countdown docks. */
.timer-chip.is-waiting{visibility:hidden}
.timer-chip svg{width:22rem;height:22rem;display:block;color:var(--aamras)}
.timer-chip .tc-time{
  font:700 22rem/1 "Gotham Rounded","Nunito",sans-serif;color:var(--kohl);
  font-variant-numeric:tabular-nums;text-align:center;white-space:nowrap;
}
/* Last ten seconds — the chip turns lal and breathes. Transform-only
   (never blur/filter on anything that animates). */
.timer-chip.is-low{
  border-color:var(--lal);background:rgba(213,0,50,.10);
  animation:timer-low-pulse 1s var(--ease) infinite;
}
.timer-chip.is-low svg{color:var(--lal)}
.timer-chip.is-low .tc-time{color:var(--lal)}
@keyframes timer-low-pulse{
  0%,100%{transform:scale(1)}
  50%{transform:scale(1.06)}
}
/* 0:00 — one headshake, then it just sits red. The timer paces, it never
   blocks: nothing advances, nothing is taken away. */
.timer-chip.is-up{
  border-color:var(--lal);background:rgba(213,0,50,.10);
  animation:timer-up-wiggle .55s var(--ease) 1;
}
.timer-chip.is-up svg{color:var(--lal)}
.timer-chip.is-up .tc-time{color:var(--lal)}
@keyframes timer-up-wiggle{
  0%,100%{transform:rotate(0)}
  20%{transform:rotate(-5deg)}
  45%{transform:rotate(4deg)}
  70%{transform:rotate(-2deg)}
}
/* Activity resolved (feedback overlay up) — the clock recedes, frozen. */
.timer-chip.is-done{opacity:.55;transition:opacity var(--t-med) var(--ease)}

/* Centre-stage intro — same interstitial family as the idle-reset card:
   dark scrim, white card, then the digits fly up into the header chip.
   z-index 8: above the module chrome (5), below overlays (10). */
.timer-stage{position:absolute;inset:0;z-index:8;display:grid;place-items:center}
.timer-stage-scrim{position:absolute;inset:0;background:rgba(20,20,31,.5)}
.timer-stage-card{
  position:relative;display:flex;flex-direction:column;align-items:center;gap:4rem;
  background:#fff;border-radius:24rem;box-shadow:var(--sh-cta);
  padding:40rem 80rem 32rem;
}
.timer-stage-title{
  font:700 34rem/1.2 "Gotham Rounded","Nunito",sans-serif;color:var(--kohl);
  text-wrap:balance;text-align:center;
}
.timer-stage-time{
  font:700 124rem/1.05 "Gotham Rounded","Nunito",sans-serif;color:var(--aamras);
  font-variant-numeric:tabular-nums;text-align:center;
}
.timer-stage-time.is-low,
.timer-stage-time.is-up{color:var(--lal)}

@media (prefers-reduced-motion: reduce){
  .timer-chip.is-low,
  .timer-chip.is-up{animation:none}
}

.activity-footer{
  position:absolute;left:0;bottom:0;width:1920rem;
  display:flex;align-items:center;justify-content:space-between;
  padding:16rem 52rem;gap:16rem;z-index:5;
}
.footer-left{display:flex;align-items:center;gap:16rem}

/* CTA inner wrapper — every magnetic CTA wraps its content in
   <span class="cta-inner"> so the boing keyframe can animate the inner
   while magnetic.js writes inline transform on the outer button. The
   outer button used to set gap between icon + label; we replicate it
   here. .cta-secondary uses 10rem, the rest 8rem. */
.cta-back .cta-inner,
.cta-continue .cta-inner,
.cta-play .cta-inner{
  display:inline-flex;align-items:center;gap:8rem;
  /* Press feedback lives on the inner (see the :active rules) because the
     outer is magnetic — its inline transform would override any :active
     transform there, which is why mouse users never saw the old press.
     The transition doesn't fight the hover/activation keyframes: running
     animations beat transitioned values for the same property. */
  transition:transform var(--t-fast) var(--ease);
}
.complete-card .cta-discover .cta-inner,
.complete-card .cta-secondary .cta-inner{
  display:inline-flex;align-items:center;gap:10rem;
  transition:transform var(--t-fast) var(--ease);
}

/* While magnetic is tracking the cursor we suppress competing
   transform animations on the outer button (the idle cta-wiggle
   would overwrite the inline magnetic transform every frame) and
   short-circuit the transform transition so tracking is frame-perfect.
   On pointerleave the class lifts and is-returning takes over,
   springing the button back to centre with --ease-boing (overshoot). */
.cta-play.is-tracking,
.complete-card .cta-discover.is-tracking{
  animation:none !important;
}
.cta-back.is-tracking,
.cta-continue.is-tracking,
.cta-play.is-tracking,
.complete-card .cta-discover.is-tracking,
.complete-card .cta-secondary.is-tracking{
  transition-property:filter,box-shadow,background;
}

/* Spring-back when the cursor leaves. magnetic.js adds is-returning the
   same frame it clears the inline transform; --ease-boing
   (cubic-bezier(.34,1.56,.64,1)) overshoots ~0 then settles, giving
   a genuine bounce rather than a flat slide. Duration matches the
   hover boings (520ms) so the family of motions stays consistent.
   Targets [data-magnetic] so every magnetic CTA inherits without
   per-variant selectors; specificity (0,2,0) beats the single-class
   base transitions. transitionend on transform clears the class. */
[data-magnetic].is-returning{
  transition:transform 520ms var(--ease-boing),
             filter      var(--t-fast) var(--ease),
             box-shadow  var(--t-fast) var(--ease),
             background  var(--t-fast) var(--ease);
}

.cta-back{
  display:inline-flex;align-items:center;gap:8rem;
  background:#fff;color:var(--kohl);border-radius:24rem;
  /* Same kohl-ink hairline as .btn-close — see that rule for the
     contrast rationale. The wider white pill has more area to dissolve
     on light grounds, so the defined edge matters most here. */
  border:2rem solid rgba(20,20,31,.7);
  padding:12rem 22rem;box-shadow:var(--sh-cta);
  font:700 24rem/1 "Gotham Rounded","Nunito",sans-serif;letter-spacing:.3rem;
  transition:transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
/* Hover boing now plays on the inner wrapper so the outer button is
   free for magnetic translation (data-magnetic on the button →
   inline transform set by magnetic.js). Boing keyframes still return
   to rest at 100%, so a mid-flight mouseout cancels cleanly. */
.cta-back:hover{
  box-shadow:0 14rem 30rem rgba(0,0,0,.32);
}
.cta-back:hover .cta-inner{
  animation:cta-back-boing 520ms var(--ease-boing);
}
/* Press on the inner — animation:none interrupts a mid-flight hover boing
   so the press always reads instantly. */
.cta-back:active .cta-inner{animation:none;transform:scale(.96)}
.cta-back .arrow{width:34rem;height:28rem;transform:scaleX(-1);transition:transform var(--t-fast) var(--ease)}
.cta-back .arrow img{width:100%;height:100%;object-fit:contain;filter:brightness(0)}
.cta-back:hover .arrow{animation:cta-back-arrow 520ms var(--ease-boing)}
@keyframes cta-back-boing{
  0%, 100% { transform:translate(0,0)        rotate(0)     scale(1); }
  30%      { transform:translate(2rem,-2rem)   rotate(3deg)  scale(1.02); }
  60%      { transform:translate(-3rem,-8rem)  rotate(-6deg) scale(1.07); }
}
@keyframes cta-back-arrow{
  0%, 100% { transform:scaleX(-1) translate(0,0); }
  60%      { transform:scaleX(-1) translate(-9rem,-2rem); }
}

.cta-continue{
  display:inline-flex;align-items:center;gap:8rem;
  /* Light primary — white pill, kohl label + inked-black arrow. Pairs
     against the dark Back pill, and the brand ring/aura read stronger on
     white. Armed vs waiting is carried by ring + aura + padlock→arrow
     (both states share the white fill; see .is-inactive). */
  background:var(--paper);color:var(--kohl);border-radius:24rem;
  padding:12rem 22rem;box-shadow:var(--sh-cta);
  font:700 24rem/1 "Gotham Rounded","Nunito",sans-serif;letter-spacing:.3rem;
  position:relative;isolation:isolate;
  transition:transform var(--t-fast) var(--ease), opacity var(--t-med) var(--ease),
             box-shadow var(--t-fast) var(--ease), background var(--t-fast) var(--ease),
             color var(--t-fast) var(--ease);
}
@property --cta-angle{ syntax:'<angle>'; initial-value:0deg; inherits:false; }
/* ── Active border-glow: the MAP palette travels CLOCKWISE around the button's
   2rem border. A conic gradient (its angle driven by --cta-angle) is confined
   to a thin ring by the mask-composite "border" trick, so the colours sweep
   cleanly around the perimeter of the wide pill. The soft halo is done with
   directional colour box-shadows on .is-active (transform-safe — unlike a
   z-index:-1 pseudo, which the magnetic / press transforms would break). */
.cta-continue::before{
  /* inset:-1rem (+ fully-round radius) so the gradient overshoots the pill edge
     and fully covers it — otherwise the dark face peeks through as a hairline
     ring just outside the masked border. */
  content:'';position:absolute;inset:-1rem;box-sizing:border-box;
  /* 4rem ring (was 3) — from kiosk distance the thin edge compressed to a
     fringe; one extra rem keeps it jewelry, not noise. */
  border-radius:999rem;pointer-events:none;padding:4rem;
  /* Brand stops, saturated through the warm→cool run, but the teal→amber
     seam closes through the pale tints — interpolating #5CB8B2→#FFA300
     directly passes through muddy olive (RGB midpoint ≈ #ADAD59), which
     read as a dirty band on the spinning ring. The pale bridge melts into
     the white pill edge instead. */
  background:conic-gradient(from var(--cta-angle),
    #FFA300,#FF6FB1,#D0006F,#9B5FC0,#5CB8B2,#A6E0DB,#FFD17F,#FFA300);
  -webkit-mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
  -webkit-mask-composite:xor;
          mask:linear-gradient(#000 0 0) content-box, linear-gradient(#000 0 0);
          mask-composite:exclude;
  opacity:0;transition:opacity .35s ease-in-out;
  animation:cta-border-spin 4s linear infinite;
}
/* Paused while the button is disabled — animating the @property angle
   repaints the conic every frame even at opacity:0. (During the 350ms
   deactivation fade the ring freezes; imperceptible at that opacity.) */
.cta-continue.is-inactive::before{animation-play-state:paused}
/* Thin but full-strength so the bright colours read cleanly (not muddied by
   blending with the dark face at lower opacity). The .2s delay sequences
   activation: the choice's lock-in keyframe owns the first beat, then the
   ring blooms as attention lands on the gate (activateContinue's
   .is-activating pop fires on the same beat). Deactivation drops the rule,
   so the fade-out starts immediately. */
.cta-continue.is-active::before{ opacity:1; transition-delay:.2s; }
@keyframes cta-border-spin{ to{ --cta-angle:360deg; } }
/* Pulse ring — a uiverse-style "tap me" ripple (re-voiced from an outline
   ripple to a soft box-shadow halo, on-brand). An expanding ring blooms
   outward from the pill edge and fades, looping. Added ONLY by the content
   module (journey-end/cover excluded; MCQ/personality are tap-to-answer,
   slider is gated) via the .is-pulsing class, 5s after the screen settles —
   a gentle nudge to advance a passive read screen. The pill has no
   overflow:hidden, so the ring escapes the box; ::after is otherwise unused
   here (::before is the conic ring). The hue comes from --pulse-rgb, which
   content.js advances one brand colour per loop (the bloom sits at 6%, not
   0%, so the loop-boundary colour swap happens while the ring is invisible —
   no flash). */
.cta-continue.is-pulsing{ --pulse-rgb:255,163,0; }   /* fallback; content.js cycles it */
.cta-continue.is-pulsing::after{
  content:'';position:absolute;inset:0;border-radius:inherit;
  pointer-events:none;
  animation:cta-continue-pulse 1.6s ease-out infinite;
}
@keyframes cta-continue-pulse{
  0%, 100% { box-shadow:0 0 0 0    rgba(var(--pulse-rgb),0); }
  6%       { box-shadow:0 0 0 0    rgba(var(--pulse-rgb),.6); }
  55%      { box-shadow:0 0 0 16rem rgba(var(--pulse-rgb),.16); }
  80%      { box-shadow:0 0 0 24rem rgba(var(--pulse-rgb),0); }
}
/* Hover boing on the inner wrapper — see cta-back note. Outer button
   handles the magnetic translation; inner does the lean + scale. */
.cta-continue:not(.is-inactive):hover{
  background:#FFF6E8;   /* warm cream lift; the is-active glow stays */
}
.cta-continue:not(.is-inactive):hover .cta-inner{
  animation:cta-continue-boing 520ms var(--ease-boing);
}
/* Press on the inner — outer stays free for magnetic (see .cta-inner note).
   animation:none on BOTH levels interrupts a mid-flight hover boing or the
   shell's activation pop so the press always reads instantly. */
.cta-continue:not(.is-inactive):active{animation:none}
.cta-continue:not(.is-inactive):active .cta-inner{animation:none;transform:scale(.96)}
.cta-continue .arrow{width:34rem;height:28rem;transition:transform var(--t-fast) var(--ease)}
/* The arrow SVG ships white — brightness(0) re-inks it black for the
   white pill. This img is only the no-mask-support fallback: where CSS
   masks work (the glint section), the img is hidden and the glyph is
   painted as a masked gradient on the span instead, so the label's shine
   band can sweep THROUGH the arrow itself. */
.cta-continue .arrow img{width:100%;height:100%;object-fit:contain;filter:brightness(0)}
/* ⚠ Any rule that touches this element's `animation` MUST keep
   cta-icon-sweep in the list with IDENTICAL coordinates (4.5s linear 1.2s
   infinite) — an animation survives a property change only if its name
   persists; dropping it here restarted the icon's glint clock on hover
   and permanently desynced it from the label's sweep. */
.cta-continue:not(.is-inactive):hover .arrow{
  animation:cta-continue-arrow 520ms var(--ease-boing),
            cta-icon-sweep 4.5s linear 1.2s infinite;
}
/* Inactive Continue — "waiting", not "broken". An UNFILLED pill: paper
   surface, cream stroke ring, full-contrast kohl label and a padlock in
   the icon slot. It rhymes with the unpicked choice cards (white + stroke
   + soft shadow), so the system reads "pick one and I fill in" — and the
   old 35%-opacity ghost stopped inverting the footer hierarchy (the solid
   Back used to be the most button-looking thing on a fresh question).
   Still CLICKABLE — a tap gets a teaching nudge (head-shake + padlock
   rattle + hint-pill boing via JourneyHelpers.nudgeSubmit) instead of dead
   silence. Hover/active styling is gated off above so the waiting button
   doesn't invite, and magnetic.js skips .is-inactive elements. */
.cta-continue.is-inactive{
  background:var(--paper);
  color:var(--kohl);
  box-shadow:
    inset 0 0 0 2rem var(--stroke),
    0 4rem 7rem rgba(0,0,0,.08);
}
/* Soft glow — one offset colour box-shadow per corner blends into a gentle
   multicolour aura; a dark drop-shadow grounds the chip over lighter images. */
.cta-continue.is-active{
  opacity:1;pointer-events:auto;
  /* The "shadow" IS the ring's colours — no black drop shadow. TWO glows
     at opposite poles (warm + cool) orbit the pill via cta-aura-spin,
     same 4s clockwise period as the conic ring. Two separated pools
     instead of four concentric corners: the old corner set sat ±6rem
     under 24rem blur, so pink/teal/purple/amber all overlapped and mixed
     into grey-mauve mud. Opposite poles never meet, so the halo stays
     saturated. Box-shadows rather than a blurred z-index:-1 conic copy,
     because a transformed button (magnetic / press / activation pop)
     becomes a stacking context and a negative-z pseudo would jump ON TOP
     of the white face. The static declaration equals the keyframe's 0%
     pose, so the .2s entrance handoff and any animation interruption
     (press sets animation:none) land on the same look. */
  /* Elliptical orbit radii (±12 x / ±7 y): the pill is wide and short, so
     a vertically-offset glow shows along the WHOLE long edges while a
     horizontal one only peeks past the end caps — equal radii read as
     much heavier above/below the button. */
  box-shadow:
    0 -7rem 12rem -4rem rgba(255,163,0,.50),
    0  7rem 12rem -4rem rgba(92,184,178,.50);
  /* Delay only the glow (3rd transition = box-shadow): the waiting
     stroke-ring hands off to the colours on the sequenced .2s beat,
     together with the ring bloom, shell pop and padlock→arrow swap. The
     orbit starts at .4s, once that handoff has finished. */
  transition-delay:0s,0s,.2s,0s,0s;
  animation:cta-aura-spin 4s linear .4s infinite;
}
/* Two glow pools trace a circle around the pill (offsets at 45° steps,
   clockwise, matching the ring), always 180° apart. The warm pole
   breathes amber→pink→gulabi→back; the cool pole teal→soft-purple→back.
   Position AND colour interpolate between steps, so the halo slides
   smoothly with no grey mixing zone. */
@keyframes cta-aura-spin{
  0%{
    box-shadow:
      0 -7rem 12rem -4rem rgba(255,163,0,.50),
      0  7rem 12rem -4rem rgba(92,184,178,.50);
  }
  12.5%{
    box-shadow:
       8.5rem -5rem 12rem -4rem rgba(255,163,0,.50),
      -8.5rem  5rem 12rem -4rem rgba(92,184,178,.50);
  }
  25%{
    box-shadow:
       12rem 0 12rem -4rem rgba(255,111,177,.50),
      -12rem 0 12rem -4rem rgba(92,184,178,.50);
  }
  37.5%{
    box-shadow:
       8.5rem  5rem 12rem -4rem rgba(255,111,177,.50),
      -8.5rem -5rem 12rem -4rem rgba(92,184,178,.50);
  }
  50%{
    box-shadow:
      0  7rem 12rem -4rem rgba(208,0,111,.50),
      0 -7rem 12rem -4rem rgba(155,95,192,.50);
  }
  62.5%{
    box-shadow:
      -8.5rem  5rem 12rem -4rem rgba(208,0,111,.50),
       8.5rem -5rem 12rem -4rem rgba(155,95,192,.50);
  }
  75%{
    box-shadow:
      -12rem 0 12rem -4rem rgba(255,111,177,.50),
       12rem 0 12rem -4rem rgba(155,95,192,.50);
  }
  87.5%{
    box-shadow:
      -8.5rem -5rem 12rem -4rem rgba(255,163,0,.50),
       8.5rem  5rem 12rem -4rem rgba(92,184,178,.50);
  }
  100%{
    box-shadow:
      0 -7rem 12rem -4rem rgba(255,163,0,.50),
      0  7rem 12rem -4rem rgba(92,184,178,.50);
  }
}
@keyframes cta-continue-boing{
  0%, 100% { transform:translateY(0)    rotate(0)     scale(1); }
  30%      { transform:translateY(-2rem) rotate(-2deg) scale(1.02); }
  60%      { transform:translateY(-9rem) rotate(5deg)  scale(1.07); }
}
/* Activation flourish — fired by activateContinue ~0.2s after the gate
   unlocks (class .is-activating, restartable). Plays on the SHELL so the
   whole object — fill, ring, aura — pops as one (animating the inner made
   the label swell inside a static pill, which read as text jumping). A CSS
   animation outranks magnetic's inline transform for its 480ms run, and
   the kiosk is touch anyway. The overshoot matches the choice lock-in's
   family so "locked" and "unlocked" speak one language; the arrow reuses
   the hover nudge as a one-shot "go" while it crossfades in from the
   padlock handoff (see .gate-ico below). */
.cta-continue.is-activating{
  animation:cta-activate-pop 480ms var(--ease-boing);
}
/* is-activating and is-active share the button's single animation
   property — list both during the flourish, or whichever rule loses the
   cascade would silently kill the other (pop without aura, or aura
   without pop). */
.cta-continue.is-active.is-activating{
  animation:cta-activate-pop 480ms var(--ease-boing),
            cta-aura-spin 4s linear .4s infinite;
}
/* Specificity (0,5,0) deliberately outguns the glint rule (0,4,0) so the
   arrival nudge plays during the flourish — COMPOSED with the sweep
   (same coordinates) so the glint's clock isn't restarted when
   .is-activating lifts; replacing the list outright left the slider
   gate's icon permanently ~0.9s behind its label. */
.cta-continue.is-active.is-activating .cta-inner .arrow{
  animation:cta-continue-arrow 520ms var(--ease-boing),
            cta-icon-sweep 4.5s linear 1.2s infinite;
}
@keyframes cta-activate-pop{
  0%, 100% { transform:translateY(0)     scale(1); }
  35%      { transform:translateY(-6rem) scale(1.06); }
  65%      { transform:translateY(-2rem) scale(1.005); }
}

/* ── Gate icon: padlock ↔ arrow ──
   ctaSubmit buttons stack both icons in a fixed 34×28 slot; the state
   classes crossfade them so the gate reads pictographically — a pre-reader
   sees LOCKED, then the shackle springs open and the lock hands off to the
   go-arrow on the sequenced activation beat. ctaContinue / feedback-overlay
   buttons keep their plain .arrow markup and never enter these states. */
.cta-continue .gate-ico{
  position:relative;width:34rem;height:28rem;
  display:inline-block;flex:0 0 auto;
}
.cta-continue .gate-ico .arrow,
.cta-continue .gate-ico .lock{
  position:absolute;inset:0;
  display:flex;align-items:center;justify-content:center;
  transition:opacity .2s var(--ease), transform .25s var(--ease-boing);
}
.cta-continue .gate-ico .lock svg{height:26rem;width:auto;display:block}
.cta-continue .gate-ico .lock svg .lock-shackle{
  transform-box:fill-box;transform-origin:15% 100%;
  transition:transform .18s var(--ease-boing);
}
/* Locked: the padlock owns the slot. */
.cta-continue.is-inactive .gate-ico .arrow{opacity:0;transform:scale(.5)}
/* Armed: shackle springs open on the .2s beat, the lock hands off, and the
   arrow boings in mid-crossfade (its is-activating nudge is already
   running, so it arrives in motion). Deactivation drops these delayed
   rules entirely, so the lock snaps straight back. */
.cta-continue.is-active .gate-ico .lock svg .lock-shackle{
  transform:rotate(-28deg);
  transition-delay:.2s;
}
.cta-continue.is-active .gate-ico .lock{
  opacity:0;transform:scale(.6) rotate(-14deg);
  transition-delay:.34s,.34s;
}
.cta-continue.is-active .gate-ico .arrow{
  opacity:1;transform:scale(1);
  transition-delay:.3s,.3s;
}
/* Dead-tap protest — pairs with nudgeSubmit's head-shake so the "locked"
   metaphor itself carries the explanation. */
.cta-continue.is-nudging .gate-ico .lock{animation:lock-shake 380ms ease-in-out}
@keyframes lock-shake{
  0%, 100% { transform:rotate(0); }
  20%      { transform:rotate(-9deg); }
  45%      { transform:rotate(8deg); }
  70%      { transform:rotate(-5deg); }
}
@keyframes cta-continue-arrow{
  0%, 100% { transform:translate(0,0)       scale(1); }
  60%      { transform:translate(10rem,-3rem) scale(1.18); }
}

.hint-pill{
  display:flex;align-items:center;gap:10rem;
  background:var(--bg-sec);border-radius:999rem;padding:10rem 20rem 10rem 16rem;
}
.hint-pill .li{
  width:24rem;height:24rem;border-radius:999rem;background:var(--aamras);
  display:grid;place-items:center;flex:0 0 auto;
}
.hint-pill .li img{width:6rem;height:14rem;object-fit:contain}
.hint-pill .text{
  font:500 20rem/25rem "Gotham Rounded","Nunito",sans-serif;color:var(--ink);
  white-space:nowrap;
}
.hint-pill .text em{color:var(--lal);font-style:normal;font-weight:700}

/* ─────────────────────────────────────────
   COVER (1:1015)
   ───────────────────────────────────────── */
/* Cover layout — flex column so the footer always claims its content
   height first and the frame fills the remaining vertical space. The
   image hugs its natural aspect, capped at 1472rem wide and the
   currently-available frame height (less padding). This guarantees the
   24rem gap between frame and footer regardless of how long the cover
   description happens to be. Earlier absolute positioning at top:24rem
   plus a hard-coded 1472×828 image broke down once the description
   wrapped to a third line and the footer grew taller. */
.module-cover{
  position:absolute;inset:0;
  display:flex;flex-direction:column;
}
/* .btn-close override removed — the universal rule (top:16, left:48,
   z-index:2) lands at the same screen coordinate here as in the
   activity-header modules. */
.cover-frame{
  flex:1 1 auto;min-height:0;
  margin:24rem 0;
  align-self:center;            /* hug the image instead of stretching to 1920 */
  /* De-matted (2026-06-12): white mat (#fff + 16rem padding) removed; the
     drop-shadow now traces the bare artwork edge. */
  filter:drop-shadow(0 8rem 16rem rgba(0,0,0,.12));
  display:flex;align-items:center;justify-content:center;
  /* Sit above .paper-bg (z:0). Without explicit positioning the flex items
     paint *under* the absolute paper texture per CSS painting order, which
     is what was tinting the pink footer through 42%-opacity paper. */
  position:relative;z-index:1;
}
.cover-frame img{
  display:block;
  max-width:1472rem;
  max-height:100%;
  width:auto;height:auto;
  background:#FFF8EE;
}

.cover-footer{
  flex:0 0 auto;
  background:var(--gulabi);color:#fff;
  display:flex;align-items:flex-end;justify-content:space-between;
  padding:40rem 48rem;gap:48rem;
  /* Same as .cover-frame — must sit above the paper-bg (z:0) so the
     gulabi pink reads at full saturation instead of the paper texture
     bleeding through at 42% opacity. */
  position:relative;z-index:1;
}
/* Copy takes everything left of Play (basis 0 so a long single-line desc
   can never crush the button); was a fixed 1262rem, which left ~360rem of
   the 1920rem band unused. */
.cover-footer .copy{display:flex;flex-direction:column;gap:12rem;flex:1 1 0;min-width:0}
.cover-footer h1{font:700 32rem/1.1 "Gotham Rounded","Nunito",sans-serif;margin:0}
.cover-footer .desc{font:700 24rem/1.25 "Gotham Rounded","Nunito",sans-serif;text-wrap:pretty}

/* ── Cover layouts (mirror the content module's three) ──
   The rules above are the LANDSCAPE default (image above a gulabi band).
   Portrait + fullscreen re-arrange the SAME .cover-frame + .cover-footer
   markup, so cover.js only swaps the layout class and enter() is unchanged. */
/* Portrait — image column | gulabi title panel, 50 / 50. */
.module-cover.layout-portrait{flex-direction:row}
.module-cover.layout-portrait .cover-frame{
  flex:1 1 0;min-width:0;margin:0;padding:56rem;
}
.module-cover.layout-portrait .cover-frame img{max-width:100%}
.module-cover.layout-portrait .cover-footer{
  flex:1 1 0;min-width:0;
  flex-direction:column;align-items:flex-start;justify-content:center;
  gap:40rem;padding:80rem 64rem;
}
/* Column direction here — flex-grow would stretch the copy vertically and
   shove the CTA to the panel foot, so pin it back to content height. */
.module-cover.layout-portrait .cover-footer .copy{flex:0 0 auto}
/* Fullscreen — image fills; title + Play ride a floating gulabi band. */
.module-cover.layout-fullscreen .cover-frame{
  position:absolute;inset:0;margin:0;filter:none;align-self:stretch;
}
.module-cover.layout-fullscreen .cover-frame img{
  width:100%;height:100%;object-fit:contain;
  max-width:none;max-height:none;background:transparent;
}
.module-cover.layout-fullscreen .cover-footer{
  position:absolute;left:0;right:0;bottom:0;z-index:2;
}

.cta-play{
  display:inline-flex;align-items:center;gap:8rem;
  background:var(--black);color:#fff;border-radius:24rem;
  padding:16rem 32rem;
  font:700 24rem/1 "Gotham Rounded","Nunito",sans-serif;
  transition:transform var(--t-fast) var(--ease),
             filter var(--t-fast) var(--ease),
             box-shadow var(--t-fast) var(--ease);
  position:relative;overflow:hidden;
}
/* Hover boing on the inner wrapper — outer is magnetic. The idle
   cta-wiggle lives on the button itself; we suppress it via the
   .is-tracking class added by magnetic.js, so the magnetic translate
   isn't fighting the wiggle's transform. */
.cta-play:hover{
  filter:brightness(1.15);
  box-shadow:0 18rem 36rem rgba(0,0,0,.32),0 6rem 16rem rgba(0,0,0,.18);
}
.cta-play:hover .cta-inner{
  animation:cta-play-boing 620ms var(--ease-boing);
}
.cta-play:active{filter:brightness(.95)}
.cta-play .ico{width:31rem;height:31rem;transition:transform var(--t-fast) var(--ease)}
.cta-play .ico img{width:100%;height:100%;object-fit:contain}
/* Icon boing on hover. (The amber glint sweep that used to compose in
   here was removed with Play's idle glint — 2026-06-15.) */
.cta-play:hover .ico{
  animation:cta-play-icon 620ms var(--ease-boing);
}
@keyframes cta-play-boing{
  0%, 100% { transform:translateY(0)     rotate(0)     scale(1,1); }
  18%      { transform:translateY(3rem)   rotate(-3deg) scale(1.06,.92); }
  42%      { transform:translateY(-12rem) rotate(5deg)  scale(.96,1.08); }
  72%      { transform:translateY(-3rem)  rotate(-3deg) scale(1.04,.98); }
}
@keyframes cta-play-icon{
  0%, 100% { transform:rotate(0)      scale(1); }
  45%      { transform:rotate(-22deg) scale(1.32); }
  75%      { transform:rotate(10deg)  scale(1.18); }
}

/* ─────────────────────────────────────────
   JOURNEY SELECT — kiosk landing
   App-level screen (not a journey module): one card per published
   journey, shown on boot with no ?journey= param and returned to by
   every Close button. Descends from the cover module's visual language.
   Browse layout, top to bottom: compact centred heading, one BAR holding
   the search pill (left) and the Journeys/Activities toggle (right), a
   left-aligned wrapping chip row, then a full-width card pane that is the
   ONLY thing that scrolls — so every control stays under the hand however
   long the shelf gets.
   ───────────────────────────────────────── */
.module-select{position:absolute;inset:0}
/* The card pane. Owns ALL the scrolling (scrollbar hidden — touch kiosk);
   min-height:0 lets it shrink to the leftover stage height instead of
   growing the column past the screen, and display:flex makes its lone
   .select-grid child stretch to the pane's height, which is what lets the
   empty-state mascot centre while a tall shelf still scrolls from its top. */
.select-scroll{
  flex:1;min-height:0;
  display:flex;overflow-y:auto;overscroll-behavior:contain;
  scrollbar-width:none;
}
.select-scroll::-webkit-scrollbar{display:none}
/* Full-stage column: head, bar, chips, pane. No bottom padding — the pane
   runs to the stage foot, so cards use every row of the screen; the grid's
   own padding keeps the last row clear. */
.select-inner{
  position:absolute;inset:0;z-index:1;
  display:flex;flex-direction:column;align-items:stretch;
  gap:14rem;padding:16rem 48rem 0;
}
.select-head{
  display:flex;flex-direction:column;align-items:center;gap:10rem;
  text-align:center;
}
.select-title{
  /* The squiggle hangs 12rem below the text (absolute, out of flow) — the
     bottom margin is its clearance, so the sub-line gets real daylight
     (10rem head gap + 12rem = squiggle depth + 10rem air). 52rem: this is
     the kiosk's shopfront and is read from standing distance. */
  margin:0 0 12rem;font:700 52rem/1.1 "Gotham Rounded","Nunito",sans-serif;
  color:var(--kohl);
}
/* "adventure" carries the house amber and a slow-twinkling spark — the
   one splash of colour in the head, so the word itself is the invitation. */
.select-title .adv{font-style:normal;color:var(--aamras);position:relative;white-space:nowrap}
.select-title .adv .spark{
  position:absolute;top:-13rem;right:-30rem;width:26rem;height:26rem;
  color:var(--aamras);animation:select-spark 2.6s ease-in-out infinite;
}
.select-title .adv .spark svg{width:100%;height:100%;display:block}
@keyframes select-spark{
  0%,100%{transform:scale(.7) rotate(-8deg);opacity:.5}
  50%{transform:scale(1.08) rotate(10deg);opacity:1}
}
.select-sub{
  margin:0;font:500 22rem/1.3 "Gotham Rounded","Nunito",sans-serif;
  color:var(--ink-mute);
}
/* Journeys ↔ Activities — one kohl thumb SLIDES between two equal
   segments (inline-grid 1fr auto-columns, so the wider label sizes both).
   Thumb motion is a transform transition (interruptible, never keyframes);
   the labels above it only transition colour. */
.select-toggle{
  position:relative;flex:none;
  display:inline-grid;grid-auto-flow:column;grid-auto-columns:1fr;gap:6rem;
  padding:6rem;
  background:rgba(20,20,31,.08);border-radius:999rem;
}
.select-toggle-thumb{
  position:absolute;top:6rem;bottom:6rem;left:6rem;
  width:calc(50% - 9rem);
  background:var(--kohl);border-radius:999rem;
  box-shadow:0 6rem 14rem rgba(20,20,31,.26);
  transition:transform .4s var(--ease-boing);
}
.select-toggle.is-activity .select-toggle-thumb{transform:translateX(calc(100% + 6rem))}
.select-toggle-btn{
  position:relative;z-index:1;
  display:inline-flex;align-items:center;justify-content:center;gap:10rem;
  border:0;background:transparent;color:var(--ink-mute);
  font:700 24rem/1 "Gotham Rounded","Nunito",sans-serif;letter-spacing:.3rem;
  padding:16rem 38rem;border-radius:999rem;
  transition:color var(--t-fast) var(--ease),
             transform var(--t-fast) var(--ease);
}
.select-toggle-btn:hover{color:var(--kohl)}
.select-toggle-btn:active{transform:scale(.96)}
.select-toggle-btn.is-active,
.select-toggle-btn.is-active:hover{color:#FFF8EE}
.stb-ico{width:26rem;height:26rem;flex:none}
.stb-ico svg{width:100%;height:100%;display:block}
/* The bar — search pill on the left, toggle on the right, one row. */
.select-tools{
  flex:none;
  display:flex;align-items:center;justify-content:space-between;gap:24rem;
  padding:0 6rem;   /* shadow / focus-ring clearance, matches the chips row */
}
.select-search{position:relative;flex:none;width:560rem}
.select-search-input{
  width:100%;height:64rem;border-radius:999rem;
  background:#fff;border:2rem solid var(--stroke);color:var(--kohl);
  padding:0 60rem 0 64rem;             /* icon left, ✕ right — rail width */
  font:500 22rem/1 "Gotham Rounded","Nunito",sans-serif;
  box-shadow:var(--sh-card);
  transition:border-color var(--t-fast) var(--ease);
}
.select-search-input::placeholder{color:var(--ink-mute)}
.select-search-input:focus{outline:none;border-color:var(--aamras)}
.select-search-input::-webkit-search-cancel-button{display:none} /* native ✕ too small for touch */
.select-search .ss-ico{position:absolute;left:22rem;top:50%;translate:0 -50%;width:26rem;height:26rem;pointer-events:none;opacity:.55}
.select-search .ss-ico img{width:100%;height:100%;object-fit:contain}
.select-search-clear{
  position:absolute;right:8rem;top:50%;transform:translateY(-50%);
  width:48rem;height:48rem;border:0;border-radius:50%;
  background:rgba(20,20,31,.08);color:var(--kohl);
  display:grid;place-items:center;
  transition:transform 120ms var(--ease);
}
.select-search-clear svg{width:20rem;height:20rem}
.select-search-clear:active{transform:translateY(-50%) scale(.9)}
/* Every chip (games, a tick, then artists) in one left-aligned row under
   the bar, wrapping when a museum's worth of artists outgrows it — all
   visible at once, no swiping. */
.select-chips{width:100%;padding:0 6rem}
.select-chips-track{display:flex;flex-wrap:wrap;align-items:center;gap:10rem}
.select-chips-sep{flex:none;width:2rem;height:30rem;border-radius:2rem;background:var(--stroke);margin:0 8rem}
/* Chip. Selected = kohl fill + warm white — the segmented control's thumb
   vocabulary, so "picked" reads the same everywhere on this screen and
   amber stays reserved for actions (Play / Show everything). */
.select-chip{
  flex:none;display:inline-flex;align-items:center;gap:8rem;
  height:52rem;padding:0 26rem;border-radius:999rem;
  background:#fff;border:2rem solid var(--stroke);color:var(--kohl);
  font:700 20rem/1 "Gotham Rounded","Nunito",sans-serif;letter-spacing:.3rem;
  box-shadow:var(--sh-card);
  transition:transform 120ms var(--ease),background var(--t-fast) var(--ease),
             border-color var(--t-fast) var(--ease),color var(--t-fast) var(--ease);
}
.select-chip:active{transform:scale(.94)}   /* touch answer — the kiosk never hovers */
.select-chip.is-active{background:var(--kohl);border-color:var(--kohl);color:#FFF8EE}
.select-chip.is-tag i{font:500 17rem/1 "Gotham Rounded","Nunito",sans-serif;font-style:normal;opacity:.55;margin-right:2rem}
/* Filtered-empty escape hatch under the mascot. */
.select-clear-all{
  display:inline-flex;align-items:center;border:0;border-radius:999rem;
  background:var(--aamras);color:#fff;padding:18rem 36rem;
  font:700 22rem/1 "Gotham Rounded","Nunito",sans-serif;letter-spacing:.3rem;
  box-shadow:var(--sh-cta);transition:transform 120ms var(--ease);
}
.select-clear-all:active{transform:scale(.95)}
/* The shelf. Top-left anchored — this grid fills with many rows over time,
   so cards rack left-to-right in reading order and a long shelf scrolls
   with the next row peeking over the stage foot (the scroll cue).
   Side padding is shadow/focus clearance; the 56rem foot keeps the
   last row's contact shadow clear of the edge. */
.select-grid{
  flex:1;display:flex;flex-wrap:wrap;align-items:stretch;
  align-content:flex-start;
  gap:24rem;padding:10rem 10rem 56rem;
}
/* Mascot-only pane centres its one flex line — auto margins can't push
   vertically inside a wrapped line packed to flex-start. */
.select-grid:has(> .select-empty){align-content:center;justify-content:center}
/* Empty tab — the magnifier mascot delivers the message (the shared .hm
   idle keyframes animate it for free); the bubble wraps here instead of
   the plaque's default nowrap. margin:auto centres it in the pane. */
.select-empty{display:flex;flex-direction:column;align-items:center;gap:24rem;padding:16rem 0 8rem;margin:auto}
.select-empty .hm-fig{height:108rem;width:108rem}
.select-empty .hm-bubble{white-space:normal;max-width:520rem;font-size:26rem;line-height:1.3}
/* Back-to-welcome pill — rendered only when the visit arrived from
   landing.html (?from=landing), so a standalone-player kiosk never grows
   a dead Home button. Reuses the cta-back pill (arrow already points
   left) and sits in the btn-close slot every module uses (top:16/left:48),
   so "top-left leaves this screen" stays one learned gesture. */
.select-home{position:absolute;top:16rem;left:48rem;z-index:2}

/* Leave-the-player veil (Exit → landing.html) — fades up to the landing
   page's own cream so the hard page swap reads as one continuous move.
   Fixed + max z: it must cover the stage, the transition covers (z-100)
   and every overlay; it also hit-tests from frame one (even at opacity 0),
   eating stray taps while the exit is in flight. */
.leave-veil{position:fixed;inset:0;background:#EFE6D4;opacity:0;z-index:10000}

/* Journey card — full-bleed tinted cover mount up top, white body below,
   aamras Play pill. The whole card is the tap target (kiosk-friendly);
   the pill is the affordance. Hover lifts the card and boings the pill,
   reusing the cover screen's Play-CTA keyframes for one consistent
   gesture. Sized so FOUR fit a row of the full-width pane
   (4×420 + 3×24 gaps = 1752rem); the full-width blurb makes the card
   tall enough that a populated shelf's second row rides partly below
   the stage foot — that peek IS the scroll cue. Width stays at 420
   because at the old five-across 330rem the artwork read as postage
   stamps on an acre of paper.
   Hover transitions transform + border-color ONLY — a transitioning
   box-shadow repainted these big surfaces every frame (felt as lag);
   the resting shadow stays put and the amber border does the talking. */
.journey-card{
  position:relative;
  width:420rem;
  display:flex;flex-direction:column;
  background:#fff;border-radius:20rem;
  /* The card's hue lives on the border + the cover mount, never on the
     artwork itself. 62% of the hue against the shared stroke: legible as
     identity across a row, still quiet enough to be one set. */
  border:2rem solid color-mix(in srgb, var(--tint) 62%, var(--stroke));
  padding:0;text-align:left;
  box-shadow:var(--sh-card);
  transition:transform var(--t-fast) var(--ease),
             border-color var(--t-fast) var(--ease),
             opacity var(--t-med) var(--ease);
}
/* Each card takes one of the four house hues so a row of four reads as
   four different things, not four copies of the same white rectangle.
   Ordered orange → teal → magenta → purple, i.e. for adjacent contrast,
   and cycling on 4n so a second row starts the sequence over. nth-child,
   not a data attribute: the grid is innerHTML-swapped per tab and CSS
   recounts on its own. The tint lives on the mount and the under-hand
   border ONLY — the Play pill stays amber on every card (see below). */
.journey-card{--tint:var(--aamras)}
.journey-card:nth-child(4n+2){--tint:var(--ferozi)}
.journey-card:nth-child(4n+3){--tint:var(--gulabi)}
.journey-card:nth-child(4n+4){--tint:var(--baingani)}
/* Contact shadow on the lit floor — starts at top:100%, so it sits wholly
   below the card and needs no negative z-index (which would paint OVER the
   card's background whenever a mid-flight transform makes a stacking
   context). Warm brown, not grey: it's cast on paper under lamplight. */
.journey-card::after{
  content:'';position:absolute;top:100%;left:5%;right:5%;height:26rem;
  transform-origin:50% 0;pointer-events:none;
  background:radial-gradient(50% 50% at 50% 26%,
    rgba(122,76,8,.22), rgba(122,76,8,0) 72%);
  transition:transform var(--t-fast) var(--ease),
             opacity var(--t-fast) var(--ease);
}
/* Lifting the card lifts its shadow too, so the shadow spreads and softens
   to pay it back — the cue that says "off the paper", not "moved down". */
.journey-card:not(.is-loading):hover::after{
  transform:translateY(7rem) scaleX(1.08);opacity:.62;
}
.journey-card:not(.is-loading):hover{
  transform:translateY(-8rem);
  border-color:var(--tint);
}
/* Press pushes IN, not up. The kiosk is touch-only and never fires hover,
   so :active is the ONLY feedback a visitor's finger ever gets — it has to
   read as the card being pressed onto the paper. 120ms in (the release
   keeps the card's 180ms, so it's quick to answer and soft to let go). */
.journey-card:not(.is-loading):active{
  transform:scale(.96);
  transition-duration:120ms;
}
/* Pressed card is closer to the paper, so its contact shadow tightens and
   darkens instead of spreading the way the hover lift makes it. */
.journey-card:not(.is-loading):active::after{
  transform:scaleX(.9);opacity:.9;
}
/* Tapped, journey loading. NOT a disabled grey — this card is the one that
   was chosen, so it eases back a step while the journey fetches.
   pointer-events off already blocks a re-tap; the picking flag in
   bindJourneySelect blocks the double-fire. */
.journey-card.is-loading{
  opacity:.72;pointer-events:none;
  transform:scale(.985);
}

/* The mount, now bleeding to the card's edges. Covers are
   object-fit:contain — museum artwork is never cropped to fill a frame —
   so most cards show wall around the art. The card's hue washes that wall
   (14%: a painted gallery wall, not a stain on the art) and a hairline at
   the mount's edge seats the plate. Top radii = card 20 − border 2; the
   bottom edge is square, so the plate sits flush on the white body. */
.journey-card-frame{
  position:relative;
  width:100%;aspect-ratio:2 / 1;
  /* 20%, up from 14: at 14 the "painted gallery wall" was indistinguishable
     from the white body on white-backed artwork, so the mount idea never
     read. 20 is a wall you can see that still doesn't stain the art. */
  background:color-mix(in srgb, var(--tint) 20%, #FFF8EE);
  border-radius:18rem 18rem 0 0;overflow:hidden;
  display:grid;place-items:center;
}
.journey-card-frame img{width:100%;height:100%;object-fit:contain;display:block}
/* Coverless frame (fresh activities mostly) — the tab's own book/puzzle
   glyph keeps the card looking intentional rather than broken. */
.journey-card-noimg{
  display:flex;flex-direction:column;align-items:center;gap:10rem;
  font:500 19rem/1.3 "Gotham Rounded","Nunito",sans-serif;color:var(--ink-mute);
}
.journey-card-noimg svg{width:50rem;height:50rem;opacity:.5}

/* The full-bleed mount means the body owns its own gutter now. */
.journey-card-body{
  display:flex;flex-direction:column;gap:8rem;
  flex:1 1 auto;padding:12rem 20rem 14rem;
}
.journey-card-title{
  margin:0;flex:1 1 auto;
  font:700 24rem/1.2 "Gotham Rounded","Nunito",sans-serif;color:var(--kohl);
  /* Clamp to 2 lines so a runaway title can't push Play off the card. NO
     reserved second line: with the museum's real mixed fields (cards with
     and without blurb) row alignment is broken anyway, and the
     reservation just punched a blank hole under every one-line title. */
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:2;
  overflow:hidden;
}
/* The blurb the CMS already collects — without it a visitor picks between
   four covers on artwork alone (two journeys can even share a cover).
   Full card width right under the title (it used to share a squeezed
   column beside the pill, which ellipsized real intros at ~8 words);
   three lines, then ellipsis. Near-kohl, not --ink-mute: at kiosk
   standing distance the muted grey read as texture, not words. */
.journey-card-blurb{
  margin:0;
  font:500 20rem/1.35 "Gotham Rounded","Nunito",sans-serif;
  color:color-mix(in srgb, var(--kohl) 80%, #fff);
  display:-webkit-box;-webkit-box-orient:vertical;-webkit-line-clamp:3;
  overflow:hidden;
}
/* Amber on EVERY card, deliberately not var(--tint): amber is the house
   action colour; the card's hue identifies the card, the pill identifies
   the action. Kohl label + glyph, not white: white on aamras measures
   ~2:1 and this is the smallest amber thing on the kiosk — kohl clears
   9:1. The auto margins pin the pill to the card's bottom-right whether
   the card carries a three-line blurb or none at all. */
.journey-card-play{
  margin-top:auto;margin-left:auto;flex:none;
  display:inline-flex;align-items:center;
  background:var(--aamras);color:var(--kohl);border-radius:999rem;
  padding:13rem 26rem;box-shadow:var(--sh-cta);
  font:700 22rem/1 "Gotham Rounded","Nunito",sans-serif;letter-spacing:.3rem;
}
.journey-card-play .cta-inner{display:inline-flex;align-items:center;gap:9rem}
.journey-card-play .ico{width:24rem;height:24rem}
/* brightness(0) turns the white-filled play SVG near-kohl without
   touching the asset (see the note in journey-helpers.js). */
.journey-card-play .ico img{width:100%;height:100%;object-fit:contain;filter:brightness(0)}
.journey-card:not(.is-loading):hover .journey-card-play{
  animation:cta-play-boing 620ms var(--ease-boing);
}
.journey-card:not(.is-loading):hover .journey-card-play .ico{
  animation:cta-play-icon 620ms var(--ease-boing);
}

/* Keyboard focus — same amber ring language as the credits links; touch
   taps never show it (:focus-visible). */
.journey-card:focus-visible,
.select-toggle-btn:focus-visible,
.select-chip:focus-visible,
.select-search-input:focus-visible,
.select-search-clear:focus-visible,
.select-clear-all:focus-visible,
.select-home:focus-visible{outline:3rem solid var(--kohl);outline-offset:3rem}

@media (prefers-reduced-motion: reduce){
  .journey-card:not(.is-loading):hover{transform:translateY(-4rem)}
  .journey-card:hover .journey-card-play,
  .journey-card:hover .journey-card-play .ico{animation:none}
  /* Press still has to answer the finger — it just answers in colour
     instead of in movement. */
  .journey-card:not(.is-loading):active{transform:none;border-color:var(--tint)}
  .select-chip:active{transform:none;border-color:var(--kohl)}
  .select-search-clear:active{transform:translateY(-50%)}
  .select-clear-all:active{transform:none}
  .select-toggle-thumb{transition:none}
  .select-title .adv .spark{animation:none}
}

/* ─────────────────────────────────────────
   MEDIA SLOTS — video files & hosted players in "image" positions.
   Any image slot may hold a video file or a Vimeo/YouTube player link
   (CMS: pick a video / paste a link); journey-helpers mediaHTML renders
   .jmedia.is-video / .is-embed in the same spot the <img> sat. Videos
   contain-fit (museum rule: artwork shows WHOLE). Embeds have no
   intrinsic ratio, so each frame hands them its 16:9 max box and the
   hosted player letterboxes anything odd internally. All rules live
   HERE, one per frame, instead of scattered through the module blocks.
   ───────────────────────────────────────── */
.jmedia.is-embed{position:relative;overflow:hidden;background:#101018}
.jmedia.is-embed iframe{
  position:absolute;inset:0;width:100%;height:100%;border:0;display:block;
}
/* Inert placements (choice tiles, drag cards, backgrounds): the media can
   never take the tap — the button/card underneath owns it. */
.jmedia.is-inert, .jmedia.is-inert iframe{pointer-events:none}

/* Question hero frames (MCQ / personality / slider / scale). The frame
   hugs the media: imgs bring intrinsic size, so video + embed get the
   variant's max box outright (all three boxes are already 16:9-ish). */
.image-frame .img-inner .jmedia.is-video,
.image-frame .img-inner .jmedia.is-embed{
  display:block;width:1075rem;max-width:100%;aspect-ratio:16/9;
}
.image-frame.slider-size .img-inner .jmedia.is-video,
.image-frame.slider-size .img-inner .jmedia.is-embed{width:882rem}
.image-frame.compact .img-inner .jmedia.is-video,
.image-frame.compact .img-inner .jmedia.is-embed{width:633rem}
.image-frame .img-inner video.jmedia{object-fit:contain;background:#101018}

/* Content scene — file videos already ride the img rules per layout
   (tag selectors above/below); only embeds need a box. */
.module-content.layout-fullscreen .scene .jmedia.is-embed{
  position:absolute;inset:0;width:100%;height:100%;
}
.module-content.layout-portrait .scene .jmedia.is-embed,
.module-content.layout-landscape .scene .jmedia.is-embed{
  width:100%;aspect-ratio:16/9;max-height:100%;
  filter:drop-shadow(0 8rem 16rem rgba(0,0,0,.12));
}

/* Cover frame */
.cover-frame .jmedia.is-video{
  display:block;max-width:1472rem;max-height:100%;width:auto;height:auto;
  background:#101018;
}
.cover-frame .jmedia.is-embed{width:1472rem;max-width:100%;aspect-ratio:16/9}
.module-cover.layout-portrait .cover-frame .jmedia.is-embed{width:100%}
.module-cover.layout-fullscreen .cover-frame .jmedia.is-video{
  width:100%;height:100%;max-width:none;max-height:none;object-fit:contain;
  background:transparent;
}
.module-cover.layout-fullscreen .cover-frame .jmedia.is-embed{
  width:100%;height:100%;max-width:none;aspect-ratio:auto;
}

/* Fullscreen lightbox (zoom rig is image-only; media just sits big) */
.overlay-fullscreen .fs-frame .jmedia.is-video{
  width:1920rem;height:1080rem;object-fit:contain;display:block;
}
.overlay-fullscreen .fs-frame .jmedia.is-embed{
  width:1728rem;max-width:92vw;aspect-ratio:16/9;
}

/* Choice tiles — .img-fit class carries the absolute-fill from the tile
   rules; video adds contain-fit over the same paper-tint flood. */
.choice.is-image video.img-fit{
  width:100%;height:100%;object-fit:contain;background-color:#F7EBDC;
}

/* Sequence drag cards */
.card-media .jmedia.is-video{width:100%;height:100%;object-fit:contain;display:block}
.card-media .jmedia.is-embed{width:100%;height:100%}

/* Journey-end backdrop — decorative layer, cover like its img rule. */
.module-complete .bg .jmedia.is-video{width:100%;height:100%;object-fit:cover;display:block}
.module-complete .bg .jmedia.is-embed{position:absolute;inset:0}

/* Tap-to-play badge — the guard's last rung, only when a browser refuses
   even muted autoplay (e.g. Firefox "Block Audio and Video"). Centres on
   the video's positioned parent (scene / img-inner are both positioned). */
.media-tap-play{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  display:inline-flex;align-items:center;gap:10rem;z-index:3;
  background:#fff;color:var(--kohl);border:2rem solid var(--gulabi);
  border-radius:999rem;padding:14rem 26rem;cursor:pointer;
  font:700 22rem/1 "Gotham Rounded","Nunito",sans-serif;
  box-shadow:0 8rem 24rem rgba(0,0,0,.25);
  transition:box-shadow var(--t-fast) var(--ease);
}
.media-tap-play:hover{box-shadow:0 12rem 32rem rgba(0,0,0,.32)}
/* Play triangle drawn in CSS (kohl on the white pill); nudged right a
   touch — geometric centring reads left-heavy on triangles. */
.media-tap-play .ico{
  width:0;height:0;margin-left:2rem;
  border:9rem solid transparent;border-right:0;
  border-left:14rem solid var(--kohl);
}

/* Feedback overlays */
.feedback-modal .feedback-figure .jmedia.is-video,
.feedback-modal .feedback-figure .jmedia.is-embed{
  display:block;width:480rem;aspect-ratio:16/9;border-radius:6rem;
}
.feedback-modal.is-fact-card .fact-card-image .jmedia.is-video,
.feedback-modal.is-fact-card .fact-card-image .jmedia.is-embed{
  display:block;height:100%;aspect-ratio:16/9;width:auto;
}
.feedback-modal.is-fact-card .fact-card-image video.jmedia{object-fit:contain;background:#101018}

/* ─────────────────────────────────────────
   CONTENT PAGE — three layouts, set per module via `layout`:
     · fullscreen — media fills the stage, floating Back / Continue
     · portrait   — media beside a gulabi text panel (50 / 50 columns)
     · landscape  — framed media above a gulabi text band (cover-style)
   ───────────────────────────────────────── */
.module-content{position:absolute;inset:0;background:#FFF8EE}
.module-content .scene img,
.module-content .scene video{display:block;background:#FFF8EE}

/* ── Fullscreen — media fills the stage (contain, so artwork is never cropped) */
.module-content.layout-fullscreen .scene{position:absolute;inset:0}
.module-content.layout-fullscreen .scene img,
.module-content.layout-fullscreen .scene video{
  width:100%;height:100%;object-fit:contain;
}
.module-content .footer-floating{
  position:absolute;left:50%;bottom:16rem;transform:translateX(-50%);
  display:flex;align-items:center;justify-content:space-between;
  width:1816rem;z-index:2;
}

/* ── Shared by portrait + landscape — framed media + gulabi text surface ── */
.module-content.layout-portrait .scene,
.module-content.layout-landscape .scene{
  display:flex;align-items:center;justify-content:center;
  position:relative;z-index:1;
}
.module-content.layout-portrait .scene img,
.module-content.layout-portrait .scene video,
.module-content.layout-landscape .scene img,
.module-content.layout-landscape .scene video{
  width:auto;height:auto;max-width:100%;max-height:100%;
  /* De-matted (2026-06-12): the artwork sits bare, drop-shadow on its edge. */
  filter:drop-shadow(0 8rem 16rem rgba(0,0,0,.12));
}
.content-panel .row,
.content-band  .row{
  display:flex;align-items:center;justify-content:space-between;
}

/* ── Portrait — media column | gulabi text panel, 50 / 50 ── */
.module-content.layout-portrait{display:flex;flex-direction:row}
.module-content.layout-portrait .scene{flex:1 1 0;min-width:0;padding:56rem}
.module-content.layout-portrait .content-panel{
  flex:1 1 0;min-width:0;
  background:var(--gulabi);color:#fff;
  display:flex;flex-direction:column;
  padding:72rem 64rem;gap:40rem;
  position:relative;z-index:1;
}
.module-content.layout-portrait .content-panel .copy{
  flex:1 1 auto;
  display:flex;flex-direction:column;justify-content:center;gap:16rem;
  font:700 28rem/1.4 "Gotham Rounded","Nunito",sans-serif;color:#fff;
}

/* ── Landscape — framed media above a gulabi band (cover-style) ── */
.module-content.layout-landscape{display:flex;flex-direction:column}
.module-content.layout-landscape .scene{
  flex:1 1 auto;min-height:0;padding:24rem 48rem;
}
.module-content.layout-landscape .content-band{
  flex:0 0 auto;
  background:var(--gulabi);color:#fff;
  display:flex;flex-direction:column;gap:20rem;
  padding:32rem 48rem;
  position:relative;z-index:1;
}
.module-content.layout-landscape .content-band .copy{
  font:700 32rem/1.3 "Gotham Rounded","Nunito",sans-serif;color:#fff;
  display:flex;flex-direction:column;gap:12rem;
  text-wrap:pretty;
}

/* ─────────────────────────────────────────
   MCQ — flex column with header/footer in flow so centre owns remaining height
   ───────────────────────────────────────── */
.module-mcq{
  position:absolute;inset:0;display:flex;flex-direction:column;
}
.module-mcq > .activity-header{position:relative;flex:0 0 64rem}
.module-mcq > .activity-footer{position:relative;flex:0 0 85rem}

.mcq-centre{
  flex:1 1 0;min-height:0;overflow:hidden;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:32rem;padding:12rem 0;position:relative;z-index:1;
}
.mcq-centre.no-image{gap:40rem}

/* Vertical layout — portrait image on the left, question + stacked options
   on the right. Used when m.layout === 'vertical'. The right column hugs
   its content and is centered vertically against the image. */
.mcq-centre.is-vertical{
  flex-direction:row;align-items:center;justify-content:center;
  gap:60rem;padding:12rem 48rem;
}
.mcq-centre.is-vertical .image-frame .img-inner img{
  max-width:605rem;max-height:850rem;
}
.mcq-centre.is-vertical .mcq-text-col{
  display:flex;flex-direction:column;align-items:center;
  gap:24rem;flex:0 1 auto;max-width:900rem;
}
.mcq-centre.is-vertical .question{
  text-align:center;max-width:900rem;
}
.mcq-centre.is-vertical .choices-row{
  flex-direction:column;align-items:stretch;
  gap:14rem;padding:0;width:100%;
}
.mcq-centre.is-vertical .choice{
  width:100%;height:auto;min-height:88rem;
}

.image-frame{
  /* De-matted (2026-06-12, user direction): the white-mat "framed print"
     border (#fff + 10rem padding) is gone project-wide — the artwork sits
     bare with the layered shadow hugging its edge. */
  box-shadow:var(--sh-img);
  cursor:zoom-in;position:relative;flex-shrink:0;
  /* Transform is animated by GSAP at entry time; a CSS transition on it would
     intercept GSAP's per-frame writes and freeze the animation. Transition
     box-shadow only — hover scale snaps but the entry bounce plays cleanly. */
  transition:box-shadow var(--t-fast) var(--ease);
}
.image-frame:hover{box-shadow:0 12rem 40rem rgba(0,0,0,.18),0 4rem 10rem rgba(0,0,0,.10)}
/* Magnifier wiggles via GSAP — hover only changes its bg/glow, not transform. */
.image-frame .magnifier{transition:background var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease)}
.image-frame:hover .magnifier{background:#fff;box-shadow:0 4rem 14rem rgba(0,0,0,.18)}
/* no-zoom: the question media is a video / player link / gif — nothing
   still to magnify, so the frame sheds every button affordance. */
.image-frame.no-zoom{cursor:default}
.image-frame.no-zoom:hover{box-shadow:var(--sh-img)}
/* The frame hugs the image — img drives layout via its intrinsic size,
   capped at the variant's max bounds. This way photos with non-16:9
   aspects (Journey 2's photographic content, ~1.36–1.47) don't render
   inside a wider 1.778 box with cream pillarbox bars; the frame just
   resizes around them. Journey 1's 1920×1080 illustrations already match
   the 1075×605 max so visual outcome is unchanged for them. */
.image-frame .img-inner{
  position:relative;
  background:#FFF8EE;
  display:block;
  /* line-height:0 stops a stray text baseline gap under the img element. */
  line-height:0;
}
.image-frame .img-inner img{
  display:block;
  max-width:1075rem;
  max-height:605rem;
  width:auto;
  height:auto;
}
.image-frame.slider-size .img-inner img{max-width:882rem;max-height:496rem}
/* compact image used by image+image-options or image+audio variations */
.image-frame.compact   .img-inner img{max-width:633rem;max-height:356rem}
.image-frame .magnifier{
  position:absolute;right:14rem;bottom:14rem;
  display:flex;align-items:center;justify-content:center;
  background:#fff;border:1rem solid rgba(20,20,31,.08);
  border-radius:28rem;padding:10rem 16rem;
  box-shadow:0 2rem 4rem rgba(0,0,0,.10), 0 10rem 24rem -2rem rgba(38,26,13,.22);
}
.image-frame .magnifier img{width:20rem;height:20rem;flex:0 0 auto}
/* Slide-reveal "Fullscreen" text on hover — gentle width + opacity reveal. */
.image-frame .magnifier .mag-label{
  font:700 14rem/1 "Gotham Rounded","Nunito",sans-serif;
  color:var(--kohl);
  display:inline-block;white-space:nowrap;overflow:hidden;
  max-width:0;margin-left:0;opacity:0;
  transition:max-width .45s ease, margin-left .45s ease, opacity .35s ease .05s;
}
.image-frame:hover .magnifier .mag-label{max-width:96rem;margin-left:6rem;opacity:1}

/* Stack image-frame + audio player together. The shared white mat that
   fused them into one plaque is gone (de-matting pass) — the image keeps
   its own shadow and the player reads as its own chip below. */
.image-with-audio{display:flex;flex-direction:column;gap:12rem;align-items:stretch;flex-shrink:0}
.audio-player{
  background:rgba(255,163,0,.08);border:2rem solid var(--aamras);border-radius:12rem;
  display:flex;align-items:center;gap:10rem;padding:14rem 16rem;margin-top:8rem;
  box-shadow:0 1rem 3rem rgba(0,0,0,.10);
}
/* Audio-only question (no image): the player has no parent to stretch to,
   so give it a comfortable kiosk width. */
.mcq-centre > .audio-player{width:653rem;flex-shrink:0;margin-top:0;padding:24rem 28rem;}
.mcq-centre > .audio-player .wave{height:32rem}
.audio-player .play{
  width:40rem;height:40rem;border-radius:999rem;flex:0 0 auto;
  background:linear-gradient(135deg,#FFD17F 0%,var(--aamras) 100%);
  display:grid;place-items:center;
  box-shadow:0 4rem 6rem rgba(0,0,0,.10), 0 2rem 4rem rgba(0,0,0,.10);
  transition:transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.audio-player .play:hover{transform:scale(1.08);box-shadow:0 8rem 20rem rgba(255,163,0,.45),0 2rem 6rem rgba(0,0,0,.14)}
.audio-player .play:active{transform:scale(.94)}
.audio-player .play img{width:16rem;height:16rem}
.audio-player .wave{flex:1;height:24rem;display:flex;align-items:flex-end;gap:2rem;overflow:hidden}
/* Bars are full-height; their visual height is driven by GSAP scaleY (kept
   on the GPU compositor so the analyser RAF doesn't trigger layout for each
   of 28 bars × 60fps). will-change pre-promotes each bar to its own layer. */
.audio-player .wave span{
  flex:1;height:100%;background:var(--aamras);opacity:.8;border-radius:6rem;
  transform:scaleY(0.3);transform-origin:bottom;
  will-change:transform;
}

.question{
  font:700 32rem/40rem "Gotham Rounded","Nunito",sans-serif;color:var(--kohl);
  text-align:center;margin:0;max-width:1500rem;flex-shrink:0;
}
.question.multi{white-space:normal;line-height:40rem}

/* ─────────────────────────────────────────
   Typography — orphan / line-break control (client note, 2026-07-01)
   "Ensure sentences are complete in a line so there's no break in the idea;
    avoid alignment where only one or two words fall to the second line."
   `balance` evens multi-line display text (questions, titles, prompts) so a
   phrase splits into whole-idea lines instead of stranding a lone word;
   `pretty` guards longer body paragraphs against a short last line. An
   unsupported value simply falls back to normal wrapping (no harm).
   EXCLUDED on purpose: the jigsaw artwork title — its header FLIP uses a
   proportional replica that must wrap identically to the intro title, so
   balancing one side would desync the two. For a stubborn pair authors can
   still glue two words with a non-breaking space in the CMS copy. */
.question,
.header-title,
.select-title, .select-sub,
.journey-card-title, .journey-card-body,
.cover-footer h1, .cover-footer .desc,
.slider-panel .prompt, .scale-panel .prompt,
.sequence-prompt .prompt, .sequence-prompt .sub,
.slot-hint, .card-text span,
.choice .label,
.feedback-modal .hero h2, .feedback-modal .subtitle, .feedback-modal .caption,
.idle-title, .idle-sub{
  text-wrap:balance;
  /* Same guard as .choice .label: an unbroken author string (a URL, a
     mashed word) breaks inside its box instead of escaping the layout. */
  overflow-wrap:break-word;
}
.content-panel .copy p, .content-band .copy p,
.feedback-modal .body,
.complete-card .body{
  text-wrap:pretty;
  overflow-wrap:break-word;
}

.choices-row{
  display:flex;align-items:center;justify-content:center;gap:32rem;
  width:100%;padding:0 48rem;flex-shrink:0;
}
.choices-row.image-options{gap:40rem}

.choice{
  background:#fff;border:2rem solid var(--stroke);border-radius:20rem;
  padding:20rem;display:flex;align-items:center;gap:18rem;
  box-shadow:0 4rem 7rem rgba(0,0,0,.08);
  /* min-height (not height): a long sentence grows the card instead of
     spilling past the fixed box (user 2026-07-15). Short labels sit at the
     same 111rem as ever; picture cards / the feedback selected-card image
     variant keep their own explicit heights. */
  text-align:left;width:432rem;min-height:111rem;flex-shrink:0;
  /* Transform is GSAP-animated at entry — see .image-frame note. */
  transition:border-color var(--t-fast) var(--ease),
             box-shadow var(--t-fast) var(--ease),
             background-color var(--t-fast) var(--ease);
}
/* Playful hover — one-shot lift + tilt that returns to rest, plus a
   wobble on the letter badge. Skipped on cards already in a state of
   their own (locked / mid-reaction) so the click-time animation owns
   that moment. End-at-rest pattern matches the four CTA hovers;
   persistent state is conveyed via the brighter border + amber halo. */
.choice:not(.is-locked):not(.is-right):not(.is-wrong):hover{
  animation:choice-hover 460ms var(--ease-boing);
  border-color:var(--aamras);
  box-shadow:0 12rem 24rem rgba(0,0,0,.14), 0 0 0 2rem rgba(255,163,0,.18);
}
.choice .letter{
  width:48rem;height:48rem;border-radius:999rem;
  display:flex;align-items:center;justify-content:center;
  font:700 22rem/1 "Gotham Rounded","Nunito",sans-serif;color:#fff;
  box-shadow:var(--sh-letter);flex:0 0 auto;
  overflow:hidden;   /* the badge is a fixed circle — it shows what fits */
  /* No `transition: transform` — anime.js drives this property on lock-in
     (mcq.js click handler), and a transition would fight it per-frame and
     mush the spring. The hover keyframe ends at rest, so we don't need
     transition for the common mouseout case anyway. box-shadow transitions
     so the locked-state outer ring fades in/out cleanly. */
  transition:box-shadow var(--t-fast) var(--ease);
}
.choice:not(.is-locked):not(.is-right):not(.is-wrong):hover .letter{
  animation:choice-letter-pop 460ms var(--ease-boing);
}
@keyframes choice-hover{
  0%, 100% { transform:translateY(0)    rotate(0)     scale(1); }
  28%      { transform:translateY(-2rem) rotate(1deg)  scale(1.01); }
  60%      { transform:translateY(-7rem) rotate(-2deg) scale(1.03); }
}
@keyframes choice-letter-pop{
  0%, 100% { transform:rotate(0)      scale(1); }
  40%      { transform:rotate(-14deg) scale(1.20); }
  70%      { transform:rotate(8deg)   scale(1.10); }
}
/* overflow-wrap: an unbroken author string (a URL, a mashed word) breaks
   inside the 432rem card instead of escaping it. */
.choice .label{font:500 20rem/1.2 "Gotham Rounded","Nunito",sans-serif;color:#000;flex:1;min-width:0;overflow-wrap:break-word}
.choice.color-aamras   .letter{background:var(--aamras)}
.choice.color-gulabi   .letter{background:var(--gulabi)}
.choice.color-ferozi   .letter{background:var(--ferozi)}
.choice.color-baingani .letter{background:var(--baingani)}

/* image-option choice — the artwork FILLS the card edge-to-edge and the
   letter badge floats over it at the bottom-left (2026-06-15 redesign;
   was a FIT image on a cream plate, in a row beside a top-left badge).
   width 420 (not 432) so FOUR tiles + gaps clear the 1824 row: 4·420 +
   3·40 = 1800. 4:3 ≈ the Gond/Bhil source ratio, so cover crops little —
   but note an image with a small subject on white ground (e.g. the
   what-mood avatars) zooms/crops under cover; those want fuller source art. */
.choice.is-image{
  position:relative;flex-direction:column;align-items:stretch;
  width:420rem;height:315rem;padding:0;gap:0;overflow:hidden;
}
.choice.is-image .img{
  position:absolute;inset:0;border-radius:0;overflow:hidden;
  /* The empty space around a contained image is flooded with the PAPER
     background's own colour — #F7EBDC = the white --paper composited through
     the .paper-bg texture (sampled from a render) — so the letterbox reads as
     the page behind the card and the artwork floats on it. Doubles as the
     loading fallback if the image 404s. */
  background-color:#F7EBDC;
}
/* The artwork shows WHOLE (background-size:contain) so it's never clipped —
   option art is BOTH 4:3 and portrait (e.g. bhuri-bai activity 10) and
   cover-cropping ate the portrait faces top/bottom. Inline background-image
   (not a CSS var) so the relative url() resolves against the document. */
.choice.is-image .img-fit{
  position:absolute;inset:0;
  background-position:center;background-size:contain;background-repeat:no-repeat;
}
/* Badge over busy artwork: a white halo ring + deeper drop so it always
   reads, whatever the painting behind it. */
.choice.is-image .letter{
  position:absolute;left:16rem;bottom:16rem;z-index:2;
  box-shadow:0 4rem 12rem rgba(0,0,0,.40), 0 0 0 3rem rgba(255,255,255,.9);
}

/* Mixed rows (choices render their own media since 2026-07-15): text pills
   keep their normal 111rem height beside taller picture/audio cards — the
   row's align-items:center centres them (user preference; an equal-height
   stretch was tried and reverted the same day). */
/* Vertical layout: a picture card needs a real height — the generic
   vertical rule's height:auto would collapse its absolutely-positioned
   artwork to the 88rem text minimum. */
.mcq-centre.is-vertical .choice.is-image{height:315rem}

/* Restrained locked state — card stays solid white inside (no fill
   tint, no loud framing) and reads as "chosen" through three quiet
   cues only: a thin amber border edge, a subtle amber-tinted inset
   shadow (warm peachy press along the top), and a soft amber-tinted
   outer drop shadow that lifts the card off the page in a warm glow.
   Scale bumps from 1.02 → 1.04 so the chosen card is visibly a touch
   larger than its neighbours.
   The previous version's 6rem amber ring + .42-opacity drop-glow read
   as loud framing; here the amber lives in low-alpha tinted shadows
   (.14–.20) instead of solid rings — chosen card glows warm without
   being shouted at. Opacities are calibrated higher than the equivalent
   dark shadows would be (~2x), because amber on white perceives gentler
   than black at the same alpha.
   Border width stays 2rem to avoid inner-content reflow. A 380ms spring
   overshoots before settling into the static pose. The keyframe's 100%
   matches the static transform exactly so there's no jump when the
   animation hands off. The wrong/right reaction keyframes (choice-no,
   choice-yes) also use scale(1.04) at their start/end so the submit
   reactions don't snap. */
.choice.is-locked{
  background:#fff;
  border-color:var(--aamras);
  box-shadow:
    inset 0 4rem 10rem rgba(255,163,0,.14),
    0 12rem 26rem rgba(255,163,0,.20),
    0 4rem  10rem rgba(255,163,0,.14);
  transform:translateY(-6rem) scale(1.04);
  animation:choice-lock-in 380ms var(--ease-boing);
}
/* Letter badge keeps its baseline shadow only — no amber halo, to stay
   consistent with the restrained card framing. */
@keyframes choice-lock-in{
  0%   { transform:translateY(0)     scale(1); }
  40%  { transform:translateY(-11rem) scale(1.07); }
  70%  { transform:translateY(-3rem)  scale(1.025); }
  100% { transform:translateY(-6rem)  scale(1.04); }
}

/* Choice reaction — the verdict IS the tap response (tap-to-answer has no
   Continue confirm and no amber lock-in beat). mcq.js adds .is-wrong /
   .is-right TOGETHER WITH .is-locked on the tap: these rules out-cascade
   is-locked's colour + animation, while is-locked's static transform is
   what holds the lifted pose after the keyframe ends. The card either
   shakes "no-no" (incorrect) or pulses + spins its badge (correct). These
   mirror the restrained .is-locked treatment (white card, thin coloured
   border, soft tinted inset + outer shadows) just with the colour swapped
   — red (--lal #D50032) for incorrect, teal (--ferozi #5CB8B2) for
   correct. */
.choice.is-wrong{
  animation:choice-no 360ms ease-in-out;
  background:#fff;
  border-color:var(--lal);
  box-shadow:
    inset 0 4rem 10rem rgba(213,0,50,.14),
    0 12rem 26rem rgba(213,0,50,.20),
    0 4rem  10rem rgba(213,0,50,.14);
}
.choice.is-right{
  animation:choice-yes 480ms var(--ease-boing);
  background:#fff;
  border-color:var(--ferozi);
  box-shadow:
    inset 0 4rem 10rem rgba(92,184,178,.14),
    0 12rem 26rem rgba(92,184,178,.20),
    0 4rem  10rem rgba(92,184,178,.14);
}
.choice.is-right .letter{
  animation:choice-letter-spin 480ms var(--ease-boing);
}
/* Reactions fire straight from the tap, so 0% starts at GROUND level (the
   card's resting pose) and rises into the verdict — one continuous spring,
   no pre-jump. 100% reproduces .is-locked's static pose (translate -6,
   scale 1.04) so the keyframe hands off to the class transform without a
   snap when it ends. */
@keyframes choice-no{
  0%       { transform:translate(0,     0)     scale(1); }
  16%      { transform:translate(-7rem, -6rem) scale(1.04); }
  36%      { transform:translate( 7rem, -6rem) scale(1.04); }
  56%      { transform:translate(-5rem, -6rem) scale(1.04); }
  76%      { transform:translate( 4rem, -6rem) scale(1.04); }
  100%     { transform:translate(0,    -6rem)  scale(1.04); }
}
@keyframes choice-yes{
  0%   { transform:translateY(0)      scale(1); }
  30%  { transform:translateY(-14rem) scale(1.10); }
  60%  { transform:translateY(-4rem)  scale(1.02); }
  100% { transform:translateY(-6rem)  scale(1.04); }
}
@keyframes choice-letter-spin{
  0%   { transform:rotate(0)      scale(1); }
  60%  { transform:rotate(220deg) scale(1.20); }
  100% { transform:rotate(360deg) scale(1.06); }
}

/* ─────────────────────────────────────────
   SLIDER — same flex layout as MCQ
   ───────────────────────────────────────── */
.module-slider{
  position:absolute;inset:0;display:flex;flex-direction:column;
}
.module-slider > .activity-header{position:relative;flex:0 0 64rem}
.module-slider > .activity-footer{position:relative;flex:0 0 85rem}
.slider-centre{
  flex:1 1 0;min-height:0;overflow:hidden;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:40rem;padding:12rem 48rem;position:relative;z-index:1;
}
/* No card — content sits directly on the paper background (paperBg behind
   the section), same treatment as .scale-panel. Prompt/sub type matches the
   Scale's paper sizing so the two slider activities read as one family.
   Vertical padding is slimmer than the Scale's: it was the card's inset, and
   a 42rem prompt + subtext + a max-height scene image must still clear the
   footer (effect-demo step 16 is the tightest authored case). */
.slider-panel{
  background:transparent;padding:8rem 40rem 12rem;
  display:flex;flex-direction:column;align-items:center;gap:16rem;
  width:100%;max-width:1824rem;
}
.slider-panel .prompt{
  font:700 42rem/54rem "Gotham Rounded","Nunito",sans-serif;color:var(--kohl);
  text-align:center;margin:0;max-width:1500rem;
}
.slider-panel .sub{
  font:500 20rem/30rem "Gotham Rounded","Nunito",sans-serif;color:rgba(0,0,0,.6);
  text-align:center;margin:0;
}
/* 16rem panel gap keeps prompt+sub as one text block; the row's own 28rem
   top margin lands the prompt→widget rhythm on the Scale's 44rem beat. */
.slider-row{display:flex;align-items:center;justify-content:space-between;width:100%;padding:0 8rem;margin-top:28rem}

/* ── Endpoint cards — affinity-driven response ──────────────────────────
   --affinity is set per side by JS (0..1) as the knob moves. At 0 the card
   sits at rest; at 1 it has lifted, scaled, brightened, and a coloured
   ring. The continuous mapping replaces the previous binary .is-near
   threshold so the destination "leans toward you" gradually. */
.slider-endpoint{
  position:relative;
  width:300rem;border-radius:20rem;border:1rem solid var(--stroke);
  display:flex;align-items:center;justify-content:center;
  padding:22rem 20rem;background:#fff;
  --affinity:0;
  --ring-rgb:0,0,0;
  transform:translateY(calc(-10rem * var(--affinity))) scale(calc(1 + 0.07 * var(--affinity)));
  filter:brightness(calc(1 + 0.06 * var(--affinity))) saturate(calc(0.85 + 0.15 * var(--affinity)));
  box-shadow:
    0 calc(4rem + 16rem * var(--affinity)) calc(8rem + 18rem * var(--affinity)) rgba(0,0,0,calc(0.08 + 0.10 * var(--affinity))),
    0 0 0 calc(3rem * var(--affinity)) rgba(var(--ring-rgb), 0.45);
  cursor:pointer;
}
.slider-endpoint .label{font:700 20rem/1.25 "Gotham Rounded","Nunito",sans-serif;color:#000;text-align:center;width:100%}
.slider-endpoint.left  {background:var(--aamras);--ring-rgb:255,163,0;}
.slider-endpoint.right {background:var(--ferozi);--ring-rgb:92,184,178;}
/* Brief flash ring on commit — pseudo-element so it doesn't fight the
   affinity-driven box-shadow on the card itself. */
.slider-endpoint::after{
  content:'';position:absolute;inset:-3rem;border-radius:inherit;pointer-events:none;
  opacity:0;
}
.slider-endpoint.is-just-locked::after{
  animation:endpoint-just-locked 0.7s ease-out;
}
@keyframes endpoint-just-locked{
  0%   { box-shadow:0 0 0 0   rgba(var(--ring-rgb), 0.55); opacity:1; }
  100% { box-shadow:0 0 0 28rem rgba(var(--ring-rgb), 0);    opacity:0; }
}

/* ── Track — flat recessed groove with a matte fill toward the knob ─────
   The bar reads as a neutral channel at rest (the endpoint bubbles carry
   the side colours); a FLAT amber / ferozi bar grows from the centre to
   the knob as it leans. The fill is the track's two pseudo-elements,
   clipped by --knobx (the knob's live left %, mirrored by paintAffinity
   in slider.js) so it always kisses the knob — even past the affinity
   saturation points and through the spring-lag overshoot. Inset shadows
   on groove AND fill keep the whole bar recessed and matte — no
   gradients, no glow. An equilibrium tick anchors the centre and fades
   when leaning. */
.slider-track{
  flex:1;
  height:14rem;
  position:relative;
  margin:0 24rem;
  border-radius:999rem;
  background:rgba(58,42,28,.14);
  box-shadow:inset 0 2rem 5rem rgba(0,0,0,.18);
  /* Drag affordance — tapping anywhere on the bar springs the knob to
     that point and a hold drags it (see slider.js trackEl.pointerdown),
     so the bar wears the same open/closed hand as the knob itself. */
  cursor:var(--cur-grab);
  --knobx:50%;
}
.slider-track:active,
.slider-track.is-dragging{cursor:var(--cur-grabbing)}
.slider-track::before,
.slider-track::after{
  content:'';position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  /* Matte fill body — inner top shade seats it IN the groove, a soft
     under-lift keeps it from going muddy. Painted then clipped, so the
     shadow rides whatever sliver of bar is showing. */
  box-shadow:inset 0 2rem 3rem rgba(58,42,28,.30), inset 0 -1rem 2rem rgba(255,255,255,.28);
}
.slider-track::before{
  background:var(--aamras);
  clip-path:inset(0 50% 0 min(50%, var(--knobx)) round 999rem);
}
.slider-track::after{
  background:var(--ferozi);
  clip-path:inset(0 min(50%, calc(100% - var(--knobx))) 0 50% round 999rem);
}
.slider-track .equilibrium{
  position:absolute;left:50%;top:50%;
  width:2rem;height:18rem;border-radius:1rem;
  background:rgba(0,0,0,0.22);
  transform:translate(-50%,-50%);pointer-events:none;
  transition:opacity .25s var(--ease);
}
.slider-track[data-leaning] .equilibrium{opacity:0}

/* ── Knob — matte orb ───────────────────────────────────────────────────
   Inner radial gradient gives volume; a plain drop shadow seats it on
   the bar. No glow of any kind behind the knob (2026-07-09 user call —
   the rotating rainbow halo AND the affinity-tinted bloom are gone);
   leaning feedback lives in the flat fill + endpoint bubbles. At rest
   the ring is kohl — same black-ringed language as the footer pills
   (2026-07-09 user call); it lightens to the stroke while held. */
.slider-track .knob{
  position:absolute;top:50%;left:50%;width:48rem;height:48rem;border-radius:999rem;
  background:radial-gradient(circle at 35% 30%, #ffffff 0%, #f8f6f0 60%, #e8e2d9 100%);
  border:3rem solid var(--kohl);
  transform:translate(-50%,-50%);
  box-shadow:
    0 6rem 14rem rgba(0,0,0,.18),
    inset 0 -2rem 4rem rgba(0,0,0,0.08);
  /* Transform transition uses --ease-boing (overshoot) so the grab-up
     to scale(1.12) on .is-dragging and the shrink-back when it lifts both
     feel springy. The .is-settling release keyframe (below) overrides
     this transition with a richer multi-bounce when the user lets go
     after a drag — five stops, two visible bounces, more playful. */
  transition:transform 0.22s var(--ease-boing), border-color var(--t-fast) var(--ease);
  z-index:3;touch-action:none;cursor:var(--cur-grab);
  will-change:left, transform;
}
.slider-track .knob:active,
.slider-track.is-dragging .knob{
  cursor:var(--cur-grabbing);
  /* Knob scales up while held — tactile "you're gripping me" feedback,
     also gives the release a clear distance to spring back from. Bigger
     grab (1.12) makes the held state more obvious and gives the release
     keyframe more room for a playful compress / overshoot / settle.
     The resting kohl ring lightens while held — the grab visibly wakes
     the knob (black = idle, per the 2026-07-09 user call). */
  transform:translate(-50%,-50%) scale(1.12);
  border-color:var(--stroke);
  box-shadow:
    0 8rem 20rem rgba(0,0,0,.22),
    inset 0 -2rem 4rem rgba(0,0,0,0.10);
}
/* Release spring — multi-bounce compress / overshoot / micro-overshoot /
   settle. Triggered by JS in endDrag (adds .is-settling, removes after
   the animation finishes so it can re-fire on the next release). 0%
   matches the dragging scale(1.12) so there's no jump when .is-dragging
   is removed the same frame .is-settling is added; 100% matches the
   resting scale(1) so the keyframe lands cleanly on the base style.
   Five stops give two visible bounces (big and small) before resting —
   reads as playful weight without feeling jittery. JS timeout is 750ms
   to outrun the 700ms animation so .is-settling isn't pulled mid-bounce. */
@keyframes knob-settle{
  0%   { transform:translate(-50%,-50%) scale(1.12); }
  18%  { transform:translate(-50%,-50%) scale(0.88); }
  40%  { transform:translate(-50%,-50%) scale(1.10); }
  60%  { transform:translate(-50%,-50%) scale(0.96); }
  82%  { transform:translate(-50%,-50%) scale(1.03); }
  100% { transform:translate(-50%,-50%) scale(1); }
}
.slider-track .knob.is-settling{
  animation:knob-settle 0.7s ease-out;
}

/* ── Idle "slide me" hint ───────────────────────────────────────────────
   While .idle-hint is on the knob (armed once the entrance settles,
   removed on the first touch) two things say "this slides": the knob
   rocks side to side, and a kohl chevron on EACH side pulses outward in
   phase with the rock — the right one pushes as the knob sways right,
   the left one as it sways left (same keyframe; the left runs half a
   period out of phase via a negative delay). Chevrons are pseudo-elements
   ON the knob so they ride every knob move for free and vanish with the
   class — no JS position syncing. The Scale handle shares all of this and
   only bumps the chevron size (--chev/--chev-b in its own block). */
.slider-track .knob.idle-hint,
.scale-track  .knob.idle-hint{animation:knob-wiggle 1.2s ease-in-out infinite}
@keyframes knob-wiggle{
  0%, 100% { transform:translate(-50%,-50%) translateX(-7rem); }
  50%      { transform:translate(-50%,-50%) translateX( 7rem); }
}
.slider-track .knob.idle-hint::before,
.slider-track .knob.idle-hint::after,
.scale-track  .knob.idle-hint::before,
.scale-track  .knob.idle-hint::after{
  content:'';
  position:absolute;top:50%;
  width:var(--chev,15rem);height:var(--chev,15rem);
  /* Two borders of a rotated box draw the chevron — no glyph, no font. */
  border-top:var(--chev-b,5rem) solid var(--kohl);
  border-right:var(--chev-b,5rem) solid var(--kohl);
  border-radius:2rem;
  opacity:.85;
  transform:translate(0,-50%) rotate(var(--r));
  pointer-events:none;
  animation:knob-hint-chevron 1.2s ease-in-out infinite;
}
.slider-track .knob.idle-hint::after,
.scale-track  .knob.idle-hint::after{   /* points right, pushes right */
  left:100%;margin-left:10rem;--r:45deg;--d:1;
}
.slider-track .knob.idle-hint::before,
.scale-track  .knob.idle-hint::before{  /* points left, pushes left */
  right:100%;margin-right:10rem;--r:-135deg;--d:-1;
  animation-delay:-.6s;   /* half a period behind — peaks on the left sway */
}
@keyframes knob-hint-chevron{
  0%, 100% { opacity:.3; transform:translate(0,-50%) rotate(var(--r)); }
  50%      { opacity:1;  transform:translate(calc(var(--d) * 10rem),-50%) rotate(var(--r)); }
}
/* "Move this" plaque — the hint mascot (hint-mascot.js) plus a spoken
   bubble, riding the knob while the hint is on (same .idle-hint gate:
   pops with the chevrons, dies with the class on the first touch — no
   extra JS). Identical child markup in both modules (HintMascot.plaque);
   the geometry differs because the knobs rest in different spots:
     · slider knob rests at track CENTRE → plaque UNDERNEATH the knob
       (clear panel space there), mascot biased under the orb so its
       up-tilted magnifier inspects it, bubble speaking to the right;
     · scale handle rests at the track's LEFT END → underneath is the
       stop-label row, so the mascot STANDS ON the knob (feet at the
       orb) with the bubble out over the open track. Clearance above
       the scale knob is ≥60rem (track margin 46 + widget pad 14) even
       with a subtext line authored — 54rem figure + 2rem gap + 4rem
       bob peak = 60rem, right at the line (the bigger bubble rides
       higher but sits over the open track, not the labels). */
.slider-track .knob .knob-say,
.scale-track  .knob .knob-say{ display:none; }
.slider-track .knob.idle-hint .knob-say,
.scale-track  .knob.idle-hint .knob-say{
  position:absolute;left:50%;z-index:5;
  display:flex;
  /* A tap on the plaque falls through to the track — an interaction,
     which kills the hint anyway. */
  pointer-events:none;
  animation:knob-say-in .4s var(--ease-boing) backwards;
}
.slider-track .knob.idle-hint .knob-say{ top:100%;margin-top:6rem;transform:translateX(-24%); }
.scale-track  .knob.idle-hint .knob-say{ bottom:100%;margin-bottom:2rem;transform:translateX(-16%); }
/* 68rem keeps the figure's dangling legs inside .slider-centre's clip box
   now the white card is gone — the tightest authored step (subtext + a
   max-height scene image, effect-demo 16) clipped the feet at 88rem. The
   bubble lift shrinks with it so the fig stays the plaque's height driver. */
.slider-track .knob-say .hm-fig{ height:68rem;width:68rem; }
.slider-track .knob-say .hm-bubble{ margin-bottom:12rem; }
.scale-track  .knob-say .hm-fig{ height:54rem;width:54rem; }
.scale-track  .knob-say .hm-bubble{ margin-bottom:10rem; }
@keyframes knob-say-in{
  from { opacity:0; scale:.7; }
  to   { opacity:1; scale:1; }
}

/* ── Hint mascot (shared: knob plaques + sequence guide) ────────────────
   Inline SVG from hint-mascot.js. The figure bobs and tilts, pupils dart
   on a lazy loop (small excursions — the magnified left eye must stay
   inside its lens), and the bubble floats half a beat behind. All transform/opacity only — no filters
   (kiosk rule: nothing that animates blurs). The pupils are animated via
   their <g class="hm-pupil"> wrappers because the ellipses carry
   attribute transforms a CSS transform would clobber. px values inside
   the SVG are user units, so they scale with the rendered size. */
.hm-plaque{ display:flex;align-items:flex-end;gap:12rem; }
/* Every context gives .hm-fig BOTH height and width (the art is square
   110×110, so width = height). Firefox computes a flex row's intrinsic widths BEFORE
   percentage heights resolve, so a fig whose width derives from its
   height (svg auto / aspect-ratio) measures 0 wide in that pass — the
   plaque then under-reports its width and whatever follows in the row
   lands on the bubble (2026-07-07 laptop bug). Definite widths, and the
   svg just fills its box. */
.hm-fig{ flex:none;animation:hm-bob 2.4s ease-in-out infinite; }
.hm-fig .hm{ height:100%;width:100%;overflow:visible; }
.hm-pupil{ animation:hm-pupil-dart 4.2s ease-in-out infinite; }
.hm-bubble{
  position:relative;margin-bottom:14rem;
  flex:none;   /* never let any engine squeeze the bubble under its text */
  /* 3rem border: Chrome floors fractional border widths to whole device
     px (2rem computes 1.99px at kiosk scale and paints 1px) — 3rem still
     paints 2px on ~0.7× laptops. */
  background:var(--paper);border:3rem solid var(--kohl);border-radius:16rem;
  padding:11rem 20rem 12rem;
  font:700 24rem/1.25 "Gotham Rounded","Nunito",sans-serif;color:var(--kohl);
  white-space:nowrap;box-shadow:var(--sh-card);
  animation:hm-bubble-bob 2.4s ease-in-out infinite;animation-delay:-.3s;
}
.hm-bubble::before{  /* tail — aims left at the speaker */
  content:"";position:absolute;left:-7.5rem;bottom:12rem;width:13rem;height:13rem;
  background:var(--paper);
  border-left:3rem solid var(--kohl);border-bottom:3rem solid var(--kohl);
  transform:rotate(45deg);
}
/* Bob peaks at −4rem: the scale plaque budget (fig 54 + gap 2 + bob 4)
   lands exactly on the ≥60rem clearance above its knob. */
@keyframes hm-bob{ 0%,100%{ translate:0 0;rotate:-2deg; } 50%{ translate:0 -4rem;rotate:2.5deg; } }
@keyframes hm-bubble-bob{ 0%,100%{ translate:0 0; } 50%{ translate:0 -2.5rem; } }
@keyframes hm-pupil-dart{
  0%,42%,100%{ transform:translate(0,0); }
  48%,60%    { transform:translate(2.2px,-1px); }
  66%,72%    { transform:translate(-1.6px,1.2px); }
  80%,88%    { transform:translate(.6px,-.4px); }
}
@media (prefers-reduced-motion: reduce){
  /* Mascot becomes a STATIC affordance; the hint gates still retire it. */
  .hm-fig,.hm-bubble,.hm-pupil{ animation:none; }
}

/* Knob trail — ghost circles that lerp behind during drag. JS positions
   them and writes --trail-opacity per ghost; CSS reveals them only while
   dragging so they don't clutter the resting state. */
.slider-track .knob-trail{
  position:absolute;top:50%;width:42rem;height:42rem;border-radius:999rem;
  background:radial-gradient(circle at 35% 30%, rgba(255,255,255,0.7), rgba(232,226,217,0.35));
  transform:translate(-50%,-50%);
  pointer-events:none;
  z-index:2;
  opacity:0;
  transition:opacity 0.18s var(--ease);
  --trail-opacity:0.3;
}
.slider-track.is-dragging .knob-trail{opacity:var(--trail-opacity)}

/* Commit ring — single-shot expanding ring on lock. JS appends, animates,
   removes. currentColor inherits from inline style set at spawn time. */
.slider-track .commit-ring{
  position:absolute;top:50%;width:48rem;height:48rem;border-radius:999rem;
  transform:translate(-50%,-50%) scale(0);
  pointer-events:none;z-index:1;
  border:3rem solid currentColor;
  opacity:0.7;
}

@media (prefers-reduced-motion: reduce){
  .slider-track .knob.idle-hint{animation:none}
  /* Chevrons + mascot plaque stay as a STATIC affordance — no pulse,
     still removed on the first touch along with the class. (The mascot's
     own bob/pupil/leg loops are stilled in the shared .hm reduce block.) */
  .slider-track .knob.idle-hint::before,
  .slider-track .knob.idle-hint::after,
  .slider-track .knob.idle-hint .knob-say{animation:none}
  .slider-track .knob-trail{display:none}
  .slider-endpoint.is-just-locked::after{animation:none}
}

/* ─────────────────────────────────────────
   FEEDBACK overlay (1:899 / 1:955)
   ───────────────────────────────────────── */
.overlay{position:absolute;inset:0;z-index:10;display:grid;place-items:center;padding:10rem;}
.overlay-feedback{background:rgba(0,0,0,.72)}

/* Slider lean reveal — uses the MCQ ring pattern (tinted circle + border)
   so the slider feedback shares the same visual language as MCQ feedback,
   just sized up so the side's icon reads clearly. Side-tint defaults to
   aamras (left); .is-side-right overrides to ferozi. */
.feedback-modal .lean-image{
  width:140rem;height:140rem;border-radius:999rem;
  background:rgba(255,163,0,.15);border:3rem solid var(--aamras);
  display:grid;place-items:center;flex-shrink:0;
}
.feedback-modal .lean-image img{
  width:78rem;height:78rem;object-fit:contain;display:block;
}
.feedback-modal.is-side-right .lean-image{
  background:rgba(92,184,178,.15);border-color:var(--ferozi);
}

/* ─── Lean reveal — atmospheric variant ────────────────────────────────
   Triggered by .is-lean-reveal on the overlay (and the same on the modal).
   Overlay background swaps from solid black to a side-tinted radial that
   breathes; modal becomes a frosted card; the icon hosts an inline SVG
   that draws in via stroke-dasharray, then fills, then floats ambiently.
   ─────────────────────────────────────────────────────────────────────── */
.overlay-feedback.is-lean-reveal{
  background:#0a0908;
  overflow:hidden;
}
.overlay-feedback.is-lean-reveal::before{
  content:'';position:absolute;inset:-15%;pointer-events:none;
  z-index:0;
  animation:lean-bg-breathe 6s ease-in-out infinite;
}
.overlay-feedback.is-lean-reveal.is-side-left::before{
  background:radial-gradient(ellipse at center,
    rgba(255,163,0,0.42) 0%,
    rgba(255,163,0,0.18) 30%,
    rgba(0,0,0,0.85) 70%);
}
.overlay-feedback.is-lean-reveal.is-side-right::before{
  background:radial-gradient(ellipse at center,
    rgba(92,184,178,0.42) 0%,
    rgba(92,184,178,0.18) 30%,
    rgba(0,0,0,0.85) 70%);
}
@keyframes lean-bg-breathe{
  0%, 100% { transform:scale(1);    opacity:1;    }
  50%      { transform:scale(1.08); opacity:0.92; }
}
.overlay-feedback.is-lean-reveal .feedback-modal{
  position:relative;z-index:1;
}

/* Modal: frosted card so the side-tinted atmosphere shows through. */
.feedback-modal.is-lean-reveal{
  background:rgba(255,255,255,0.96);
  backdrop-filter:blur(10rem);
  -webkit-backdrop-filter:blur(10rem);
  box-shadow:0 20rem 60rem rgba(0,0,0,.40), 0 4rem 14rem rgba(0,0,0,.18);
}

/* Lean-image host — bigger, side-glow background, no hard border. The
   inline SVG sits at 120×120 and animates inside this halo. */
.feedback-modal.is-lean-reveal .lean-image{
  width:200rem;height:200rem;border:0;
  display:grid;place-items:center;
  position:relative;flex-shrink:0;
  background:rgba(255,163,0,0.10);
}
.feedback-modal.is-lean-reveal.is-side-right .lean-image{
  background:rgba(92,184,178,0.10);
}
.feedback-modal.is-lean-reveal .lean-image svg,
.feedback-modal.is-lean-reveal .lean-image img{
  width:120rem;height:120rem;display:block;
  position:relative;z-index:2;
  will-change:transform;
}

/* Sparkle bloom — small dots that fly outward from the icon centre. The
   layer is a 0×0 anchor at the icon's centre; bits get x/y tweens to
   escape outward. Tinted via inline `background` written by JS. */
.feedback-modal.is-lean-reveal .sparkle-layer{
  position:absolute;left:50%;top:50%;width:0;height:0;
  pointer-events:none;z-index:3;
}
.feedback-modal.is-lean-reveal .sparkle-bit{
  position:absolute;left:0;top:0;
  width:8rem;height:8rem;border-radius:999rem;
  transform:translate(-50%,-50%);
  box-shadow:0 0 12rem currentColor;
  will-change:transform,opacity;
}

/* Word stagger — JS splits both the title and body into per-word spans;
   each animates from y:14 / opacity:0 into place. inline-block so the
   transform applies; whitespace between spans stays as raw text so words
   wrap naturally on long titles or bodies. */
.feedback-modal.is-lean-reveal h2 .word,
.feedback-modal.is-lean-reveal .body .word{
  display:inline-block;will-change:transform,opacity;
}

/* Hero text column gets left-padding when icon is at-large; keeps the
   layout balanced when title is long. */
.feedback-modal.is-lean-reveal .hero-text{
  display:flex;flex-direction:column;align-items:center;gap:18rem;
}

@media (prefers-reduced-motion: reduce){
  .overlay-feedback.is-lean-reveal::before{animation:none}
  .feedback-modal.is-lean-reveal h2 .word,
  .feedback-modal.is-lean-reveal .body .word{display:inline}
  .feedback-modal.is-lean-reveal .sparkle-layer{display:none}
}

.feedback-modal{
  background:#fff;border-radius:24rem;padding:40rem;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:56rem;max-width:1200rem;box-shadow:var(--sh-modal);
  text-align:center;
}
.feedback-modal .hero{display:flex;flex-direction:column;align-items:center;gap:32rem;padding:0 16rem}
/* The verdict slot. Was a 76rem tinted badge circle around a small check/X
   glyph; the verdict is now a full blob character (feedback-icons.js) who
   brings his own pink face — no chrome, just a stage. overflow:visible lets
   the flung arms + sparkles animate past the viewBox without clipping. */
.feedback-modal .ring{display:grid;place-items:center}
.feedback-modal .ring svg{width:auto;display:block;overflow:visible}
/* Heights tuned so BOTH characters' faces render the same diameter (~82rem,
   near the old ring's 76rem footprint): the correct art carries taller
   sparkle headroom (viewBox 113.24) than the wrong art (93.37). */
.feedback-modal .ring .fbk-ok{height:134rem}
.feedback-modal .ring .fbk-no{height:110rem}
/* In the scored hero ROW the character sits inline with title + points pill —
   compact so the row stays a row. Same face-parity ratio. */
.feedback-modal.is-fact-card.is-scored .ring .fbk-ok{height:102rem}
.feedback-modal.is-fact-card.is-scored .ring .fbk-no{height:84rem}
/* Legacy fallback if feedback-icons.js is absent (old check/x imgs). */
.feedback-modal .ring img{height:28rem;width:auto;display:block}
.feedback-modal h2{font:700 32rem/48rem "Gotham Rounded","Nunito",sans-serif;margin:0;color:#000}
/* In the centred layout, subtitle carries the feedback message (a journey
   with image-only feedback puts its text here so body stays empty). Styled
   as a readable paragraph — the small amber accent it once was is unused. */
.feedback-modal .subtitle{font:500 24rem/1.3 "Gotham Rounded","Nunito",sans-serif;color:var(--ink-mute);margin:10rem 0 0;text-align:center;max-width:980rem}
.feedback-modal .body{font:500 24rem/1.25 "Gotham Rounded","Nunito",sans-serif;color:var(--ink-mute);text-align:center;max-width:980rem}
/* Centred-layout section gaps. .hero-copy is simple-layout only, so the
   fact-card and lean-reveal bodies stay untouched. Base 10rem = body pairs
   with the title as the message; under a subtitle the body is the FACT — a
   new section — so it (or its tag pill, which then hugs its text) gets
   clear air above instead of jamming into the message. */
.feedback-modal .hero-copy .body{margin-top:10rem}
.feedback-modal .hero-copy .subtitle+.body{margin-top:24rem}
.feedback-modal .hero-copy .fact-card-label{margin-top:28rem}
.feedback-modal .hero-copy .fact-card-label+.body{margin-top:12rem}
.feedback-modal .caption{font:500 14rem/1.4 "Gotham Rounded","Nunito",sans-serif;color:rgba(0,0,0,.45);text-align:center;margin:12rem 0 0;letter-spacing:.2rem}
/* Centred fact image — shown in the centred feedback layout when there's a
   feedback image but no body text to pair it beside. De-matted like the
   kiosk's other images; capped so the modal never outgrows the stage. */
.feedback-modal .feedback-figure{
  border-radius:6rem;
  box-shadow:var(--sh-img);
  display:flex;align-items:center;justify-content:center;flex-shrink:0;
}
.feedback-modal .feedback-figure img{
  display:block;width:auto;height:auto;
  max-width:480rem;max-height:280rem;border-radius:6rem;
}
.feedback-modal .selected-card{display:inline-flex}
/* The correct-answer card shown in incorrect feedback is a display, not a
   control — make it non-interactive (no hover lift, no pointer cursor, not
   clickable). It still gets its GSAP entry + spotlight pulse.
   transition:none — the base .choice carries a 180ms box-shadow/border/
   background transition for hover states; this card has no hover, and the
   transition smears every frame of the GSAP spotlight tween (the glow
   trails ~180ms behind and never reads crisp). GSAP owns this card. */
.feedback-modal .selected-card .choice{
  pointer-events:none;cursor:default;transition:none;
  /* Saturated teal keyline — ferozi, matching the correct (is-right)
     treatment on the question screens and the GSAP spotlight glow. With a
     cream border the glow's translucent teal ring blended into muddy beige,
     so the keyline goes full-alpha ferozi. */
  border-color:var(--ferozi);
}
/* When the correct answer card carries an image, render the fill-tile at a
   compact size so it fits the modal alongside the title + body. The .img
   inherits the base absolute-inset cover + the badge stays bottom-left. */
.feedback-modal .selected-card .choice.is-image{width:300rem;height:225rem;padding:0;gap:0}
/* #8: the recap card is non-interactive (pointer-events:none above), but that
   inherited value also silences the corner magnifier — the one control on this
   card that IS meant to be tapped (opens the correct image fullscreen). Re-arm
   just the magnifier so its click reaches the app.js zoom-option handler. */
.feedback-modal .selected-card .choice-mag{pointer-events:auto;cursor:pointer}
.feedback-modal .cta-continue{margin-top:0}

/* ─── Fact-card variant ───
   Rich feedback layout used when the journey scores or the feedback
   carries a credit caption. Hero is a single horizontal row (ring +
   title/subtitle + points pill); below it sits a fact card with the
   MCQ image on the left and a labelled body + credit on the right.
   Triggered by .is-fact-card on .feedback-modal — the simple stacked
   layout above stays intact for journeys that don't score and don't
   provide captions. */
.feedback-modal.is-fact-card{
  /* Fixed width so the modal doesn't grow to whatever the body wraps to.
     Wide enough to seat: 40rem padding + ~535rem image frame + 24rem gap +
     ~445rem content column + 40rem padding ≈ 1084 → 1100. */
  width:1100rem;
  max-width:1100rem;
  gap:28rem;
  /* Default the modal's children to centered (parent .feedback-modal
     already does this); the hero and fact card opt back into full width
     via align-self:stretch below. */
  align-items:center;
  padding:32rem 40rem;
}
/* Hero + fact card span the modal; everything else (selected card,
   continue button wrapper) is content-sized and centered by the parent
   align-items:center. */
.feedback-modal.is-fact-card .hero,
.feedback-modal.is-fact-card .fact-card{
  align-self:stretch;
}
/* Restore left-aligned text only inside the columns that need it. The
   modal-level text-align:center keeps the continue button's label
   centered as inline content. */
.feedback-modal.is-fact-card .fact-card-content{
  text-align:left;
}
/* Hero defaults to a CENTRED column — ring above a centred title — inherited
   from the base .feedback-modal .hero. Only .is-scored (the points pill is
   present) opts into the horizontal row below, where the title goes left and
   the pill takes the right. User 2026-06-16: "ring + title centred always,
   unless scoring is active" — so a non-scoring journey that still lands in the
   fact-card layout (caption, or image+body, e.g. curious-travels q5) now
   centres its ring + title instead of left-aligning them. */
.feedback-modal.is-fact-card .hero{
  gap:20rem;width:100%;padding:0 12rem;
}
.feedback-modal.is-fact-card .hero h2{
  font:700 32rem/1 "Gotham Rounded","Nunito",sans-serif;color:#000;margin:0;
}
.feedback-modal.is-fact-card .hero-text,
.feedback-modal.is-fact-card .hero-text>*{
  /* centred flex items aren't cross-size-capped by their container —
     without this a long title computes wider than the hero and escapes */
  max-width:100%;
}
.feedback-modal.is-fact-card .hero-text{
  display:flex;flex-direction:column;align-items:center;gap:6rem;
}
.feedback-modal.is-fact-card .hero-text .subtitle{
  font:500 20rem/1.25 "Gotham Rounded","Nunito",sans-serif;
  color:var(--ink-mute);margin:0;
}
.feedback-modal.is-fact-card .hero-spacer{display:none}
/* Scored: ring · title · spacer · points-pill in one left-aligned row. */
.feedback-modal.is-fact-card.is-scored .hero{
  flex-direction:row;align-items:center;justify-content:flex-start;
}
.feedback-modal.is-fact-card.is-scored .hero-text{
  /* min-width:0 (not nowrap): a long title wraps left-aligned inside the
     row instead of pushing the points pill out of the modal. */
  align-items:flex-start;text-align:left;min-width:0;
}
.feedback-modal.is-fact-card.is-scored .hero-spacer{
  display:block;flex:1 1 auto;min-width:1rem;
}
.feedback-modal.is-fact-card .points-pill{
  display:inline-flex;align-items:center;gap:10rem;
  border:1.5rem solid var(--gulabi);color:var(--gulabi);background:transparent;
  border-radius:999rem;padding:10rem 18rem;
  font:700 20rem/1 "Gotham Rounded","Nunito",sans-serif;
  white-space:nowrap;
}
.feedback-modal.is-fact-card .points-pill.is-zero{
  border-color:var(--ink-mute);color:var(--ink-mute);
}

.feedback-modal.is-fact-card .fact-card{
  display:flex;align-items:stretch;gap:24rem;
  width:100%;padding:8rem 0;
}
.feedback-modal.is-fact-card .fact-card-image{
  /* De-matted (2026-06-12): bare photo, shadow on its edge. */
  box-shadow:var(--sh-img);
  flex-shrink:0;
  height:379rem;
  display:flex;align-items:center;justify-content:center;
}
.feedback-modal.is-fact-card .fact-card-image img{
  display:block;height:100%;width:auto;max-width:none;
}
.feedback-modal.is-fact-card .fact-card-content{
  flex:1 1 auto;min-width:1rem;
  display:flex;flex-direction:column;justify-content:space-between;
  padding:16rem 0;gap:16rem;
}
.feedback-modal.is-fact-card .fact-card-stack{
  display:flex;flex-direction:column;gap:12rem;
}
/* Tag pill — unscoped from .is-fact-card: the centred layout prints it
   above the fact text too (feedback.js renderSimple). */
.feedback-modal .fact-card-label{
  display:flex;align-items:center;gap:8rem;
  font:700 20rem/1 "Gotham Rounded","Nunito",sans-serif;color:var(--gulabi);
  letter-spacing:1.6rem;text-transform:uppercase;
}
.feedback-modal .fact-card-label .dot{
  width:8rem;height:8rem;border-radius:999rem;background:var(--gulabi);
  flex:0 0 auto;
}
/* The tag text may wrap — without min-width:0 a long tag holds the flex
   row at its full width and slides out of the card. */
.feedback-modal .fact-card-label span+span{
  min-width:0;overflow-wrap:break-word;
}
.feedback-modal:not(.is-fact-card) .fact-card-label{justify-content:center}
.feedback-modal.is-fact-card .fact-card-content .body{
  font:500 20rem/1.4 "Gotham Rounded","Nunito",sans-serif;color:#000;
  margin:0;text-align:left;max-width:none;
}
.feedback-modal.is-fact-card .fact-card-credit{
  border-top:1rem solid var(--stroke);padding-top:12rem;
}
.feedback-modal.is-fact-card .fact-card-credit .caption{
  font:500 16rem/1.4 "Gotham Rounded","Nunito",sans-serif;
  color:var(--ink-mute);margin:0;text-align:left;
}
/* Selected-answer card on incorrect — centered horizontally like the
   continue button. Inherits gap from the modal's flex column. */
.feedback-modal.is-fact-card .selected-card{
  align-self:center;
}

/* ─────────────────────────────────────────
   COMPLETE (1:1103)
   ───────────────────────────────────────── */
.module-complete{position:absolute;inset:0}
.module-complete .bg{position:absolute;inset:0}
.module-complete .bg img{width:100%;height:100%;object-fit:cover;display:block}
.module-complete .dim{position:absolute;inset:0;background:rgba(0,0,0,.72)}
.complete-card{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%);
  background:#fff;border-radius:24rem;padding:56rem 56rem 48rem;
  display:flex;flex-direction:column;align-items:center;gap:24rem;
  width:767rem;box-shadow:0 24rem 60rem rgba(0,0,0,.18), 0 2rem 6rem rgba(0,0,0,.06);
  text-align:center;
}
.complete-card .medal{width:128rem;height:128rem;position:relative;will-change:transform}
/* Ambient halo — radial gradient on a sibling pseudo, with opacity-only
   keyframe pulsing. Replaces an animated filter:drop-shadow tween (which
   re-rasterizes the medal every frame) with pure compositor work. */
.complete-card .medal::before{
  content:'';position:absolute;inset:-28rem;border-radius:50%;
  background:radial-gradient(circle,
    rgba(var(--result-rgb,255,180,60),.55) 0%,
    rgba(var(--result-rgb,255,180,60),0) 65%);
  pointer-events:none;z-index:-1;opacity:0;
  animation:medal-halo 1.6s ease-in-out infinite alternate;
  will-change:opacity;
}
@keyframes medal-halo{ from{opacity:0} to{opacity:1} }
@media (prefers-reduced-motion: reduce){
  .complete-card .medal::before{animation:none;opacity:.55}
}
.complete-card .medal img,
.complete-card .medal svg{width:100%;height:100%;display:block}
/* Author-uploaded celebration graphic (journey-end `graphic`) — replaces the
   medal svg in the same slot, so the pop/drum-roll/bob choreography and halo
   apply unchanged. Artwork always FITs, never crops. */
.complete-card .medal .medal-img{object-fit:contain}
.complete-card .medal .medal-ring{
  transform-box:fill-box;transform-origin:center;
  will-change:transform;
}
.complete-card .chip{
  background:var(--result-color, var(--aamras));color:#fff;border-radius:999rem;
  /* Right pad is left pad minus the tracking: letter-spacing trails the
     last letter too, so equal padding reads text-shoved-left. */
  padding:13rem 18.1rem 13rem 20rem;font:700 16rem/1 "Gotham Rounded","Nunito",sans-serif;
  letter-spacing:1.92rem;
}
.complete-card h2{font:700 40rem/1.15 "Gotham Rounded","Nunito",sans-serif;color:#1A1A1A;margin:0}
/* Score summary line — only rendered when journey.scoring.enabled. Sits
   between the title and the divider so the eye reads the achievement
   before the body explains it. */
.complete-card .score-summary{
  display:inline-flex;align-items:baseline;gap:10rem;
  font:500 22rem/1 "Gotham Rounded","Nunito",sans-serif;color:var(--ink-mute);
}
.complete-card .score-summary strong{
  color:var(--aamras);font-weight:700;font-size:44rem;line-height:1;
}
.complete-card .divider{width:62rem;height:8rem}
.complete-card .divider img{width:100%;height:100%;object-fit:contain}
.complete-card .body{font:500 24rem/1.4 "Gotham Rounded","Nunito",sans-serif;color:#3D3D3D;width:607rem}
/* CTA row — single button stays centered (margin:auto via inline-flex);
   when a secondary CTA is present, both buttons sit side-by-side.
   Gap is set wide enough (64rem) that the magnetic hover effect on each
   button (data-magnetic 0.35 on .cta-secondary, 0.4 on .cta-discover →
   each can swing up to ~20% of its own width toward a neighbour, ≈ 50rem
   at typical label lengths) can't pull the moving button into the
   stationary one. The spring-back overshoot ([data-magnetic].is-returning
   in magnetic.js) is also covered by this headroom. */
.complete-card .cta-row{display:flex;gap:64rem;align-items:center;justify-content:center;flex-wrap:wrap}
.complete-card .cta-secondary{
  display:inline-flex;align-items:center;gap:10rem;
  background:transparent;color:var(--gulabi);
  border:2rem solid var(--gulabi);border-radius:999rem;padding:14rem 30rem;
  font:700 22rem/1 "Gotham Rounded","Nunito",sans-serif;
  transition:transform var(--t-fast) var(--ease),
             background var(--t-fast) var(--ease),
             color var(--t-fast) var(--ease);
}
/* Lift on the inner — the outer is magnetic, whose inline transform would
   swallow an outer hover/press transform (same fix as the other CTAs). */
.complete-card .cta-secondary:hover{background:rgba(208,0,111,.08)}
.complete-card .cta-secondary:hover .cta-inner{transform:translateY(-2rem) scale(1.02)}
.complete-card .cta-secondary:active .cta-inner{transform:translateY(0) scale(.97)}
.complete-card .cta-discover{
  display:inline-flex;align-items:center;gap:10rem;
  background:var(--gulabi);color:#fff;border-radius:999rem;padding:16rem 36rem;
  font:700 24rem/1 "Gotham Rounded","Nunito",sans-serif;
  box-shadow:0 8rem 18rem rgba(208,0,111,.28);
  transition:transform var(--t-fast) var(--ease),
             filter var(--t-fast) var(--ease),
             box-shadow var(--t-fast) var(--ease);
  position:relative;overflow:hidden;
}
/* Hover boing on the inner wrapper — outer is magnetic. */
.complete-card .cta-discover:hover{
  filter:brightness(1.06);
  box-shadow:0 18rem 38rem rgba(208,0,111,.45),0 6rem 14rem rgba(0,0,0,.12);
}
.complete-card .cta-discover:hover .cta-inner{
  animation:cta-discover-boing 720ms var(--ease-boing);
}
.complete-card .cta-discover:active{filter:brightness(.95)}
@keyframes cta-discover-boing{
  0%, 100% { transform:translateY(0)     rotate(0)     scale(1,1); }
  18%      { transform:translateY(4rem)   rotate(-3deg) scale(1.07,.92); }
  46%      { transform:translateY(-15rem) rotate(6deg)  scale(.94,1.10); }
  72%      { transform:translateY(-4rem)  rotate(-3deg) scale(1.06,.98); }
}

/* ── Journey-end credits ──
   .complete-stage is a full-bleed positioning shell around the card: the
   card keeps its own left/top/translate centering, and the GLIDE animates
   the SHELL's xPercent (-24 → card centre lands at 26% of the screen), so
   nothing ever fights the card's CSS transform. .is-static is the
   motionless variant (CMS preview + prefers-reduced-motion): the same end
   layout via inset offsets, credits visible, manual scroll instead of the
   auto-roll. */
.module-complete .complete-stage{position:absolute;inset:0;pointer-events:none}
.module-complete .complete-stage .complete-card{pointer-events:auto}
.module-complete.is-static .complete-stage{left:-24%;right:24%}
.credits-col{
  position:absolute;top:64rem;bottom:64rem;left:50.5%;right:4.5%;
  display:flex;flex-direction:column;justify-content:center;
  opacity:0;
}
.module-complete.is-static .credits-col{opacity:1}
/* The roll tweens scrollTop (native scroll — a finger can take over any
   time); the mask's fade depths match the list's own top/bottom padding
   so resting lines never sit half-faded. Scrollbars hide — this reads as
   a film roll, not a panel. */
.credits-scroll{
  max-height:100%;overflow-y:auto;scrollbar-width:none;
  -webkit-mask-image:linear-gradient(180deg,transparent 0,#000 56rem,#000 calc(100% - 72rem),transparent 100%);
          mask-image:linear-gradient(180deg,transparent 0,#000 56rem,#000 calc(100% - 72rem),transparent 100%);
}
.credits-scroll::-webkit-scrollbar{display:none}
/* One credit = one visual block: tight leading INSIDE an entry (a wrapped
   second line hugs its first) against a full 16rem gap BETWEEN entries, so
   a two-line credit can never be misread as two credits. balance splits a
   wrap into even centred halves — it reads as a designed unit. */
.credits-list{
  display:flex;flex-direction:column;align-items:center;gap:16rem;
  padding:56rem 24rem 72rem;text-align:center;
}
.credits-list .cr-h{
  font:700 26rem/1.3 "Gotham Rounded","Nunito",sans-serif;color:var(--kohl);
  max-width:760rem;text-wrap:balance;margin:20rem 0 0;
}
/* Section headings carry the card's own dot-divider motif — the same
   ornament that sits under the ending's title ties the roll to the card. */
.credits-list .cr-h::after{
  content:'';display:block;width:44rem;height:9rem;margin:10rem auto 2rem;
  background:url('../img/icon-dotdiv.svg') center/contain no-repeat;
}
.credits-list .cr-h:first-child{margin-top:0}
.credits-list .cr-line{
  font:500 19rem/1.3 "Gotham Rounded","Nunito",sans-serif;color:rgba(26,26,26,.78);
  max-width:760rem;text-wrap:balance;
}
/* Paragraph-length entries (>420 chars ≈ past Chromium's six-line
   balancing cap, where balance silently no-ops) fall back to pretty. */
.credits-list .cr-line.cr-long{text-wrap:pretty}
/* A credit whose catalogue phrase opens or closes the line renders as a
   museum wall label: the artwork+artist line with the small catalogue
   line tucked beneath — two lines by DESIGN, so a long credit can never
   read as two credits. The DOM keeps the authored order; accession-first
   credits flip visually via flex order only. */
.credits-list .cr-line.cr-lockup{display:flex;flex-direction:column}
.credits-list .cr-lockup .cr-main{order:1;display:block;color:rgba(26,26,26,.88)}
.credits-list .cr-lockup .cr-sub{
  order:2;display:block;margin-top:3rem;
  font-size:14.5rem;line-height:1.3;letter-spacing:.03em;
}
/* The catalogue voice recedes so the artwork and artist carry the line;
   the code itself stays crisp — tabular, gently spaced, never broken. */
.credits-list .cr-acc{color:rgba(26,26,26,.52);font-weight:400}
.credits-list .cr-acc .cr-code{
  font-weight:500;font-variant-numeric:tabular-nums;
  letter-spacing:.02em;white-space:nowrap;
}
.credits-list .cr-link .cr-acc{color:inherit;opacity:.72;font-weight:500}
.credits-list .cr-gap{height:6rem;flex:none}
/* A credit with a url — the whole line is the tap target (inline padding
   grows the hit area without moving the text; entries sit 16rem apart so
   8rem each side never overlaps a neighbour). Soft underline at rest, the
   pink carries "tappable"; press turns it solid and deepens the pink. */
.credits-list .cr-link{
  color:var(--gulabi);font-weight:600;
  text-decoration:underline;text-decoration-thickness:2rem;
  text-underline-offset:4rem;text-decoration-color:rgba(208,0,111,.35);
  padding:8rem 6rem;margin:0 -6rem;border-radius:6rem;
  transition:text-decoration-color .15s ease,color .15s ease;
}
.credits-list .cr-link:hover{text-decoration-color:currentColor}
.credits-list .cr-link:active{text-decoration-color:currentColor;color:#A6005A}
.credits-list .cr-link:focus-visible{outline:3rem solid var(--aamras);outline-offset:2rem}
.cr-link-arrow{
  width:13rem;height:13rem;display:inline-block;margin-left:5rem;
  vertical-align:baseline;transform:translateY(1.5rem);opacity:.75;
}

/* ─────────────────────────────────────────
   FULLSCREEN IMAGE OVERLAY (8:126)
   ───────────────────────────────────────── */
.overlay-fullscreen{
  background:linear-gradient(rgba(0,0,0,.88),rgba(0,0,0,.88));
  padding:0;align-items:stretch;justify-items:stretch;
}
.overlay-fullscreen .fs-frame{
  position:absolute;inset:0;display:grid;place-items:center;
}
.overlay-fullscreen .fs-frame img{
  /* No cream backing plate — contain-fit artwork floats on the dark
     backdrop like a proper lightbox (de-matting pass, 2026-06-12). */
  width:1920rem;height:1080rem;object-fit:contain;display:block;
  /* JS sets transform-origin to 0,0 inline (matches the cursor-anchored
     zoom math); the cursor swaps based on zoom state below. */
  cursor:zoom-in;
  user-select:none;
  -webkit-user-drag:none;
  touch-action:none;
}
.overlay-fullscreen .fs-frame img.is-zoomed{cursor:var(--cur-grab)}
.overlay-fullscreen .fs-frame img.is-panning{cursor:var(--cur-grabbing)}

.fs-exit{
  position:absolute;left:50%;bottom:16rem;transform:translateX(-50%);
  display:inline-flex;align-items:center;gap:8rem;
  background:#fff;color:var(--kohl);border:2rem solid var(--gulabi);
  border-radius:24rem;padding:12rem 22rem 12rem 18rem;
  font:700 24rem/1 "Gotham Rounded","Nunito",sans-serif;letter-spacing:.24rem;
  box-shadow:0 4rem 4rem rgba(0,0,0,.25), 0 6rem 18rem rgba(0,0,0,.18);
  /* Hover transitions transform/bg/color/shadow together so the lift,
     pink fill, and the gulabi-tinted shadow read as one gesture. */
  transition:transform var(--t-fast) var(--ease),
             box-shadow var(--t-fast) var(--ease),
             background var(--t-fast) var(--ease),
             color var(--t-fast) var(--ease);
  /* Sit above any image transform — when the user zooms or pans, the
     image can scale past the exit button's footprint, but the button
     stays anchored to the bottom of the overlay and on top of the image. */
  z-index:10;
}
.fs-exit:hover{
  transform:translateX(-50%) translateY(-4rem) scale(1.04);
  background:var(--gulabi);
  color:#fff;
  box-shadow:0 10rem 22rem rgba(208,0,111,.45),
             0 14rem 32rem rgba(0,0,0,.30);
}
.fs-exit:active{
  transform:translateX(-50%) translateY(-1rem) scale(0.99);
}
.fs-exit .ico{
  width:26rem;height:26rem;
  display:inline-flex;align-items:center;justify-content:center;
}
.fs-exit .ico img{
  width:100%;height:100%;object-fit:contain;
  /* SVG ships as a black-stroke X; on hover we flip it white via filter
     so it stays legible against the gulabi fill. */
  transition:filter var(--t-fast) var(--ease);
}
.fs-exit:hover .ico img{filter:brightness(0) invert(1);}
.fs-exit:hover .ico{animation:fs-exit-icon-twirl 540ms var(--ease-boing);}
/* Twirl follows the same beat as cta-play-icon (project's "boing"
   keyframe pattern): asymmetric rotation with overshoot scale, then
   settle. -45° instead of -90° so the X clearly reads as rotating
   (multiples of 90° loop back on a symmetric X). */
@keyframes fs-exit-icon-twirl{
  0%, 100% { transform:rotate(0)     scale(1);    }
  40%      { transform:rotate(-45deg) scale(1.25); }
  70%      { transform:rotate(15deg)  scale(1.10); }
}

/* ─────────────────────────────────────────
   TRANSITION OVERLAYS — cover-type page transitions (transitions.js).
   Created per-navigation inside #screen-container and destroyed on the
   timeline's completion. z-index 30: above sections + feedback overlays,
   below the idle-reset prompt (40) and confetti (100).
   ───────────────────────────────────────── */
.transition-cover{
  position:absolute;inset:0;z-index:30;
  pointer-events:none;overflow:hidden;
}
/* Sheet-rise backdrop — deliberately NO z-index: as a z-auto positioned
   element it paints BENEATH the z-indexed sections, so the receding old
   page dims into this dark void (the source demo's black body) while the
   new sheet rises above. */
.transition-underlay{
  position:absolute;inset:0;
  background:var(--kohl);
  pointer-events:none;
}
/* Paper-flip shading (transitions.js flip()) — created per-flip inside
   the two sections, removed with the timeline. z-index 50 sits above the
   module's own chrome (header/footer z5) within the section's stacking
   context (sections have contain:paint). */
.flip-shade{
  position:absolute;inset:0;pointer-events:none;z-index:50;
  background:linear-gradient(100deg,
    rgba(20,20,31,0) 35%, rgba(20,20,31,.22) 78%, rgba(20,20,31,.38) 100%);
  opacity:0;
}
.flip-cast{
  position:absolute;inset:0;pointer-events:none;z-index:50;
  background:linear-gradient(to left,
    rgba(20,20,31,.35) 0%, rgba(20,20,31,.12) 30%, rgba(20,20,31,0) 55%);
  opacity:0;
}
/* Page-curl flap (transitions.js curl()) — the blank paper back of the
   turning page, clipped per frame to the folded region and REFLECTED
   across the crease. transform-origin 0 0 is load-bearing: the per-frame
   transform string carries its own translate(F)…translate(−F) framing,
   and any other origin would offset the reflection. The drop-shadow
   follows the clip-path shape, giving the under-flap cast shadow free. */
.curl-flap{
  position:absolute;left:0;top:0;width:1920rem;height:1080rem;
  z-index:3;pointer-events:none;
  background:linear-gradient(120deg,
    #E9E2D4 0%, #FFFFFF 35%, #FBF7EF 70%, #EDE6D9 100%);
  /* Two drop-shadows, both tracing the clip-path silhouette: the tight
     0-offset pass is the paper's EDGE — without it the white flap merges
     into the white page beneath — and the offset soft pass is its mass
     shadow falling onto the freshly revealed page beside the fold. */
  filter:drop-shadow(0 0 4rem rgba(20,20,31,.45))
         drop-shadow(12rem 16rem 34rem rgba(20,20,31,.45));
  transform-origin:0 0;
  will-change:transform,clip-path;
}
/* The bend-line where the paper folds — a soft shadow band centered on
   the crease, rotated to the (constant) crease angle, repositioned per
   frame. Lives inside the OLD section, whose clip-path trims it at the
   crease so only the unfolded side shows it. transform-origin 0 0 is
   load-bearing here too (the transform string self-frames). */
.curl-crease{
  position:absolute;left:0;top:0;width:2600rem;height:120rem;
  pointer-events:none;z-index:60;
  transform-origin:0 0;
  background:linear-gradient(to bottom,
    rgba(20,20,31,0) 0%, rgba(20,20,31,.16) 42%, rgba(20,20,31,.30) 50%,
    rgba(20,20,31,.08) 62%, rgba(20,20,31,0) 100%);
}
.transition-cover.transition-grid{display:grid}
.transition-cover.transition-grid > div{
  width:100%;height:100%;
  transform-origin:50% 50%;
  will-change:transform,opacity;
}
.transition-layer{
  /* Stationary full-bleed sheets — revealed by clip-path unfolds, not
     movement (see transitions.js layers()). opacity:0 until each sheet's
     own tween starts. */
  position:absolute;inset:0;
  opacity:0;
  will-change:clip-path,filter;
}
.transition-slice{
  position:absolute;left:0;width:100.5%;
  background:var(--kohl);
  will-change:transform;
}
.transition-hexcell{
  position:absolute;
  background:var(--kohl);
  opacity:0;
  clip-path:polygon(50% 0%, 100% 25%, 100% 75%, 50% 100%, 0% 75%, 0% 25%);
  /* Slight oversize so honeycomb neighbours overlap — antialiased
     clip-path edges would otherwise show hairline seams at full cover. */
  transform:scale(1.04);
  will-change:opacity;
}

/* ─────────────────────────────────────────
   JOURNEY-LOAD OVERLAY — shown when the cover's Play is pressed; app.js
   startJourneyLoad() preloads the whole journey's images + audio so the
   rest navigates with no per-screen fetch (matters once served remotely).
   Honeycomb spinner (uiverse tender-swan-29, re-voiced as brand hexagons)
   beside "Loading", and a progress bar (uiverse warm-emu-89 — a brand-
   gradient "liquid" fill whose WIDTH tracks the real load %).
   ───────────────────────────────────────── */
.overlay-loading{
  position:absolute;inset:0;z-index:60;
  display:grid;place-items:center;
  /* Frosted scrim — blurs + dims the cover behind so the loading card
     reads as a distinct layer. The old flat cream backdrop matched the
     cover's cream/paper and merged into it. Same dark-scrim + white-card
     pattern as the idle overlay. */
  background:rgba(10,9,8,.5);
  backdrop-filter:blur(18rem) saturate(1.05);
  -webkit-backdrop-filter:blur(18rem) saturate(1.05);
}
.loading-card{
  position:relative;z-index:1;
  display:flex;flex-direction:column;align-items:center;gap:30rem;
  background:#fff;border-radius:32rem;padding:54rem 72rem 48rem;
  box-shadow:0 26rem 72rem rgba(0,0,0,.32);
}
.loading-head{display:flex;align-items:center;gap:22rem}
.loading-text{
  font:700 36rem/1 "Gotham Rounded","Nunito",sans-serif;
  color:var(--kohl);letter-spacing:1rem;
}
/* Honeycomb: 7 clip-path hexagons (centre + ring), each pulsing on a
   staggered delay so a colour ripple runs out from the middle. Cells are
   absolutely centred; app.js sets each one's translate + hue + delay. */
.honeycomb{position:relative;width:108rem;height:92rem}
.honeycomb .hex-cell{
  position:absolute;top:50%;left:50%;
  width:26rem;height:30rem;margin:-15rem 0 0 -13rem;
}
.honeycomb .hex{
  display:block;width:100%;height:100%;
  clip-path:polygon(50% 0%,100% 25%,100% 75%,50% 100%,0% 75%,0% 25%);
  /* Matches uiverse tender-swan-29 pulse00: scale 1 → ~0 → 1 at 50% over
     2s (a shrink-to-nothing, no opacity dim). The per-cell delays keep the
     ripple. */
  animation:hex-pulse 2s ease-in-out infinite;
}
@keyframes hex-pulse{
  0%,100%{transform:scale(1)}
  50%    {transform:scale(.06)}
}
/* Progress bar styled after uiverse tall-fly-13: a left→right brand
   gradient fill (revealed as it fills) with a two-tone glow, a soft radial
   "ripple" pulse on the fill, and the % centred on the bar. */
.loader-track{
  position:relative;width:460rem;height:36rem;
  background:radial-gradient(circle at 50% 50%, #1f1f2e, #0c0c14);
  border-radius:999rem;overflow:hidden;
  box-shadow:inset 0 2rem 6rem rgba(0,0,0,.55), 0 6rem 18rem rgba(0,0,0,.28);
  border:1rem solid rgba(255,255,255,.06);
}
/* Static brand gradient, ordered warm→cool for smooth transitions (amber →
   lal → gulabi → baingani → ferozi — lal sits between amber & gulabi so the
   warm run never skips red; teal eases out of purple at the end). Glow uses
   the two end hues. .liquid-fill keeps its name — app.js drives the WIDTH. */
.liquid-fill{
  position:absolute;top:0;left:0;height:100%;width:0;border-radius:999rem;
  /* Gradient anchored to the FULL track width — the fill's width (= load %)
     REVEALS the gradient left→right instead of squashing the whole amber→
     teal run into the filled portion. So at 30% you see amber→red, at 100%
     the full amber→teal. background-size MUST match .loader-track width. */
  background:linear-gradient(90deg,#FFA300,#D50032,#D0006F,#9B5FC0,#5CB8B2) left center / 460rem 100% no-repeat;
  box-shadow:0 0 15rem rgba(255,163,0,.55), 0 0 30rem rgba(92,184,178,.45);
  transition:width .3s ease-out;   /* smooths real-progress jumps */
}
/* Ripple highlight emanating from the fill (tall-fly-13 ::before). */
.liquid-fill::before{
  content:'';position:absolute;top:50%;left:50%;
  transform:translate(-50%,-50%);width:200%;height:200%;
  background:radial-gradient(circle, rgba(255,255,255,.20), transparent 60%);
  animation:fill-ripple 3s ease-out infinite;
}
@keyframes fill-ripple{
  0%  {transform:translate(-50%,-50%) scale(.5);opacity:.7}
  100%{transform:translate(-50%,-50%) scale(1.5);opacity:0}
}
/* % centred on the bar (white, over fill or dark track). */
.loader-track .loading-pct{
  position:absolute;top:50%;left:50%;transform:translate(-50%,-50%);z-index:2;
  font:700 16rem/1 "Gotham Rounded","Nunito",sans-serif;
  color:#fff;letter-spacing:.5rem;text-shadow:0 1rem 4rem rgba(0,0,0,.7);
}
@media (prefers-reduced-motion: reduce){
  .honeycomb .hex{animation:none}
  .liquid-fill{transition:none}
  .liquid-fill::before{animation:none;opacity:0}
}

/* ─────────────────────────────────────────
   DEMO PICKERS — floating chip rails shown by app.js on journeys flagged
   "transitionPicker": true (the Effect Demo journey). Right rail rewrites
   journey.transition so the NEXT navigation plays it; left rail flips the
   reveal scheme and replays the current screen's entrance on the spot.
   Demo chrome, not visitor UI — sits above overlays (35) but under the
   idle prompt (40).
   ───────────────────────────────────────── */
#transition-picker,
#reveal-picker{
  /* Vertical rails, centred on the screen edges — clear of the corner
     close button, the footer CTAs and the centred question content. */
  position:absolute;top:50%;transform:translateY(-50%);
  display:flex;flex-direction:column;align-items:stretch;gap:8rem;
  background:rgba(20,20,31,.9);border-radius:20rem;
  padding:14rem 12rem;z-index:35;
  box-shadow:0 8rem 24rem rgba(0,0,0,.32);
}
#transition-picker{right:24rem}
#reveal-picker{left:24rem}
#transition-picker .tp-label,
#reveal-picker .tp-label{
  font:700 13rem/1 "Gotham Rounded","Nunito",sans-serif;
  color:rgba(255,255,255,.55);letter-spacing:1.2rem;text-transform:uppercase;
  text-align:center;margin-bottom:4rem;white-space:nowrap;
}
#transition-picker button,
#reveal-picker button{
  font:700 15rem/1 "Gotham Rounded","Nunito",sans-serif;color:#fff;
  background:rgba(255,255,255,.12);border-radius:999rem;
  padding:10rem 16rem;white-space:nowrap;text-align:center;
  transition:background var(--t-fast) var(--ease),
             color var(--t-fast) var(--ease),
             transform var(--t-fast) var(--ease);
}
/* Hover nudges each rail toward the stage it acts on. */
#transition-picker button:hover{background:rgba(255,255,255,.24);transform:translateX(-2rem)}
#reveal-picker button:hover{background:rgba(255,255,255,.24);transform:translateX(2rem)}
#transition-picker button:active,
#reveal-picker button:active{transform:translateX(0) scale(.96)}
#transition-picker button.is-current,
#reveal-picker button.is-current{background:var(--aamras);color:var(--kohl)}

/* ─────────────────────────────────────────
   IDLE RESET PROMPT — kiosk attract-loop guard
   Shown by app.js after ~110s of stillness mid-journey. Any tap lands on
   the overlay (press + release both hit it, so nothing clicks through)
   and dismisses; the ring drains in real time and expiry returns to the
   journey-select landing so the next visitor never inherits a
   half-finished quiz.
   ───────────────────────────────────────── */
.overlay-idle{
  position:absolute;inset:0;z-index:40;
  display:grid;place-items:center;
  background:rgba(10,9,8,.78);
}
.idle-card{
  background:#fff;border-radius:24rem;padding:48rem 72rem 44rem;
  display:flex;flex-direction:column;align-items:center;gap:18rem;
  box-shadow:var(--sh-modal);text-align:center;
}
.idle-ring{position:relative;width:120rem;height:120rem}
.idle-ring svg{width:100%;height:100%;transform:rotate(-90deg)}
.idle-ring .ring-bg{fill:none;stroke:var(--bg-sec);stroke-width:8}
.idle-ring .ring-fg{fill:none;stroke:var(--aamras);stroke-width:8;stroke-linecap:round}
.idle-num{
  position:absolute;inset:0;display:grid;place-items:center;
  font:700 44rem/1 "Gotham Rounded","Nunito",sans-serif;color:var(--kohl);
}
.idle-title{margin:0;font:700 40rem/1.1 "Gotham Rounded","Nunito",sans-serif;color:var(--kohl)}
.idle-sub{margin:0;font:500 22rem/1.3 "Gotham Rounded","Nunito",sans-serif;color:var(--ink-mute)}
.idle-stay{
  margin-top:10rem;
  display:inline-flex;align-items:center;
  background:var(--aamras);color:#fff;border-radius:999rem;
  padding:14rem 32rem;box-shadow:var(--sh-cta);
  font:700 24rem/1 "Gotham Rounded","Nunito",sans-serif;letter-spacing:.3rem;
  transition:transform var(--t-fast) var(--ease), filter var(--t-fast) var(--ease);
}
.idle-stay:hover{transform:translateY(-3rem) scale(1.03);filter:brightness(1.06)}
.idle-stay:active{transform:translateY(0) scale(.97)}

/* Time's-up + leave-video variant (app.js showTimeUpPrompt, content.js
   askLeaveVideo) — same card, two doors */
.timeup-row{display:flex;gap:16rem;margin-top:10rem}
.timeup-row .idle-stay{margin-top:0}
.timeup-next{background:var(--ferozi)}

/* ─────────────────────────────────────────
   Loading + landscape
   ───────────────────────────────────────── */
.loading{position:fixed;inset:0;display:grid;place-items:center;color:#fff;
  background:#0a0a0a;font:700 16rem/1.4 "Nunito",sans-serif;z-index:9999;
  transition:opacity .3s ease}
.loading.is-hidden{opacity:0;pointer-events:none}

.portrait-tip{position:fixed;inset:0;display:none;place-items:center;color:#fff;
  background:#0a0a0a;padding:32rem;text-align:center;font:700 16rem/1.5 "Nunito",sans-serif;z-index:99}
@media (orientation:portrait){
  .portrait-tip{display:grid}
  .stage{display:none}
}

/* ──────────────────────────────────────────────────────────────────────────
   Personality module additions. The section reuses .module-mcq for all the
   centre / choice / footer styling; these rules cover only what's new:
   audio-option cards (CSS equaliser), the neutral "chosen" confirm pulse, the
   hero-less Fact reveal, and the multi-paragraph result screen.
   ────────────────────────────────────────────────────────────────────────── */

/* ── Audio-option cards ───────────────────────────────────────────────────────
   Letter badge + a dedicated play button + an equaliser wave. ONLY the play
   button toggles audio (one clip at a time, enforced in JS); the letter, wave,
   label, and card surface all SELECT the option. .audio-mini is a plain layout
   group (no box) so the card's own border is the only frame.
   Scoped .module-mcq (not .module-personality) since MCQ choices gained audio
   2026-07-16 — the personality section carries BOTH classes, so this covers it. */
.choices-row.audio-options{gap:20rem}
/* Vertical layout: audio cards stack full-width like text choices. */
.module-mcq .mcq-centre.is-vertical .choice.is-audio{width:100%}

/* Base audio card — compact stacked column (letter, then play+wave, then
   label): the shape a lone listening card takes inside a MIXED row, sitting
   beside picture/text cards. All-audio rows restyle it via .audio-inline. */
.module-mcq .choice.is-audio{
  flex-direction:column;align-items:stretch;justify-content:center;
  width:400rem;height:auto;min-height:0;gap:12rem;padding:16rem 18rem;
}
.module-mcq .choice.is-audio .letter{align-self:flex-start}

.module-mcq .audio-mini{display:flex;align-items:center;gap:14rem;min-width:0}

/* Play button — the single tap target for audio; reads as a button via its
   season-coloured fill, hover lift, and press. */
.module-mcq .audio-mini .play{
  width:48rem;height:48rem;border-radius:999rem;flex:0 0 auto;cursor:pointer;
  background:linear-gradient(135deg,#FFD17F 0%,var(--aamras) 100%);
  display:grid;place-items:center;
  box-shadow:0 6rem 14rem rgba(255,163,0,.32),0 2rem 4rem rgba(0,0,0,.10);
  transition:transform var(--t-fast) var(--ease),filter var(--t-fast) var(--ease),box-shadow var(--t-fast) var(--ease);
}
.module-mcq .audio-mini .play:hover{transform:scale(1.08);filter:brightness(1.05)}
.module-mcq .audio-mini .play:active{transform:scale(.92)}
.module-mcq .audio-mini .play img{width:18rem;height:18rem}

/* Wave — equaliser bars sitting on the card, bottom-aligned. Idle is a calm low
   line; while the clip plays the bars rise and fall. Season-coloured. */
.module-mcq .audio-mini .wave{
  flex:1 1 auto;min-width:0;height:40rem;
  display:flex;align-items:flex-end;gap:3rem;overflow:hidden;
}
.module-mcq .audio-mini .wave span{
  flex:1;height:100%;background:var(--aamras);opacity:.55;border-radius:6rem;
  transform:scaleY(.22);transform-origin:bottom;
  /* Bar motion is JS-driven (real AnalyserNode spectrum, or a GSAP
     random-walk fallback — see journey-helpers.js setupAudioOptions) so
     play/pause EASES between the idle pose and the live wave; the old CSS
     keyframe snapped on both edges. No transform transition here — it
     would fight the per-frame writes. Opacity still cross-fades on the
     class change below. */
  will-change:transform;
  transition:opacity .25s var(--ease);
}
/* While playing — bars brighten; their motion comes from setupAudioOptions. */
.module-mcq .choice.is-audio.is-playing .audio-mini .wave span{opacity:.92}

.module-mcq .choice.is-audio .label{flex:0 0 auto;width:100%;text-align:center}

/* Inline layout — letter, play, wave, [label] on one row. The ONLY style for
   all-audio rows since 2026-07-16 (the stacked/inline knob retired).
   Compact ~96rem rows. Full-width inside the vertical column; everywhere
   else the row becomes a centred 900rem list — without that, four
   100%-wide flex-shrink:0 cards sit in ONE flex row and clip off-stage. */
.module-mcq .choices-row.audio-inline .choice.is-audio{
  flex-direction:row;align-items:center;gap:18rem;
  width:100%;height:auto;min-height:96rem;padding:18rem 20rem;
}
.module-mcq .mcq-centre:not(.is-vertical) .choices-row.audio-inline{
  flex-direction:column;align-items:center;gap:14rem;
}
.module-mcq .mcq-centre:not(.is-vertical) .choices-row.audio-inline .choice.is-audio{
  width:900rem;max-width:100%;
}
.module-mcq .choices-row.audio-inline .choice.is-audio .letter{align-self:center}
/* Grows into empty space but NEVER shrinks below play+wave (shrink 0):
   the base .audio-mini's min-width:0 would let a long label squeeze the
   box under its content and the wave would paint over the label text. */
.module-mcq .choices-row.audio-inline .choice.is-audio .audio-mini{flex:1 0 auto}
/* The wave never shrinks into invisibility beside a long label… */
.module-mcq .choices-row.audio-inline .choice.is-audio .wave{min-width:140rem}
/* …and the label wraps inside the card instead of nowrap-spilling past it. */
.module-mcq .choices-row.audio-inline .choice.is-audio .label{
  width:auto;flex:0 1 auto;min-width:0;text-align:left;
}

/* Per-colour accent — play button + wave match the card's letter badge. */
.module-mcq .choice.is-audio.color-gulabi   .audio-mini .play{background:linear-gradient(135deg,#FF6FB1 0%,var(--gulabi) 100%);box-shadow:0 6rem 14rem rgba(208,0,111,.30),0 2rem 4rem rgba(0,0,0,.10)}
.module-mcq .choice.is-audio.color-gulabi   .audio-mini .wave span{background:var(--gulabi)}
.module-mcq .choice.is-audio.color-ferozi   .audio-mini .play{background:linear-gradient(135deg,#A6E0DB 0%,var(--ferozi) 100%);box-shadow:0 6rem 14rem rgba(92,184,178,.32),0 2rem 4rem rgba(0,0,0,.10)}
.module-mcq .choice.is-audio.color-ferozi   .audio-mini .wave span{background:var(--ferozi)}
.module-mcq .choice.is-audio.color-baingani .audio-mini .play{background:linear-gradient(135deg,#9B5FC0 0%,var(--baingani) 100%);box-shadow:0 6rem 14rem rgba(80,7,120,.30),0 2rem 4rem rgba(0,0,0,.10)}
.module-mcq .choice.is-audio.color-baingani .audio-mini .wave span{background:var(--baingani)}

/* Neutral "chosen" confirm — personality has no right/wrong, so the committed
   card pops straight into is-locked's amber pose instead of the MCQ red/teal
   verdict. Added together with .is-locked on the tap (it out-cascades the
   lock-in animation); 0% starts from ground so the tap reads as one spring,
   100% matches the locked static transform for a clean hand-off. */
.module-personality .choice.is-chosen{animation:choice-chosen 520ms var(--ease-boing)}
@keyframes choice-chosen{
  0%  {transform:translateY(0)      scale(1)}
  40% {transform:translateY(-11rem) scale(1.085)}
  100%{transform:translateY(-6rem)  scale(1.04)}
}

/* Fact reveal — verdict-less variant of the fact card. Shrinks to content
   (text-only facts on the avatar / painting questions) rather than the fixed
   1100rem scoring layout. The magnifier mascot heads the modal where the
   check/X ring would sit — same helper character as the hint plaques; its
   idle bob/pupil loops ride the shared .hm-fig CSS. Fig carries BOTH
   dimensions (square art: width = height) — see the .hm-fig note. */
.feedback-modal.is-fact-reveal{width:auto;max-width:1100rem;min-width:720rem;gap:20rem}
.feedback-modal.is-fact-reveal .fact-hero{display:flex;justify-content:center}
.feedback-modal.is-fact-reveal .fact-hero .hm-fig{height:118rem;width:118rem}
.feedback-modal.is-fact-reveal .fact-card{padding:8rem 6rem}
.feedback-modal.is-fact-reveal .fact-card-content{justify-content:center}
.feedback-modal.is-fact-reveal .fact-card-content .body{max-width:900rem}

/* Result screen — per-category personality outcome. Wider card + tighter
   rhythm to seat the multi-paragraph body comfortably. */
.module-complete.is-result .complete-card{width:920rem;gap:18rem;padding:48rem 56rem 44rem}
.module-complete.is-result .complete-card .body{
  width:760rem;font:500 21rem/1.45 "Gotham Rounded","Nunito",sans-serif;
}

/* Accent plumbing — journey-end render() sets --result-color/--result-rgb
   inline on the card when the winning category declares a colour; the chip
   and medal halo read the vars directly (see their base rules). The medal's
   inner disc needs this CSS hook because SVG presentation attributes can't
   hold var(); the fallback reproduces the stock gulabi disc. */
.complete-card .medal-core{fill:var(--result-color, #D0006F)}
/* Word-stagger spans on the result title (H.splitWords) — inline-block so
   per-word transforms apply; words still wrap like normal text. */
.complete-card h2 .word{display:inline-block;will-change:transform,opacity}

/* ─────────────────────────────────────────────────────────────────────────
   Jigsaw module — drag-to-assemble puzzle. Layout mirrors .module-mcq
   (absolute-inset flex column: header / stage / footer). The .jigsaw-board is
   a framed tray hosting the engine's two stacked <canvas> layers; pieces are
   drawn in the canvas's own pixel space (see jigsaw-engine.js), so the board
   only needs a definite rendered size for getComputedStyle() to read.
   ───────────────────────────────────────────────────────────────────────── */
.module-jigsaw{position:absolute;inset:0;display:flex;flex-direction:column;}
/* The play area uses the FULL screen — the header/footer float ON TOP as
   transparent overlays (base .activity-header/-footer are already absolute).
   pointer-events:none lets piece-drags pass straight through the empty strips
   to the canvas below; only the actual buttons stay clickable. This lets pieces
   be moved into the header/footer bands and lets the finished picture fill the
   whole screen. */
.module-jigsaw > .activity-header{position:absolute;top:0;left:0;z-index:6;pointer-events:none}
.module-jigsaw > .activity-footer{position:absolute;left:0;bottom:0;z-index:6;pointer-events:none}
.module-jigsaw > .activity-header button,
.module-jigsaw > .activity-footer button{pointer-events:auto}

.jigsaw-stage{
  position:absolute;inset:0;z-index:1;
  overflow:hidden;
  /* Full-SCREEN transparent play surface — spans the whole section, UNDER the
     header/footer overlays, so pieces play (and can be dragged) edge to edge
     top to bottom. The paper-bg shows through the transparent canvases.
     overflow:hidden still clips a stray piece from leaving the stage. */
}
.jigsaw-board{position:absolute;inset:0;padding:0;border:0;}
.jigsaw-board canvas{position:absolute;inset:0;width:100%;height:100%;touch-action:none;cursor:var(--cur-grab);}
.jigsaw-board canvas:active{cursor:var(--cur-grabbing);}

/* Reference peek — full-stage frosted overlay over the board. */
.jigsaw-reference{
  position:absolute;inset:0;z-index:20;cursor:pointer;padding:48rem;
  display:flex;align-items:center;justify-content:center;
  background:rgba(20,20,31,.55);backdrop-filter:blur(8rem) saturate(1.05);
}
.jigsaw-reference[hidden]{display:none;}
.jigsaw-reference img{max-width:100%;max-height:100%;object-fit:contain;border-radius:var(--r-card);box-shadow:var(--sh-img);}
.jigsaw-reference-hint{
  position:absolute;bottom:28rem;left:50%;transform:translateX(-50%);
  color:#fff;font:500 20rem/1 "Gotham Rounded","Nunito",sans-serif;opacity:.85;
}

/* Footer-right controls (delayed hint + peek + delayed skip). */
.footer-right{display:flex;align-items:center;gap:16rem;}
.jigsaw-peek{
  display:inline-flex;align-items:center;gap:10rem;height:56rem;padding:0 26rem;
  border:0;border-radius:var(--r-cta);background:#fff;color:var(--ink);cursor:pointer;
  font:600 22rem/1 "Gotham Rounded","Nunito",sans-serif;box-shadow:var(--sh-cta);
}
.jigsaw-peek img{width:24rem;height:24rem;object-fit:contain;}
.jigsaw-skip{
  height:56rem;padding:0 26rem;border:0;border-radius:var(--r-cta);
  background:var(--kohl);color:#fff;cursor:pointer;
  font:600 20rem/1 "Gotham Rounded","Nunito",sans-serif;box-shadow:var(--sh-cta);
}
.jigsaw-skip[hidden]{display:none;}
/* Hint — appears only after 15s without a single join; each press glides one
   group home (engine.hintStep). Amber pill, dark ink (white on amber reads
   ~1.9:1 — same contrast lesson as the CMS primaries). [disabled] = the
   cooldown between assisted merges. */
.jigsaw-hint{
  height:56rem;padding:0 30rem;border:0;cursor:pointer;border-radius:var(--r-cta);
  background:var(--aamras);color:#3A2A00;
  font:700 22rem/1 "Gotham Rounded","Nunito",sans-serif;
  box-shadow:0 6rem 18rem color-mix(in srgb, var(--aamras) 50%, transparent);
  transition:transform var(--t-fast) var(--ease),box-shadow var(--t-fast) var(--ease),opacity var(--t-fast) var(--ease);
}
.jigsaw-hint:hover{ transform:translateY(-2rem) scale(1.05);
  box-shadow:0 10rem 24rem color-mix(in srgb, var(--aamras) 55%, transparent); }
.jigsaw-hint:active{ transform:scale(.95); }
.jigsaw-hint[hidden]{ display:none; }
.jigsaw-hint[disabled]{ opacity:.45;pointer-events:none;transform:none; }

/* Mascot plaque in the footer-left — replaces the old static hint pill with
   the same magnifier + bubble the slider/scale/sequence hints use (shared
   .hm-plaque styles). Fig gets BOTH dimensions — see the .hm-fig note. */
.jigsaw-say .hm-fig{ height:72rem;width:72rem; }
.jigsaw-say .hm-bubble{ margin-bottom:20rem;font-size:22rem;white-space:normal;max-width:430rem; }

/* Solve reveal — the finished picture glides to the centre with a shimmer
   sweep over it (positioned/animated by the module via GSAP; these are just
   the looks). No glow — only the module's SOLVED_SHADOW depth shadow. */
.jigsaw-solved{
  position:absolute;z-index:15;overflow:hidden;border-radius:var(--r-card);
  box-shadow:var(--sh-img);will-change:left,top,width,height;
}
.jigsaw-solved img{display:block;width:100%;height:100%;object-fit:fill;}
.jigsaw-shine{
  position:absolute;top:-20%;left:0;width:55%;height:140%;pointer-events:none;
  transform:skewX(-18deg);mix-blend-mode:screen;
  background:linear-gradient(90deg,transparent,rgba(255,255,255,.7),transparent);
}
@media (prefers-reduced-motion: reduce){ .jigsaw-shine{display:none;} }

/* ── Difficulty-select entrance ─────────────────────────────────────────────
   Shown first (section carries .is-intro). The play chrome — progress dots,
   score chip, footer mascot/hint/peek/skip — is suppressed until Start; only
   the close button + journey title remain in the header. */
.module-jigsaw.is-intro .progress-wrap,
.module-jigsaw.is-intro .score-chip,
.module-jigsaw.is-intro .jigsaw-footer .footer-right,
.module-jigsaw.is-intro .jigsaw-footer .jigsaw-say{ display:none; }
.jigsaw-board[hidden]{ display:none; }

/* The artwork title flies into the header centre on Start, REPLACING the
   progress bar (hidden for jigsaw). A full-width flex strip over the header
   centres the text WITHOUT a transform, so the FLIP transform on .jht-inner
   is clean and clearProps returns it to centre. .jht-inner is a proportional
   (22/48) replica of .jigsaw-artwork-title, so the two wrap identically. */
.module-jigsaw .progress-wrap{ display:none; }
.jigsaw-header-title{
  position:absolute;left:0;right:0;top:0;height:64rem;z-index:6;pointer-events:none;
  display:flex;align-items:center;justify-content:center;
}
.jigsaw-header-title[hidden]{ display:none; }
.jht-inner{
  max-width:495rem;text-align:center;color:var(--ink);
  font:700 22rem/1.18 "Gotham Rounded","Nunito",sans-serif;will-change:transform;
}

.jigsaw-intro{
  position:absolute;inset:0;z-index:5;
  display:flex;align-items:center;justify-content:center;padding:32rem 40rem;
}
.jigsaw-intro[hidden]{ display:none; }
/* No card — content sits directly on the paper background. */
.jigsaw-intro-card{
  width:min(1240rem,95%);
  padding:20rem 40rem;text-align:center;
  display:flex;flex-direction:column;align-items:center;
}
.jigsaw-artwork-title{                 /* biggest element on the screen */
  margin:0;max-width:1080rem;color:var(--ink);
  font:700 48rem/1.18 "Gotham Rounded","Nunito",sans-serif;
}
.jigsaw-intro-heading{                 /* clear gap below the title */
  margin:54rem 0 0;color:var(--gulabi);
  font:800 34rem/1.15 "Gotham Rounded","Nunito",sans-serif;letter-spacing:.4rem;
}
.jigsaw-diff-options{ display:flex;gap:24rem;justify-content:center;flex-wrap:wrap;margin:40rem 0 0; }
.jigsaw-diff{
  --acc:var(--ferozi);
  position:relative;cursor:pointer;user-select:none;width:236rem;
  padding:26rem 22rem;border-radius:var(--r-card);
  background:#fff;border:3rem solid var(--stroke);box-shadow:var(--sh-card);
  display:flex;flex-direction:column;align-items:center;gap:10rem;
  transition:border-color var(--t-fast) var(--ease),transform var(--t-fast) var(--ease),box-shadow var(--t-fast) var(--ease);
}
.jigsaw-diff input{ position:absolute;opacity:0;pointer-events:none; }
.jigsaw-diff-radio{
  width:30rem;height:30rem;border-radius:999rem;border:3rem solid var(--ink-mute);position:relative;
  transition:border-color var(--t-fast) var(--ease);
}
.jigsaw-diff-radio::after{
  content:"";position:absolute;inset:5rem;border-radius:999rem;background:var(--acc);
  transform:scale(0);transition:transform var(--t-fast) var(--ease-boing);
}
.jigsaw-diff-name{ font:800 30rem/1 "Gotham Rounded","Nunito",sans-serif;color:var(--ink); }
.jigsaw-diff-sub{ font:500 20rem/1 "Gotham Rounded","Nunito",sans-serif;color:var(--ink-mute); }
/* Piece-count pictogram — same box, more (smaller) cells = harder; reads
   without words. Cells tint with the card's accent. */
.jigsaw-diff-grid{
  display:grid;grid-template-columns:repeat(var(--c,4),1fr);gap:3rem;
  width:96rem;margin-top:2rem;
}
.jigsaw-diff-grid i{
  aspect-ratio:1;border-radius:3rem;
  background:color-mix(in srgb, var(--acc) 45%, #fff);
}
.jigsaw-diff:has(input:checked) .jigsaw-diff-grid i{
  background:color-mix(in srgb, var(--acc) 78%, #fff);
}
.jigsaw-diff:hover{ transform:translateY(-3rem); }
.jigsaw-diff:has(input:checked){
  border-color:var(--acc);transform:translateY(-3rem);
  box-shadow:0 0 0 4rem color-mix(in srgb, var(--acc) 22%, transparent), var(--sh-card);
}
.jigsaw-diff:has(input:checked) .jigsaw-diff-radio{ border-color:var(--acc); }
.jigsaw-diff:has(input:checked) .jigsaw-diff-radio::after{ transform:scale(1); }
.jigsaw-diff input:focus-visible + .jigsaw-diff-radio{ outline:3rem solid var(--acc);outline-offset:3rem; }
/* Start — solid dark pill, white label + white arrow (the shared .cta-continue
   is a white-on-white gated pill, invisible here). */
.jigsaw-start{
  margin:46rem 0 0;display:inline-flex;border:0;cursor:pointer;padding:0;
  background:var(--kohl);border-radius:var(--r-cta);box-shadow:var(--sh-cta);
  transition:transform var(--t-fast) var(--ease),box-shadow var(--t-fast) var(--ease);
}
.jigsaw-start .cta-inner{ display:inline-flex;align-items:center;gap:12rem;padding:18rem 46rem; }
.jigsaw-start-label{ font:800 28rem/1 "Gotham Rounded","Nunito",sans-serif;letter-spacing:.5rem;color:#fff; }
.jigsaw-start-arrow{ width:30rem;height:26rem;object-fit:contain;filter:brightness(0) invert(1); }
.jigsaw-start:hover{ transform:translateY(-3rem);box-shadow:0 10rem 30rem rgba(0,0,0,.24); }
.jigsaw-start:active{ transform:scale(.97); }

/* ── Scale Slider module (modules/scale.js) ─────────────────────────────────
   A STEPPED slider: the handle snaps to N labelled stops. Layout mirrors
   .module-slider (flex column, fixed header/footer, centred white panel); the
   knob reuses the slider's settle / wiggle @keyframes. Stops sit at column
   CENTRES — (i+0.5)/n — so sentence-length labels get an even column each
   and the end labels never overflow the bar. */
.module-scale{ position:absolute;inset:0;display:flex;flex-direction:column; }
.module-scale > .activity-header{ position:relative;flex:0 0 64rem; }
.module-scale > .activity-footer{ position:relative;flex:0 0 85rem; }
.scale-centre{
  flex:1 1 0;min-height:0;overflow:hidden;
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:40rem;padding:12rem 48rem;position:relative;z-index:1;
}
/* No card — content sits directly on the paper background (paperBg behind the
   section), like the jigsaw intro. */
.scale-panel{
  background:transparent;padding:24rem 40rem 28rem;
  display:flex;flex-direction:column;align-items:center;gap:44rem;
  width:100%;max-width:1780rem;
}
.scale-panel .prompt{
  font:700 42rem/54rem "Gotham Rounded","Nunito",sans-serif;color:var(--kohl);
  text-align:center;margin:0;max-width:1500rem;
}
.scale-panel .sub{
  font:500 20rem/30rem "Gotham Rounded","Nunito",sans-serif;color:rgba(0,0,0,.6);
  text-align:center;margin:0;
}

/* Widget = track (ticks + handle) above a row of column labels. --n (stop
   count) is set inline; the label columns divide the width evenly by it. */
.scale-widget{ width:100%;padding:14rem 44rem 0;--n:4; }
.scale-track{
  position:relative;height:28rem;border-radius:999rem;margin:46rem 0 0;
  /* Recessed warm groove — reads as a channel on the cream paper (the old
     #e8e2d9 fill blended into it once the white card was removed). */
  background:rgba(58,42,28,.18);box-shadow:inset 0 4rem 8rem rgba(0,0,0,.18);cursor:var(--cur-grab);
}
.scale-track:active,
.scale-track.is-dragging{cursor:var(--cur-grabbing)}
.scale-fill{
  position:absolute;left:0;top:0;height:100%;width:0;border-radius:inherit;
  /* Flat amber level, recessed INTO the groove — an inner top shade +
     soft under-lift + hairline edge give it a textured matte read
     (2026-07-09 user call: no gradient, no glow; amber). Verdict rules
     below only swap the background, so the matte treatment carries. */
  background:var(--aamras);
  box-shadow:
    inset 0 4rem 6rem rgba(58,42,28,.28),
    inset 0 -3rem 5rem rgba(255,255,255,.22),
    inset 0 0 0 1rem rgba(58,42,28,.10);
  pointer-events:none;z-index:1;
}

/* Ticks — big invisible hit area, small visible dot. The active dot hides
   under the handle (which paints above it). */
.scale-stop{
  position:absolute;top:50%;left:50%;width:76rem;height:76rem;
  transform:translate(-50%,-50%);
  display:flex;align-items:center;justify-content:center;
  background:none;border:0;padding:0;cursor:var(--cur-grab);z-index:3;
}
/* pressing a stop springs the knob to it — same gesture family as the
   track, so the closed hand answers the press here too */
.scale-stop:active{cursor:var(--cur-grabbing)}
.scale-stop .dot{
  width:22rem;height:22rem;border-radius:999rem;background:#fff;
  border:3rem solid var(--stroke);box-shadow:0 2rem 4rem rgba(0,0,0,.14);
  transition:transform .2s var(--ease-boing);
}
.scale-stop:hover .dot{ transform:scale(1.25); }
.scale-stop.is-active .dot{ opacity:0; }

/* Handle — reuses the slider knob look + its settle / wiggle keyframes. */
.scale-track .knob{
  position:absolute;top:50%;left:0%;width:74rem;height:74rem;border-radius:999rem;
  background:radial-gradient(circle at 35% 30%, #ffffff 0%, #f8f6f0 60%, #e8e2d9 100%);
  /* Kohl ring at rest (2026-07-09 user call — black when untouched);
     lightens while held, goes gulabi once a stop is picked, verdict
     colours take over in answer mode. */
  border:5rem solid var(--kohl);transform:translate(-50%,-50%);
  box-shadow:0 10rem 22rem rgba(0,0,0,.22), inset 0 -3rem 6rem rgba(0,0,0,.10);
  transition:transform .22s var(--ease-boing), border-color var(--t-fast) var(--ease);
  z-index:4;touch-action:none;cursor:var(--cur-grab);will-change:left,transform;
}
.scale-track.has-selection .knob{ border-color:var(--gulabi); }
.scale-track .knob:active,
.scale-track.is-dragging .knob{
  transform:translate(-50%,-50%) scale(1.12);cursor:var(--cur-grabbing);
  border-color:var(--stroke);
  box-shadow:0 8rem 20rem rgba(0,0,0,.22), inset 0 -2rem 4rem rgba(0,0,0,.10);
}
.scale-track .knob.is-settling{ animation:knob-settle .7s ease-out; }
/* Idle "slide me" hint — pulsing chevrons come from the slider knob's
   shared .idle-hint block; the bigger handle scales them up. The scale's
   handle only ever wears the hint UNTOUCHED AT THE TRACK'S LEFT END
   (arming is gated on "no pick yet"), so the left chevron would point
   off the track at nothing — hide it, and swap the symmetric rock for a
   rightward lunge (same 1.2s beat, so the surviving right chevron's 50%
   peak stays in phase with the knob's push). */
.scale-track .knob.idle-hint{
  --chev:19rem; --chev-b:6rem;
  animation-name:knob-wiggle-right;
}
.scale-track .knob.idle-hint::before{ display:none; }
@keyframes knob-wiggle-right{
  0%, 100% { transform:translate(-50%,-50%); }
  50%      { transform:translate(-50%,-50%) translateX(9rem); }
}
/* Labels — one even column per stop, centred under its tick, free to wrap for
   sentence-length options. Active column bolds, darkens and lifts a touch. */
.scale-labels{ position:relative;margin-top:38rem;min-height:120rem; }
.scale-label{
  position:absolute;top:0;left:50%;transform:translateX(-50%);
  width:calc(100% / var(--n,4) - 18rem);
  text-align:center;cursor:pointer;overflow-wrap:break-word;
  font:600 27rem/1.32 "Gotham Rounded","Nunito",sans-serif;color:rgba(0,0,0,.55);
  transition:color .2s var(--ease), transform .2s var(--ease-boing);
}
.scale-label.is-active{
  color:var(--kohl);font-weight:800;transform:translateX(-50%) scale(1.06);
}

/* ── Answer-mode verdict (m.mode:'answer') ──
   Pressing the gate locks the pick: the widget wears is-right / is-wrong
   while the feedback overlay lifts in. Knob ring, fill and the active
   label flip to the verdict colour (same ferozi/lal the MCQ locked choices
   use); wrong adds a short head-shake on the track. Placed AFTER the
   has-selection knob rule — equal specificity, source order decides. */
.scale-widget.is-right .knob{ border-color:var(--ferozi); }
.scale-widget.is-wrong .knob{ border-color:var(--lal); }
.scale-widget.is-right .scale-fill{ background:var(--ferozi); }
.scale-widget.is-wrong .scale-fill{ background:var(--lal); }
.scale-widget.is-right .scale-label.is-active{ color:var(--ferozi); }
.scale-widget.is-wrong .scale-label.is-active{ color:var(--lal); }
.scale-widget.is-wrong .scale-track{ animation:scale-verdict-shake .5s ease; }
@keyframes scale-verdict-shake{
  0%,100%{ transform:translateX(0); }
  20%{ transform:translateX(-8rem); } 40%{ transform:translateX(7rem); }
  60%{ transform:translateX(-5rem); } 80%{ transform:translateX(3rem); }
}

@media (prefers-reduced-motion: reduce){
  .scale-track .knob.idle-hint{ animation:none; }
  .scale-track .knob.idle-hint::before,
  .scale-track .knob.idle-hint::after,
  .scale-track .knob.idle-hint .knob-say,
  .scale-track .knob.idle-hint .knob-say img{ animation:none; }
  .scale-widget.is-wrong .scale-track{ animation:none; }
}

/* ── Arrange-in-Sequence module (modules/sequence.js) ────────────────────────
   Drag cards into ordered drop positions. Cards live IN FLOW (tray cells / slot
   zones); the module lifts + FLIPs them on drag (rect math only on interaction,
   so mount is transform-safe through the page-flip). Layout mirrors the other
   activity shells: flex column, fixed header/footer, centred play area on the
   paper background. Card footprint is one shared size so a placed card exactly
   covers its slot (pure-translate FLIP). */
.module-sequence{
  position:absolute;inset:0;display:flex;flex-direction:column;
  /* Two footprints: raw TRAY thumbnails vs framed DROP zones — decoupled so
     6 thumbnails fit one tray row; the FLIP tween scales a card between the
     two on drop / fly-home.
     --slot-w is a MAX: the slots row never wraps, so with 5–6 positions each
     zone flex-shrinks below it (aspect ratio kept) until the row fits.
     2026-07-07 (user: square drop zones + bigger draggables): --slot-ar is
     1/1 — a 6-up board renders ~277rem squares (width-bound), matching the
     square artwork. The 320 cap is the VERTICAL safety line for sparse
     boards: a 3-slot board at 320 squares + full tray + prompt lands within
     the centre with the guide at its 40rem floor — a taller cap overflows.
     (Area-neutral vs the old 460×340 rectangles.) Tray cells are square too
     (the shipped art is 1:1), sized WITH the tray gap so a 6-card row spans
     the full 1840rem stage width exactly, flush with the zones row's edges:
     6×260 + 5×56 = 1840 (user 2026-07-07 — "cards too close together, use
     the full horizontal space"). Grow one only at the other's expense. */
  --card-w:260rem;--card-h:260rem;
  --slot-w:320rem;--slot-ar:1/1;
}
.module-sequence > .activity-header{ position:relative;flex:0 0 64rem; }
/* z-index:0 demotes the generic footer's z:5 INSIDE sequence only: hovered
   (scale 1.5) and dragged (fixed, z:60) cards live in .sequence-centre's
   z:1 context, and the user wants them painting OVER Back + the hint pill
   when they overhang the footer band (2026-07-07). Footer buttons keep
   their hit area — the bands don't geometrically overlap; only a card's
   scale overhang crosses the line, and then the card is the point. */
.module-sequence > .activity-footer{ position:relative;flex:0 0 85rem;z-index:0; }
/* space-between spreads the three bands across the FULL height: prompt pinned
   at the top, drop zones floating in the middle, tray along the bottom.
   2026-07-09 (user: zones + guide + tray a bit down toward the footer): the
   down-bias is a trimmed bottom padding (tray drops toward the footer) plus
   a margin under the prompt (air under the text; space-between passes ~2/3
   of it to the zones row, ~1/3 to the guide). Margins, never transforms —
   see the fixed-drag containing-block note in bind(). */
.sequence-centre{
  flex:1 1 0;min-height:0;position:relative;z-index:1;overflow:visible;
  display:flex;flex-direction:column;align-items:center;justify-content:space-between;
  gap:24rem;padding:44rem 40rem 24rem;
}

/* Prompt */
.sequence-prompt{ text-align:center;max-width:1560rem;margin-bottom:20rem; }
.sequence-prompt .prompt{ margin:0;color:var(--kohl);font:700 42rem/58rem "Gotham Rounded","Nunito",sans-serif;text-wrap:balance; }
.sequence-prompt .sub{ margin:12rem 0 0;color:rgba(0,0,0,.6);font:500 23rem/32rem "Gotham Rounded","Nunito",sans-serif; }

/* Slots row — bottom-aligned so zones line up whether or not a hint is set.
   ONE row always: no wrap; each slot shrinks equally once the row overflows. */
.sequence-slots{ display:flex;gap:48rem;justify-content:center;align-items:flex-end;width:100%; }
/* No hints authored → the bare zones read a touch low in the leftover space;
   extra bottom margin biases space-between to lift the row (up by half of it).
   A margin, NOT a transform: the zones must never gain a transformed ancestor
   (it would re-base the position:fixed card mid-drag — see the bind() note). */
.sequence-slots.no-hints{ margin-bottom:52rem; }
.sequence-slot{ display:flex;flex-direction:column;align-items:center;justify-content:flex-end;gap:22rem;flex:0 1 var(--slot-w);min-width:0; }
.slot-hint{ color:var(--kohl);text-align:center;max-width:100%;font:600 24rem/1.3 "Gotham Rounded","Nunito",sans-serif; }

/* Drop zone — a flat SQUARE-CORNERED amber wash on ::before (no outline, no
   radius — user 2026-07-07 stripped the dashed border and the rounding; the
   wash is deepened so the socket still reads without its edge). :has()
   retires the wash AND drops the DROP-HERE + badge from flow, so the placed
   card sits CENTRED. */
.slot-zone{
  position:relative;width:100%;aspect-ratio:var(--slot-ar);border-radius:0;
  /* min-height:0 kills the flex content floor — without it, the instant a card
     lands (before bind() wrap-sizes it) its img's intrinsic height inflates the
     zone past its ratio, and the wrap math then measures that swollen box. */
  min-height:0;
  display:flex;align-items:center;justify-content:center;
  transition:transform var(--t-fast) var(--ease-boing), box-shadow var(--t-fast) var(--ease);
}
.slot-zone::before{
  content:"";position:absolute;inset:0;border-radius:inherit;pointer-events:none;
  background:color-mix(in srgb, var(--aamras) 12%, transparent);
  transition:background var(--t-fast) var(--ease), opacity var(--t-fast) var(--ease);
}
.slot-badge{
  position:absolute;top:12rem;left:12rem;z-index:2;width:38rem;height:38rem;border-radius:999rem;
  display:flex;align-items:center;justify-content:center;background:var(--aamras);color:#fff;
  font:800 22rem/1 "Gotham Rounded","Nunito",sans-serif;box-shadow:var(--sh-card);
}
.slot-drop{
  position:relative;z-index:1;text-transform:uppercase;letter-spacing:.6rem;
  color:color-mix(in srgb, var(--aamras) 88%, var(--kohl));
  font:800 26rem/1 "Gotham Rounded","Nunito",sans-serif;
}
/* Hover — a card is over this zone. NB: no `transform` here on purpose — a
   transform would make the slot-zone a containing block for the position:fixed
   card being dragged (when it's over its own slot), re-basing it mid-drag. A
   glow ring gives the same emphasis without that side effect. */
.slot-zone.is-over{ box-shadow:0 0 0 6rem color-mix(in srgb, var(--gulabi) 28%, transparent), 0 0 40rem color-mix(in srgb, var(--gulabi) 30%, transparent); }
.slot-zone.is-over::before{ background:color-mix(in srgb, var(--gulabi) 15%, transparent); }
.slot-zone.is-over .slot-drop{ opacity:.35; }
/* Filled — DROP-HERE + badge leave the flow so the placed card centres. */
.slot-zone:has(.seq-card)::before{ opacity:0; }
.slot-zone:has(.seq-card) .slot-drop{ display:none; }
.slot-zone:has(.seq-card) .slot-badge{ display:none; }
/* Verdict — on a FILLED zone the ring+bloom hugs the CARD's frame instead of
   the zone box (wrapped cards are narrower/taller than the socket, so a
   zone-edge ring floats detached from the artwork); the card-side rules live
   below the card styles. Empty zones keep the zone-edge ring as a fallback.
   Pop/shake stay on the zone so the card rides the animation. */
.slot-zone.is-correct{ box-shadow:0 0 0 5rem color-mix(in srgb, var(--ferozi) 30%, transparent), 0 0 42rem color-mix(in srgb, var(--ferozi) 42%, transparent); animation:seq-pop .5s var(--ease-boing); }
.slot-zone.is-wrong{ box-shadow:0 0 0 5rem color-mix(in srgb, var(--lal) 30%, transparent), 0 0 42rem color-mix(in srgb, var(--lal) 42%, transparent); animation:seq-shake .5s ease; }
.slot-zone.is-correct:has(.seq-card), .slot-zone.is-wrong:has(.seq-card),
.slot-zone.is-over:has(.seq-card){ box-shadow:none; }
@keyframes seq-pop{ 0%{transform:scale(1)} 45%{transform:scale(1.08)} 100%{transform:scale(1)} }
@keyframes seq-shake{ 0%,100%{transform:translateX(0)} 20%{transform:translateX(-8rem)} 40%{transform:translateX(7rem)} 60%{transform:translateX(-5rem)} 80%{transform:translateX(3rem)} }

/* Between-bands guide — signage in the gap: the cards BELOW are the pieces,
   and they belong UP in the zones. It sits IN FLOW between the two bands, so
   space-between both lifts the slots row toward the prompt and widens the
   slots↔tray distance (its own height + gap redistribution) — the spacing
   change IS the guide. 2026-07-07: the signage is now the hint mascot
   speaking the line in a bubble, flanked by the client's hand_point_n
   glyphs pulsing upward (hint-mascot.js supplies both). flex-basis rather
   than a fixed height so a maximal module (8 cards, hints, subtext)
   squeezes it before anything overflows; the figure scales with the band
   (height:100% chain) and the bubble may poke above a fully squeezed band
   — that overflow is visual-only (pointer-events:none) and opens into the
   inter-band gap, never over cards. One-shot: the first grab adds
   .is-spent — opacity only, the held space stays, no mid-activity layout
   shift. */
.seq-guide{
  flex:0 1 104rem;min-height:40rem;
  display:flex;align-items:center;justify-content:center;gap:36rem;
  pointer-events:none;
  transition:opacity .45s var(--ease);
}
/* flex:none on the row's items + a DEFINITE fig width (see .hm-fig note):
   the row must never shrink anything into the bubble. 104rem = the fig's
   width at the full 104rem band (square art); when the band squeezes, the
   svg letterboxes inside the fixed box (art centres, stays whole). */
.seq-guide .hm-plaque{ align-self:stretch;flex:none; }
.seq-guide .hm-fig{ height:100%;width:104rem; }
.seq-guide .hm-bubble{ font-size:26rem;margin-bottom:12rem; }
/* hand_point_n pointers — rise-and-fade pulse, both in phase (they frame
   the plaque like the old chevrons did). */
.seq-guide .point{ flex:none; }
.seq-guide .point svg{
  width:54rem;height:54rem;overflow:visible;
  animation:seq-guide-point 1.2s ease-in-out infinite;
}
@keyframes seq-guide-point{
  0%,100%{ opacity:.5; transform:translateY(2rem); }
  50%    { opacity:1;  transform:translateY(-10rem); }
}
.seq-guide.is-spent{ opacity:0; }
.seq-guide.is-spent *{ animation:none!important; }  /* invisible — stop compositor work */

/* Tray — raw thumbnails along the bottom, spread across the full stage
   width (gap sized with --card-w: 6 cards fill 1840rem exactly; fewer
   cards centre with the same air between them). */
.sequence-tray{ display:flex;gap:56rem;justify-content:center;align-items:center;flex-wrap:wrap;width:100%;min-height:var(--card-h); }
/* 7–8 cards would wrap to a second row — and the tray is a shrinkable flex
   child, so the extra row paints past its box, under the footer and off the
   stage. Shrink the cells instead: one row always (8×181 + 7×56 = 1840). */
.sequence-tray:has(.tray-cell:nth-child(7)){ --card-w:214rem;--card-h:214rem; }
.sequence-tray:has(.tray-cell:nth-child(8)){ --card-w:181rem;--card-h:181rem; }
.tray-cell{ width:var(--card-w);height:var(--card-h);border-radius:0;position:relative; }
.tray-cell:hover{ z-index:6; }   /* a hovered (scaled) thumbnail rides above its neighbours */
/* Full-bleed row: the OUTER cells sit flush with the stage padding, so a
   centred hover pop would grow past the screen edge — grow those two
   inward instead (origin applies in the hover state only; the resting
   tilt keeps its default pivot). */
.tray-cell:first-child .seq-card:not(.is-dragging):hover .seq-card-inner{ transform-origin:left center; }
.tray-cell:last-child .seq-card:not(.is-dragging):hover .seq-card-inner{ transform-origin:right center; }

/* Cards — fill their cell/zone. The OUTER element is what GSAP transforms on
   drag/FLIP (translate + scale between the two footprints); the INNER wrapper
   carries the tilt, via CSS only. */
/* Square corners board-wide (user 2026-07-07) — the artwork shows as a sharp
   print; the inner inherits, so placed/text tiles square up too. */
.seq-card{
  position:relative;width:100%;height:100%;border-radius:0;
  cursor:var(--cur-grab);touch-action:none;will-change:transform;
}
/* Base = RAW image: no frame / border / background (user: "show them as they
   are"). overflow:visible so the image's soft drop-shadow isn't clipped. */
.seq-card-inner{
  width:100%;height:100%;border-radius:inherit;overflow:visible;
  background:transparent;border:0;box-shadow:none;
  display:flex;align-items:center;justify-content:center;
  transform:rotate(var(--tilt,0deg));
  transition:transform var(--t-med) var(--ease-boing), box-shadow var(--t-med) var(--ease);
}
.seq-card.is-dragging{ cursor:var(--cur-grabbing); }
.seq-card.is-dragging .seq-card-inner{ transform:rotate(0deg) scale(1.06); }
/* Dragging a TRAY thumbnail: it stays big the whole way to the zone, but a
   TOUCH smaller than the 1.5 hover — the ~8% grab-compress is the "picked
   up" squeeze (user direction 2026-07-06; previously these two were kept
   equal by design — don't re-lockstep them, and don't let them drift far
   apart either or the grab reads as a jarring shrink). The FLIP settles it
   into the slot on drop. (Cards dragged back OUT of a slot keep the modest
   1.06 lift above.) Multipliers dropped 1.9/1.75 → 1.5/1.38 when the base
   cell grew 180→240 square (2026-07-07): the INSPECTED size stays ~360rem
   — the bump lives in the resting cards, not the zoom. */
.tray-cell .seq-card.is-dragging .seq-card-inner{ transform:rotate(0deg) scale(1.38); }
/* Held card: depth only — the frame-coloured halo went with the hover glows
   (user 2026-07-09, matte pass); the deeper plain shadow carries the lift. */
.tray-cell .seq-card.is-image.is-dragging .card-media img{
  filter:drop-shadow(0 12rem 22rem rgba(0,0,0,.30));
}
/* Placed cards stay RAW — no coloured frame (user 2026-07-07 removed it; the
   wrap math measures the border, so 0 self-adjusts and the art fills its box
   edge-to-edge). Straighten + a paper backing (doubles as the not-yet-loaded
   placeholder) + card shadow for lift. Text cards keep the same backing tile
   always — the tinted background is what keeps them legible. */
.slot-zone .seq-card-inner,
.seq-card.is-text .seq-card-inner{
  overflow:hidden;border:0;
  background:var(--paper);box-shadow:var(--sh-card);
}
.slot-zone .seq-card-inner{ transform:rotate(0deg); }
.slot-zone .seq-card.is-dragging .seq-card-inner,
.seq-card.is-text.is-dragging .seq-card-inner{ transform:rotate(0deg) scale(1.04);box-shadow:var(--sh-img); }
/* Filled-zone glows, moved onto the card frame (see the verdict comment above).
   Extra .seq-card in the selector outranks the framed/text/drag shadows. */
.slot-zone.is-correct .seq-card .seq-card-inner{
  box-shadow:0 0 0 5rem color-mix(in srgb, var(--ferozi) 30%, transparent),
             0 0 42rem color-mix(in srgb, var(--ferozi) 42%, transparent), var(--sh-card);
}
.slot-zone.is-wrong .seq-card .seq-card-inner{
  box-shadow:0 0 0 5rem color-mix(in srgb, var(--lal) 30%, transparent),
             0 0 42rem color-mix(in srgb, var(--lal) 42%, transparent), var(--sh-card);
}
.slot-zone.is-over .seq-card .seq-card-inner{
  box-shadow:0 0 0 6rem color-mix(in srgb, var(--gulabi) 28%, transparent),
             0 0 40rem color-mix(in srgb, var(--gulabi) 30%, transparent), var(--sh-card);
}

/* Image — FIT, never crop (project rule). Raw thumbnails carry their own soft
   drop-shadow so they lift off the paper without a frame; once framed in a slot
   the frame supplies the depth, so the image's own shadow is dropped. */
.card-media{ width:100%;height:100%;display:flex;align-items:center;justify-content:center;background:transparent; }
.card-media img{ width:100%;height:100%;object-fit:contain;display:block; }
.seq-card.is-image .card-media img{ filter:drop-shadow(0 6rem 14rem rgba(0,0,0,.22));transition:filter var(--t-med) var(--ease); }
.seq-card.is-image.is-dragging .card-media img{ filter:drop-shadow(0 14rem 26rem rgba(0,0,0,.30)); }
/* Placed IMAGE cards WRAP the artwork (any aspect — no bars, no crop): on
   placement, bind() sizes the card inline so the framed box = the image's
   natural-ratio art box (+ the inner's measured border), contain-fitted in the
   zone; the img then fills the frame edge-to-edge and the base 100%/100% here
   is just the not-yet-loaded fallback (fills the zone like a text card).
   Sizing lives in JS, not CSS aspect-ratio: Chrome neither transfers a %
   max-height back into width through aspect-ratio nor holds the zone's own
   ratio against a taller child's content floor — both were observed.
   flex:none — the card is a flex item of the zone, and default flex-shrink
   would silently squeeze any inline width sitting proud of the socket. */
.slot-zone .seq-card{ flex:none; }
.slot-zone .seq-card.is-image .card-media img{ filter:none; }
/* Text card content — backing comes from the shared paper tile rule above. */
.card-text{ width:100%;height:100%;display:flex;align-items:center;justify-content:center;padding:18rem 24rem; }
.card-text span{ color:var(--kohl);text-align:center;font:700 30rem/1.28 "Gotham Rounded","Nunito",sans-serif; }

/* Hover to INSPECT (tray thumbnails only) — straighten + scale up so small
   details read. Depth comes from a deeper PLAIN shadow only: the accent-
   coloured halo/ring glows were stripped in the 2026-07-09 matte pass (same
   direction as the slider knobs). The inner's existing transform/box-shadow
   transition (t-med, ease-boing) gives a springy pop; excluded while
   dragging or once placed in a slot. */
.tray-cell .seq-card:not(.is-dragging):hover .seq-card-inner{
  transform:rotate(0deg) scale(1.5);
}
.tray-cell .seq-card.is-image:not(.is-dragging):hover .card-media img{
  filter:drop-shadow(0 10rem 20rem rgba(0,0,0,.30));
}
.tray-cell .seq-card.is-text:not(.is-dragging):hover .seq-card-inner{
  box-shadow:var(--sh-img);
}
/* …and while one is inspected the REST of the line-up recedes a touch, so
   the hovered card reads as picked out of the row without any glow. The
   trigger needs a real (non-drag) hover on a tray card: during a drag,
   pointer capture pins :hover to the held card (excluded above), so the
   tray never jiggles mid-gesture. Receding cards keep their resting tilt;
   the shared inner transition springs both directions. */
.sequence-tray:has(.tray-cell .seq-card:not(.is-dragging):hover)
  .tray-cell .seq-card:not(:hover):not(.is-dragging) .seq-card-inner{
  transform:rotate(var(--tilt,0deg)) scale(.92);
}

/* Hint button — every wrong attempt re-arms it; the first press flies two
   random cards home, later presses one (sequence.js). Amber pill, dark ink
   (white on amber reads ~1.9:1 — same contrast lesson as the CMS primaries). */
.seq-hint-btn{
  background:var(--aamras);color:#3A2A00;border:0;cursor:pointer;
  border-radius:999rem;padding:12rem 30rem;
  font:700 20rem/25rem "Gotham Rounded","Nunito",sans-serif;
  box-shadow:0 6rem 18rem color-mix(in srgb, var(--aamras) 50%, transparent);
  transition:transform var(--t-fast) var(--ease), box-shadow var(--t-fast) var(--ease);
}
.seq-hint-btn:hover{ transform:translateY(-2rem) scale(1.05);
  box-shadow:0 10rem 24rem color-mix(in srgb, var(--aamras) 55%, transparent); }
.seq-hint-btn:active{ transform:scale(.95); }
.seq-hint-btn[hidden]{ display:none; }

/* Skip — appears (and stays) after the first wrong attempt, seated on the
   footer's right where Continue lives on content screens. Same kohl recipe
   as .jigsaw-skip; advances via ctx.goNext(). */
.seq-skip{
  height:56rem;padding:0 26rem;border:0;border-radius:var(--r-cta);
  background:var(--kohl);color:#fff;cursor:pointer;
  font:600 20rem/1 "Gotham Rounded","Nunito",sans-serif;box-shadow:var(--sh-cta);
}
.seq-skip[hidden]{display:none;}

/* A hinted card is locked in — a teal RING says "this one's right" (the same
   semantic as a correct locked MCQ choice) and it can't be dragged out. The
   ring is stronger than the old 26% because it carries the whole signal now
   that cards are borderless. */
.slot-zone .seq-card.is-hinted{ cursor:default; }
.slot-zone .seq-card.is-hinted .seq-card-inner{
  box-shadow:0 0 0 5rem color-mix(in srgb, var(--ferozi) 55%, transparent),
             0 0 24rem color-mix(in srgb, var(--ferozi) 30%, transparent), var(--sh-card);
}

@media (prefers-reduced-motion: reduce){
  .seq-card-inner{ transition:none; }
  .slot-zone.is-correct, .slot-zone.is-wrong{ animation:none; }
  /* Guide stays as a STATIC affordance; still retires on the first grab.
     (Mascot bob/pupils/legs are stilled in the shared .hm reduce block.) */
  .seq-guide .point svg{ animation:none; }
}

/* ── #8 Zoom for MCQ/personality image options (client request) ── */
.choice.is-image .choice-mag{
  position:absolute;right:16rem;bottom:16rem;z-index:2;
  width:48rem;height:48rem;border-radius:999rem;
  display:flex;align-items:center;justify-content:center;
  background:#fff;
  box-shadow:0 4rem 12rem rgba(0,0,0,.40), 0 0 0 3rem rgba(255,255,255,.9);
  transition:transform var(--t-fast) var(--ease)
}
.choice.is-image .choice-mag img{width:22rem;height:22rem}
.choice.is-image .choice-mag:hover{transform:scale(1.1)}
/* The echoed card inside the feedback modal is a display copy — nothing
   binds its magnifier, and #overlay-feedback would bury the zoom anyway. */
/* Client request: keep the magnifier VISIBLE on the correct-answer card in the
   incorrect feedback popup, and wire it (app.js showFeedback) to open the image
   fullscreen. The fullscreen overlay is lifted above the feedback popup below so
   the zoom floats over it rather than being buried. */
.overlay-fullscreen{z-index:20}

/* #8 (extended, client request): every STILL image shown inside a result /
   feedback popup — the fact photo (.fact-card-image) and the stop-reveal /
   simple-feedback figure (.feedback-figure) — carries the same corner
   magnifier, wired in app.js (wirePopupZoom) to open it fullscreen. The
   .has-zoom flag turns the flex figure into the magnifier's positioning
   context; the button matches the choice magnifier's look. */
.feedback-modal .feedback-figure.has-zoom,
.feedback-modal .fact-card-image.has-zoom{position:relative}
.feedback-modal .feedback-figure .choice-mag,
.feedback-modal .fact-card-image .choice-mag{
  position:absolute;right:12rem;bottom:12rem;z-index:3;
  width:48rem;height:48rem;border-radius:999rem;
  display:flex;align-items:center;justify-content:center;
  background:#fff;cursor:pointer;pointer-events:auto;
  box-shadow:0 4rem 12rem rgba(0,0,0,.40), 0 0 0 3rem rgba(255,255,255,.9);
  transition:transform var(--t-fast) var(--ease)
}
.feedback-modal .feedback-figure .choice-mag img,
.feedback-modal .fact-card-image .choice-mag img{width:22rem;height:22rem}
.feedback-modal .feedback-figure .choice-mag:hover,
.feedback-modal .fact-card-image .choice-mag:hover{transform:scale(1.1)}
