/* ══════════════════════════════════════════════════════════
   BaiSed Lucky Draw — shared styles
   Used by the single lucky-draw landing page at /th/lucky-draw/.
   Type: Switzer for Latin, Sarabun for Thai. Both are declared in
   fonts.css and resolve per glyph, because Switzer carries no Thai —
   so one stack handles a line that mixes the two. Do not reintroduce
   Anuphan or Cabinet Grotesk here.
   ══════════════════════════════════════════════════════════ */

*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }

:root {
  --red: #FF6B6B;
  --orange: #FFBD71;
  --yellow: #FF6B6B;
  --bg: #F6F7F9;
  --bg2: #EDEEF2;
  --ink: #1a1a2e;
  /* Darkened for the warm ground: at the old 0.55 and 0.42 the secondary
     and tertiary greys fell to 4.1 and 2.5 against the orange band, under
     AA. These clear 4.5 and 3.4 at the worst point of the gradient while
     keeping the two tiers visually distinct. */
  --muted: rgba(26,26,46,0.66);
  --faint: rgba(26,26,46,0.52);
  --line: rgba(26,26,46,0.10);
  --card: #ffffff;
  --sans: 'Switzer', 'Sarabun', -apple-system, BlinkMacSystemFont, sans-serif;
  --serif: var(--sans);
  --fire: linear-gradient(100deg, #FF6B6B, #FFBD71);
  /* The fan campaign's own hue, drawn from its prize art. It exists
     because the pair used to be fire against var(--ink), and --ink is the
     TEXT colour: on the night page it resolves to #EEF1FC, so that card
     and its tag came out near-white with white type on them and neither
     could be read. A face needs a background token, not a foreground one.
     The light end clears 4.7:1 against white, so the small caps on the
     tag hold AA. */
  --fan: linear-gradient(100deg, #6D28D9, #A93BE0);
  --shell: 1120px;
  /* The star sheet, held as a token because two layers now lay it: the
     fixed night sky, and the permit section's own navy ground, which
     would otherwise cut a starless hole in the night. */
  --stars: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNDAwIiBoZWlnaHQ9IjEwMDAiIHZpZXdCb3g9IjAgMCAxNDAwIDEwMDAiPjxjaXJjbGUgY3g9IjI1Ni4xIiBjeT0iMjcuMSIgcj0iMC43MyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC4zOSIvPjxjaXJjbGUgY3g9IjEwMzYuMSIgY3k9IjczLjkiIHI9IjAuODEiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDUiLz48Y2lyY2xlIGN4PSI3OTAuMiIgY3k9IjM2NC4yIiByPSIwLjc1IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjQiLz48Y2lyY2xlIGN4PSI2MzYuNiIgY3k9IjMzNS4yIiByPSIxLjMiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNzUiLz48Y2lyY2xlIGN4PSIzODEuNCIgY3k9Ijk0MC44IiByPSIwLjkxIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjUyIi8+PGNpcmNsZSBjeD0iMTE1Mi44IiBjeT0iNTM3LjkiIHI9IjEuMyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC43NSIvPjxjaXJjbGUgY3g9IjEwNTQuMCIgY3k9IjU2OC44IiByPSIwLjk0IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjU0Ii8+PGNpcmNsZSBjeD0iNzA1LjMiIGN5PSI1NjYuNSIgcj0iMC42NiIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC4zNCIvPjxjaXJjbGUgY3g9IjczMi44IiBjeT0iNTc0LjgiIHI9IjEuMyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC43NSIvPjxjaXJjbGUgY3g9IjczMC41IiBjeT0iNTY4LjMiIHI9IjAuNjMiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuMzIiLz48Y2lyY2xlIGN4PSIyNy45IiBjeT0iMzc3LjgiIHI9IjAuNzQiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNCIvPjxjaXJjbGUgY3g9IjgyOS4zIiBjeT0iOTE2LjAiIHI9IjAuOTciIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNTYiLz48Y2lyY2xlIGN4PSI4NTUuNiIgY3k9IjgxMi4yIiByPSIwLjkyIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjUyIi8+PGNpcmNsZSBjeD0iNTYxLjkiIGN5PSIxOTcuNSIgcj0iMC42NyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC4zNSIvPjxjaXJjbGUgY3g9IjExODguNyIgY3k9Ijc0Ny45IiByPSIwLjYzIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjMyIi8+PGNpcmNsZSBjeD0iNDc3LjEiIGN5PSI2NjUuNSIgcj0iMC43IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjM3Ii8+PGNpcmNsZSBjeD0iNTkxLjQiIGN5PSI5NTcuMyIgcj0iMC44NSIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC40NyIvPjxjaXJjbGUgY3g9IjMzMi45IiBjeT0iNDU1LjkiIHI9IjAuODIiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDUiLz48Y2lyY2xlIGN4PSIxMzgyLjIiIGN5PSI5MjUuNyIgcj0iMC42OCIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC4zNiIvPjxjaXJjbGUgY3g9Ijk5NC4wIiBjeT0iNTAxLjEiIHI9IjAuNjEiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuMzEiLz48Y2lyY2xlIGN4PSIxMjExLjgiIGN5PSI1MjQuNiIgcj0iMC42NyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC4zNSIvPjxjaXJjbGUgY3g9IjExOTIuNyIgY3k9IjQ5Ny44IiByPSIwLjkxIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjUyIi8+PGNpcmNsZSBjeD0iNjAuOCIgY3k9IjEwMS43IiByPSIwLjk5IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjU4Ii8+PGNpcmNsZSBjeD0iODc5LjMiIGN5PSI4NC40IiByPSIwLjciIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuMzciLz48Y2lyY2xlIGN4PSIxMzA2LjkiIGN5PSI0MTEuMCIgcj0iMC42OSIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC4zNiIvPjxjaXJjbGUgY3g9IjMxOS40IiBjeT0iODc3LjgiIHI9IjAuNjUiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuMzMiLz48Y2lyY2xlIGN4PSIxMDcyLjIiIGN5PSI1NzUuMSIgcj0iMS43IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjk1Ii8+PGNpcmNsZSBjeD0iMjE2LjgiIGN5PSIxNDguNCIgcj0iMC42NCIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC4zMyIvPjxjaXJjbGUgY3g9IjEyNzYuNyIgY3k9IjU0NS4wIiByPSIxLjMiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNzUiLz48Y2lyY2xlIGN4PSIyNDkuMiIgY3k9Ijc2OC41IiByPSIwLjc5IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjQ0Ii8+PGNpcmNsZSBjeD0iNDgxLjIiIGN5PSIxNC44IiByPSIwLjk4IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjU2Ii8+PGNpcmNsZSBjeD0iNzg5LjIiIGN5PSI2MzAuMSIgcj0iMS4zIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjc1Ii8+PGNpcmNsZSBjeD0iNTE0LjkiIGN5PSIzNi45IiByPSIwLjg1IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjQ3Ii8+PGNpcmNsZSBjeD0iNTY2LjEiIGN5PSIzOTMuNiIgcj0iMC42OCIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC4zNSIvPjxjaXJjbGUgY3g9Ijc5OC4yIiBjeT0iMTkzLjUiIHI9IjAuNzkiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDQiLz48Y2lyY2xlIGN4PSIxNjAuNCIgY3k9IjcyNy44IiByPSIwLjk1IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjU0Ii8+PGNpcmNsZSBjeD0iODk1LjUiIGN5PSI3MjIuMCIgcj0iMS4zIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjc1Ii8+PGNpcmNsZSBjeD0iMTM0NC45IiBjeT0iODExLjYiIHI9IjAuNjciIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuMzUiLz48Y2lyY2xlIGN4PSIzNDguNCIgY3k9IjQ2Mi41IiByPSIwLjk0IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjU0Ii8+PGNpcmNsZSBjeD0iMzY3LjAiIGN5PSI0MzYuOSIgcj0iMC43OCIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC40MyIvPjxjaXJjbGUgY3g9IjEwOTUuMSIgY3k9IjQyNy42IiByPSIwLjc4IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjQzIi8+PGNpcmNsZSBjeD0iMTE5NS42IiBjeT0iNDE5LjIiIHI9IjAuOTMiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNTMiLz48Y2lyY2xlIGN4PSI4OTUuOCIgY3k9Ijk4LjMiIHI9IjAuNjQiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuMzMiLz48Y2lyY2xlIGN4PSIxMTEuOSIgY3k9IjUwMC40IiByPSIwLjYzIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjMyIi8+PGNpcmNsZSBjeD0iNjk3LjIiIGN5PSI0ODguMSIgcj0iMC43MiIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC4zOCIvPjxjaXJjbGUgY3g9IjExMzIuMiIgY3k9Ijk0Ni41IiByPSIwLjgxIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjQ0Ii8+PGNpcmNsZSBjeD0iMTAzMy44IiBjeT0iMjM5LjIiIHI9IjAuNzUiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDEiLz48Y2lyY2xlIGN4PSIxMjMuOSIgY3k9Ijk3Ni43IiByPSIwLjc4IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjQzIi8+PGNpcmNsZSBjeD0iNjA2LjUiIGN5PSI2NDkuNyIgcj0iMC44IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjQ0Ii8+PGNpcmNsZSBjeD0iMTM3OS4zIiBjeT0iNzUyLjQiIHI9IjAuOTEiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNTEiLz48Y2lyY2xlIGN4PSIxMjcwLjgiIGN5PSI2MzUuNCIgcj0iMC43NSIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC40MSIvPjxjaXJjbGUgY3g9IjY3NC4xIiBjeT0iMzk2LjgiIHI9IjAuODYiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDgiLz48Y2lyY2xlIGN4PSI0OTEuNyIgY3k9IjQzMS45IiByPSIwLjkiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNTEiLz48Y2lyY2xlIGN4PSI2ODcuNCIgY3k9IjEyOC42IiByPSIwLjkiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNTEiLz48Y2lyY2xlIGN4PSIxMDcuMyIgY3k9IjM3OS4zIiByPSIxLjMiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNzUiLz48Y2lyY2xlIGN4PSIxMjc0LjYiIGN5PSI2NDguNSIgcj0iMC45NiIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC41NSIvPjxjaXJjbGUgY3g9IjQxMS4yIiBjeT0iOTA0LjgiIHI9IjAuNjYiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuMzQiLz48Y2lyY2xlIGN4PSI0NzcuNiIgY3k9IjYzNi42IiByPSIwLjkxIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjUyIi8+PGNpcmNsZSBjeD0iODYzLjQiIGN5PSIyMDguMSIgcj0iMC44NiIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC40OSIvPjxjaXJjbGUgY3g9IjgzMi40IiBjeT0iOTAxLjQiIHI9IjEuNyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC45NSIvPjxjaXJjbGUgY3g9IjcxOS4xIiBjeT0iODk2LjMiIHI9IjAuNzgiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDMiLz48Y2lyY2xlIGN4PSIzOTYuOCIgY3k9Ijg4MC44IiByPSIwLjYzIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjMyIi8+PGNpcmNsZSBjeD0iMTMxNi4zIiBjeT0iNDg3LjIiIHI9IjEuMyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC43NSIvPjxjaXJjbGUgY3g9IjEzMzcuOSIgY3k9IjI4MS4wIiByPSIxLjciIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuOTUiLz48Y2lyY2xlIGN4PSIxOTUuMyIgY3k9IjgwMi4xIiByPSIxLjMiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNzUiLz48Y2lyY2xlIGN4PSI5NzkuMSIgY3k9IjY4Mi40IiByPSIxLjAiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNTgiLz48Y2lyY2xlIGN4PSIxMTQuNCIgY3k9IjUxMy44IiByPSIwLjc3IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjQyIi8+PGNpcmNsZSBjeD0iMTM4Mi4zIiBjeT0iMjc2LjIiIHI9IjEuMyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC43NSIvPjxjaXJjbGUgY3g9Ijc0NC4xIiBjeT0iNzIwLjEiIHI9IjAuOTkiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNTciLz48Y2lyY2xlIGN4PSI1NTkuOSIgY3k9Ijc5MC40IiByPSIxLjMiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNzUiLz48Y2lyY2xlIGN4PSI4MzYuNCIgY3k9IjYxMy45IiByPSIwLjYyIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjMxIi8+PGNpcmNsZSBjeD0iNjI2LjEiIGN5PSI2NzAuNiIgcj0iMC43NiIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC40MSIvPjxjaXJjbGUgY3g9IjY0OS44IiBjeT0iNzk0LjciIHI9IjAuODYiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDgiLz48Y2lyY2xlIGN4PSI2LjciIGN5PSI5MDkuOSIgcj0iMS4zIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjc1Ii8+PGNpcmNsZSBjeD0iMTAyMC42IiBjeT0iMzAxLjIiIHI9IjEuMyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC43NSIvPjxjaXJjbGUgY3g9IjQ2Ni40IiBjeT0iOTIyLjYiIHI9IjAuOCIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC40NCIvPjxjaXJjbGUgY3g9IjMzMC4yIiBjeT0iNjY0LjUiIHI9IjAuODciIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDkiLz48Y2lyY2xlIGN4PSIxMTQ3LjUiIGN5PSIxODMuMiIgcj0iMS4wIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjU4Ii8+PGNpcmNsZSBjeD0iMTM3Mi4xIiBjeT0iNTk0LjYiIHI9IjAuOCIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC40NCIvPjxjaXJjbGUgY3g9IjM3Ni4wIiBjeT0iODc3LjgiIHI9IjAuODQiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDciLz48Y2lyY2xlIGN4PSIxMjIuMSIgY3k9IjI0My42IiByPSIwLjYxIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjMxIi8+PGNpcmNsZSBjeD0iNzQyLjkiIGN5PSI4NjguNCIgcj0iMC44MyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC40NiIvPjxjaXJjbGUgY3g9IjUzMS4zIiBjeT0iMTgzLjQiIHI9IjAuOTgiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNTciLz48Y2lyY2xlIGN4PSI5NTYuNSIgY3k9IjI4LjMiIHI9IjAuOSIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC41MSIvPjxjaXJjbGUgY3g9IjEwNzEuMSIgY3k9Ijk3Ny45IiByPSIwLjc2IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjQxIi8+PGNpcmNsZSBjeD0iODcwLjMiIGN5PSI3OTcuNSIgcj0iMC44OSIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC41MSIvPjxjaXJjbGUgY3g9Ijg1OS40IiBjeT0iNTA2LjMiIHI9IjEuMyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC43NSIvPjxjaXJjbGUgY3g9IjE4MS42IiBjeT0iNjU1LjciIHI9IjAuODQiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDciLz48Y2lyY2xlIGN4PSI2NDEuOCIgY3k9Ijk2Ny42IiByPSIwLjgzIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjQ2Ii8+PGNpcmNsZSBjeD0iODE0LjUiIGN5PSIzMjMuOCIgcj0iMC42NCIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC4zMyIvPjxjaXJjbGUgY3g9IjI0My40IiBjeT0iNzY2LjgiIHI9IjEuMyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC43NSIvPjxjaXJjbGUgY3g9IjM4MS4wIiBjeT0iNzc4LjgiIHI9IjAuODEiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDQiLz48Y2lyY2xlIGN4PSIxMzA3LjkiIGN5PSIxMzkuNSIgcj0iMC45IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjUxIi8+PGNpcmNsZSBjeD0iODY2LjAiIGN5PSI2NTguMiIgcj0iMC45NiIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC41NSIvPjxjaXJjbGUgY3g9IjkyNy44IiBjeT0iNDQwLjgiIHI9IjAuNjIiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuMzIiLz48Y2lyY2xlIGN4PSIxMTc4LjUiIGN5PSI2MjIuNyIgcj0iMS4zIiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjc1Ii8+PGNpcmNsZSBjeD0iNDk4LjciIGN5PSIyMy4wIiByPSIxLjciIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuOTUiLz48Y2lyY2xlIGN4PSIxMTc0LjUiIGN5PSI0NjIuMyIgcj0iMC45IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjUxIi8+PGNpcmNsZSBjeD0iNjQyLjIiIGN5PSI5MzQuMiIgcj0iMC42NiIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC4zNCIvPjxjaXJjbGUgY3g9IjM3NS43IiBjeT0iOTQzLjQiIHI9IjAuODQiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDciLz48Y2lyY2xlIGN4PSI4MjQuNiIgY3k9IjI0LjgiIHI9IjAuNjEiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuMzEiLz48Y2lyY2xlIGN4PSI1My44IiBjeT0iMzI1LjciIHI9IjAuNzYiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDIiLz48Y2lyY2xlIGN4PSI0OTcuMSIgY3k9IjM1NC4wIiByPSIwLjY4IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjM2Ii8+PGNpcmNsZSBjeD0iMTIzMy42IiBjeT0iODg5LjEiIHI9IjEuMyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC43NSIvPjxjaXJjbGUgY3g9IjI3MC41IiBjeT0iOTk3LjMiIHI9IjAuNzciIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDIiLz48Y2lyY2xlIGN4PSIxMjczLjYiIGN5PSI5MTUuMSIgcj0iMC45OCIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC41NyIvPjxjaXJjbGUgY3g9IjkwOS4zIiBjeT0iNTU0LjgiIHI9IjAuODQiIGZpbGw9IiNmZmYiIG9wYWNpdHk9IjAuNDciLz48Y2lyY2xlIGN4PSIxMDYzLjAiIGN5PSI5NDYuNyIgcj0iMC44OCIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC41Ii8+PGNpcmNsZSBjeD0iOTQuNSIgY3k9IjMzNi45IiByPSIwLjc4IiBmaWxsPSIjZmZmIiBvcGFjaXR5PSIwLjQyIi8+PGNpcmNsZSBjeD0iOTQ0LjgiIGN5PSI4OTEuNCIgcj0iMC43MyIgZmlsbD0iI2ZmZiIgb3BhY2l0eT0iMC4zOSIvPjwvc3ZnPg==");
}

html {
  scroll-behavior: smooth;
  -webkit-text-size-adjust: 100%;
  /* NO RUBBER BAND, and this is the only reliable cure for the strip that
     used to appear under the footer.

     Scrolling past either end of the page lifts the document off the window,
     and the layers that make up this page's ground do not come with it: the
     warm gradient and the night sky are both position: fixed, so they stay
     exactly where they are while the content slides away from them. At the
     bottom that exposed the night sky, star field and all, several shades
     off the footer and clearly a different surface.

     Nothing paintable can reach into that gap. The footer's own shadow
     covers real space below it, which is why it is still there and still
     earning its keep on a phone, but the overscroll gap is outside the
     document entirely and only the fixed layers are drawn in it. So the
     bounce itself goes, and with no gap there is nothing to paint.

     The cost is the elastic bounce at both ends, and on Android the
     pull-to-refresh gesture. On a page this long, neither is load-bearing. */
  overscroll-behavior-y: none;
}

body {
  background: var(--bg);
  color: var(--ink);
  font-family: var(--sans);
  line-height: 1.65;
  margin: 0;
  overflow-x: hidden;
  -webkit-font-smoothing: antialiased;
}

img { max-width: 100%; display: block; }
a { color: inherit; }

.shell { max-width: var(--shell); margin: 0 auto; width: 100%; }

/* Section rhythm — this is what keeps the page short */
.band { padding: 88px 6vw; }
.band--tight { padding: 64px 6vw; }
.band--alt { background: var(--bg2); }
.band--line { border-top: 1px solid var(--line); }

.eyebrow {
  font-size: 0.68rem;
  letter-spacing: 0.22em;
  text-transform: uppercase;
  color: var(--faint);
  font-weight: 600;
  margin-bottom: 14px;
}
html[lang="th"] .eyebrow { letter-spacing: 0.12em; }

h1, h2, h3, h4 { font-family: var(--serif); letter-spacing: -0.02em; line-height: 1.13; font-weight: 700; }
html[lang="th"] h1, html[lang="th"] h2, html[lang="th"] h3, html[lang="th"] h4 {
  font-family: var(--sans);
  letter-spacing: 0;
  line-height: 1.35;
  font-weight: 600;
}
.grad {
  background: var(--fire);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
em.grad { font-style: normal; font-weight: 800; }
html[lang="th"] em.grad { font-style: normal; font-weight: 700; }

.sec-head { max-width: 720px; margin-bottom: 44px; }
.sec-head h2 { font-size: clamp(1.7rem, 3.4vw, 2.7rem); margin-bottom: 14px; }
.sec-head p { color: var(--muted); font-weight: 300; font-size: 1rem; }

/* ── NAV ────────────────────────────────────────────────── */
#nav {
  position: sticky;
  top: 0;
  z-index: 900;
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 16px;
  padding: 10px 6vw;
  background: rgba(246,247,249,0.88);
  backdrop-filter: blur(14px);
  -webkit-backdrop-filter: blur(14px);
  border-bottom: 1px solid transparent;
  transition: border-color 0.3s, box-shadow 0.3s;
}
#nav.stuck { border-bottom-color: var(--line); box-shadow: 0 4px 20px rgba(26,26,46,0.04); }
.nav-logo {
  display: flex;
  align-items: center;
  text-decoration: none;
  font-family: var(--sans);
  font-size: 30px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.01em;
  flex-shrink: 0;
}
.nav-logo img { height: 52px; width: auto; margin-right: 8px; }
.nav-logo span {
  background: var(--fire);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
}
/* ── THE MARK AS ONE FILE ─────────────────────────────────
   A modifier rather than an edit to the two rules above, because this
   stylesheet is shared: about.html and /th/lucky-draw/ still build the
   mark the old way, out of baisedSplash.png beside a gradient-clipped
   <span>, and rewriting .nav-logo in place would leave their wordmark as
   plain 30px text with no gradient on it.

   46, not the 52 the bare icon wore. The file carries the icon and the
   wordmark at their true relative sizes, so height is the only handle:
   at 52 the lockup came out 178 wide against the old pair's 157, and the
   whole bar shifted right. 46 lands it at 158. Matching the WIDTH is what
   matters here — the bar is a row, and its first item's width is what
   every gap after it is measured from. */
.nav-logo--lockup img { height: 46px; width: auto; margin-right: 0; display: block; }
.nav-links { display: flex; gap: 6px; list-style: none; align-items: center; }
.nav-links a {
  text-decoration: none;
  color: var(--nav-fg, var(--muted));
  font-size: 0.82rem;
  padding: 8px 15px;
  border-radius: 100px;
  transition: color 0.2s, background 0.2s;
  white-space: nowrap;
}
.nav-links a:hover { color: var(--nav-fg-strong, var(--ink)); background: var(--nav-hover, rgba(26,26,46,0.05)); }
.nav-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
/* The phone menu button. Only exists under 900px; see the block that hides
   .nav-links there. */
.nav-toggle { display: none; }
.lang-switch {
  display: flex;
  border: 1px solid var(--line);
  border-radius: 100px;
  overflow: hidden;
  background: var(--nav-pill, #fff);
}
/* The two controls are <button> now. A bare button carries a UA background,
   border and font, none of which this pill wants, so they come off here. */
.lang-switch button {
  appearance: none;
  -webkit-appearance: none;
  border: 0;
  background: none;
  cursor: pointer;
}
.lang-switch a,
.lang-switch button {
  font-family: var(--sans);
  font-size: 0.72rem;
  font-weight: 600;
  letter-spacing: 0.05em;
  padding: 7px 14px;
  text-decoration: none;
  color: var(--nav-fg, var(--faint));
  transition: color 0.2s, background 0.2s;
}
.lang-switch a:hover,
.lang-switch button:hover { color: var(--nav-fg-strong, var(--ink)); }
/* Matches the ATTRIBUTE, not one spelling of its value. The markup writes
   aria-current="true" because that is what every already-cached copy of
   this file targets: HTML and CSS are fetched separately and expire
   separately, so a visitor can hold an old stylesheet against new markup.
   Keying the highlight to a value only the new CSS knows about is what left
   the pill unhighlighted and grey during that window. */
.lang-switch a[aria-current],
.lang-switch button[aria-current] { background: var(--fire); color: #fff; }
.lang-switch button:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }

.btn {
  display: inline-block;
  border-radius: 100px;
  text-decoration: none;
  font-weight: 500;
  font-size: 0.9rem;
  padding: 14px 30px;
  transition: transform 0.2s, box-shadow 0.2s, opacity 0.2s, background 0.2s, border-color 0.2s;
  text-align: center;
}
.btn--fire { background: var(--fire); color: #fff; box-shadow: 0 6px 24px rgba(255,107,107,0.26); }
.btn--fire:hover { transform: translateY(-2px); box-shadow: 0 12px 34px rgba(255,107,107,0.36); opacity: 0.95; }
.btn--ghost { border: 1px solid rgba(26,26,46,0.18); color: rgba(26,26,46,0.8); }
.btn--ghost:hover { border-color: rgba(26,26,46,0.34); background: rgba(26,26,46,0.04); }
.btn--sm { padding: 9px 20px; font-size: 0.8rem; }

/* ── HERO ───────────────────────────────────────────────── */
#hero {
  position: relative;
  padding: 76px 6vw 68px;
  overflow: hidden;
  text-align: center;
}
#hero::before {
  content: '';
  position: absolute;
  top: -180px; left: 50%;
  transform: translateX(-50%);
  width: 1000px; height: 660px;
  max-width: 150vw;
  background: radial-gradient(ellipse, rgba(255,189,113,0.24) 0%, rgba(255,107,107,0.08) 46%, transparent 72%);
  pointer-events: none;
}
.hero-inner { position: relative; z-index: 1; max-width: 820px; margin: 0 auto; }
#hero h1 {
  font-size: clamp(2rem, 5.2vw, 3.9rem);
  line-height: 1.08;
  letter-spacing: -0.035em;
  margin-bottom: 20px;
}
html[lang="th"] #hero h1 { line-height: 1.3; letter-spacing: -0.01em; }
.hero-lede {
  font-size: clamp(1rem, 1.5vw, 1.15rem);
  color: var(--muted);
  font-weight: 300;
  max-width: 620px;
  margin: 0 auto 32px;
}
.hero-cta { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-bottom: 40px; }

/* Three proof chips right under the fold */
.proof {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 14px;
  max-width: 660px;
  margin: 0 auto;
}
.proof div {
  background: rgba(255,255,255,0.78);
  border: 1px solid var(--line);
  border-radius: 14px;
  padding: 16px 12px;
}
.proof b {
  display: block;
  font-family: var(--serif);
  font-size: clamp(1.25rem, 2.6vw, 1.85rem);
  font-weight: 800;
  line-height: 1.1;
  background: var(--fire);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  margin-bottom: 3px;
}
.proof span { font-size: 0.76rem; color: var(--muted); }


/* ── STEPS ──────────────────────────────────────────────── */
.steps { display: grid; grid-template-columns: repeat(4, 1fr); gap: 18px; counter-reset: step; }
.step {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 26px 22px;
}
.step-n {
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  color: var(--orange);
  margin-bottom: 12px;
}
.step h3 { font-size: 1.05rem; font-weight: 700; margin-bottom: 8px; }
.step p { font-size: 0.85rem; color: var(--muted); font-weight: 300; }

/* ── FAIR / FEATURE CARDS ───────────────────────────────── */
.cards { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }

/* ── ONE GROUND FOR #whystack AND #why ─────────────────────
   The two sections each painted their own rainbow and the two could never
   join: a wash that is sticky and one viewport tall anchors its gradient
   to the WINDOW, so the boundary showed whatever part of each circle
   happened to fall under it at that scroll. No amount of matching stops or
   tints fixes that, and flipping one ring to bloom from the other end was
   tried and did not.

   Painted once instead. Sticky inside the WRAPPER rather than inside a
   section, so it stays pinned across both of them and there is no boundary
   left to line up — what the reader scrolls past is a single unbroken
   ground. The negative margin takes it back out of flow so it costs the
   band no height, the same trick .whystack__wash uses for its own pin.

   Behind everything: both sections are transparent over it, and z-index
   -1 keeps it under the road, the cards, the heading and the printer. */
.rainbowband { position: relative; }
.rainbowband__ground {
  position: sticky;
  top: 0;
  height: 100vh;
  margin-bottom: -100vh;
  z-index: -1;
  pointer-events: none;
  --wash: 0;
  /* The journey's nine step colours, mixed well down into white — the same
     set both sections used, kept in one place now. */
  --w9: color-mix(in srgb, #AC4293 52%, #fff);
  --w8: color-mix(in srgb, #7154C6 42%, #fff);
  --w7: color-mix(in srgb, #4067C4 38%, #fff);
  --w6: color-mix(in srgb, #1F86AE 38%, #fff);
  --w5: color-mix(in srgb, #1F927B 38%, #fff);
  --w4: color-mix(in srgb, #588E2E 38%, #fff);
  --w3: color-mix(in srgb, #AD7B0B 40%, #fff);
  --w2: color-mix(in srgb, #D2661F 42%, #fff);
  --w1: color-mix(in srgb, #E1543E 44%, #fff);
  /* NOTHING OF ITS OWN before the paint. At --wash: 0 every stop collapses
     to the origin and the whole thing is transparent, so what shows is the
     page's own ground — which is what #whystack looked like before the pour
     arrived, and the state the road is built to interrupt. */
  background-color: transparent;
  /* AND IT EXPANDS. Every stop is a fraction of --wash, so the ring opens
     out of the point the road crosses in and the spectrum grows with it.
     washOne() writes --wash here rather than to #whystack's own wash, which
     is what lets the flood and the shared ground coexist: it expands once,
     on the paint's arrival, then clamps at 1 and holds for the rest of the
     band, so #why and #data-demo inherit the finished spread rather than
     starting a second one. --wash-x is the crossing, measured off the
     ribbon's own path. */
  background-image: radial-gradient(circle at var(--wash-x, 50%) 0%,
      var(--w9) calc(var(--wash) * 22%), var(--w8) calc(var(--wash) * 32%),
      var(--w7) calc(var(--wash) * 42%), var(--w6) calc(var(--wash) * 52%),
      var(--w5) calc(var(--wash) * 62%), var(--w4) calc(var(--wash) * 72%),
      var(--w3) calc(var(--wash) * 81%), var(--w2) calc(var(--wash) * 89%),
      var(--w1) calc(var(--wash) * 95%),
      #FFE3D1 calc(var(--wash) * 100%),
      rgba(255,227,209,0) calc(var(--wash) * 118%));
  background-repeat: no-repeat;
}
/* THE COPY THAT PINS INSIDE #channels-fit, one section past the wrapper.
   It starts at the finished spread rather than at nothing, because it is
   never on screen for the pour: by the time the reader is this far down
   the band's own ground has been clamped at 1 for several sections. That
   is also the honest no-script state - with the driver never running, the
   band above keeps its opaque ground and this one keeps the spectrum,
   instead of both collapsing to a transparent strip. washOne() overwrites
   both custom properties every frame it writes the band's, so the two can
   never disagree once the script is up. */
.rainbowband__ground--tail {
  --wash: 1;
  /* BACK OUT OF THE BAND'S PADDING, on both axes. Its host is a .band, so
     its content box is already inset by the page gutter and by the band's
     own top rule, and a ground sized to that is a panel rather than a
     ground.

     Sideways it matters because the ring's origin is a percentage of the
     element's own width, so an inset box puts the origin somewhere the
     band's ground above it never had it. Negative margins rather than
     100vw: 100vw counts the scrollbar and the band does not.

     Upward it matters more, and it is the reason for the second pair. A
     sticky box pins when its OWN top edge would leave the window, and this
     one starts a padding's depth below the section's top - so pinned and
     "section reached the top" were the same moment only to within that
     padding, and in the gap between them the ring bloomed from a line part
     way down the screen. Lifted flush with the section's top edge, the two
     coincide exactly. --tail-lift is added back into margin-bottom so the
     whole thing still costs the section nothing in flow. */
  --tail-inset: 6vw;
  --tail-lift: 64px;
  margin-inline: calc(-1 * var(--tail-inset));
  margin-top: calc(-1 * var(--tail-lift));
  margin-bottom: calc(var(--tail-lift) - 100vh);
}

/* ── Why BaiSed ───────────────────────────────────────────
   One line held for a screen. The scene's own height lives with the other
   scene heights further down. */
/* The second landing. The paint arrives here amber rather than magenta —
   the road's own gradient turns as it crosses in, see flowGradient's tail
   stops — and the section floods that colour out from the contact, the same
   way #whystack floods rainbow. Two landings, two colours, one pour.

   Inside the PINNED frame, not the section: this scene is 180vh and its
   frame is the one screen the reader actually sees, so a wash on the
   section would be sized to a box mostly off screen and its circle would
   have to be enormous to cover the part in view. */
.why__wash {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  --wash: 0;
  /* PAINTS NOTHING. The rainbow this used to carry moved up to
     .rainbowband__ground, which is pinned across this section AND the band
     above so the two share one unbroken ground — see its note. The nine
     colour tokens went with it.
     The element stays for two reasons that have nothing to do with colour:
     washOne() writes --wash to it every frame, and it carries the
     right-hand dissolve mask the proof panel lands on. */
  background: transparent;
  /* Nothing overflows this any more — the wedges that needed holding in
     are gone — but it costs nothing and guards whatever lands here next. */
  overflow: hidden;
  /* AND IT ENDS WHERE THE PHONE BEGINS. Full bleed while the heading is
     centred and there is no panel; as the phone arrives the whole
     composition retracts to a hard edge just left of it, so the phone
     stands on clean ground instead of on top of the artwork. Driven by
     the same --why-split as the heading and the panel, so all three
     move as one gesture.

     Losing the wash's own #FFFBEE on the clipped side costs nothing —
     .scene--why declares the same value underneath it, so the join is
     invisible and there is no seam to line up. */
  /* IT DISSOLVES, it does not stop. A hard inset() cut ended the artwork
     on a straight vertical line, and a straight line with plain ground
     on the other side of it reads as a panel edge — the right-hand third
     of the screen looked like a container it is not. Nothing is painted
     there at all; it is the section's own #FFFBEE both sides of the cut.

     A mask fading over the last 220px removes the line and keeps what
     the cut was for: the phone still lands on ground the artwork has let
     go of. Both the fade's width and its position ride on --why-split,
     so at split 0 the two stops collapse onto 100% and the wash is
     opaque edge to edge, exactly as it was before the panel arrives. */
  --why-edge: calc(100% - var(--why-split, 0) * var(--why-wash-cut, 0px));
  -webkit-mask-image: linear-gradient(to right,
    #000 calc(var(--why-edge) - var(--why-split, 0) * 220px), transparent var(--why-edge));
          mask-image: linear-gradient(to right,
    #000 calc(var(--why-edge) - var(--why-split, 0) * 220px), transparent var(--why-edge));
}
/* NO ARTWORK ON THIS GROUND. The four hard-edged wedges that used to
   slide in from the four edges are gone, and so are the clip paths they
   were cut with. What is left is the flat butter below, which the
   section itself also declares — so .why__wash paints nothing the
   section would not paint on its own. It stays because washOne() writes
   --wash to it every frame and because it carries the right-hand
   dissolve the proof panel needs. */
@media (prefers-reduced-motion: reduce) { .why__wash { --wash: 1; } }
/* Over the wash. It is a positioned element at z-index 0, which paints
   above static content, so the question has to be lifted or it sits under
   the colour it caused. */
/* Centred, and the pour clears it on the left rather than the type moving
   out of the way. That costs the road a much harder turn — see LEAN and
   R_TAIL in lucky-draw-scenes.js, which now run the ribbon out to the edge
   of its own column — so if either is ever loosened, check this heading is
   still clear before shipping it. */
/* ── THE HEADLINE STAYS PUT AND STEPS BACK ──────────────
   IT DOES NOT MOVE SIDEWAYS. It used to travel to the left gutter as the
   phone arrived at the right one, which put the words hard against the
   page margin with every bit of slack piled up between them and the
   phone. The heading is centred on the frame, and it stays centred for
   the whole section; the only thing the split changes about it is its
   size.

   The transform is composed in CSS from custom properties rather than
   written as a string by the driver, and that is not tidiness. The
   entrance (updateWhy) and the split (updateWhyRail) are computed in
   different functions on different clocks; both writing element.style
   .transform means whichever ran last that frame wins and the other is
   simply lost. As separate properties they compose, and neither can
   clobber the other.

   The scale is what makes it fit, and it is now SOLVED rather than
   typed. Only the phrases from "Any paper receipt." onward are ever on
   screen at the same time as the panel — the split is still 0 while the
   two before them are up, so the longest phrase of all, "Because we are
   the best." at 955px, never has to clear anything. measureWhySplit()
   takes the widest of the phrases that do, and picks the largest scale
   up to 0.78 that keeps its ink 40px clear of the panel's own left
   edge, whatever the viewport has made of both. At 1440 that solves to
   about 0.73. It is a scale and not a width because a width would
   re-wrap the phrases, and layoutWhyHeadline() has already measured
   their heights to position the rail — re-wrapping mid-scroll
   invalidates every one of them.

   transform-origin is CENTRE, and that is load-bearing now. Off the left
   edge the box shrinks toward its own left and the centred type inside
   it drifts left with it, which is the thing this whole change is
   undoing. */
.why-q {
  /* --why-entrance is gone: the opening move is a rail slide now, not a
     nudge on this element. What is left here is the split's scale. */
  /* THE HEADLINE GOES INTO THE MACHINE. --exit-dx/dy are the vector from
     the heading's own middle to the printer's paper slot, measured once by
     measureWhyExit(); --exit-s is the size receipt type is next to this,
     about a ninth. Multiplied by --exit, the sentence travels down into
     the slot and shrinks to a printed line as it goes, which is the same
     move the paper then makes coming back out.

     Translate OUTSIDE the scale, because transforms apply right to left:
     the heading shrinks about its own centre first and is then carried to
     the slot, rather than being carried and having the journey shrink
     with it. */
  /* THE HEADING ITSELF NO LONGER TRAVELS. It used to fly as one block and
     shrink toward the printed line, which made the sentence a single object
     being swapped for another at the end. The glyphs go individually now —
     see --gx/--gy/--gs on .why-q__ch — so this is back to carrying nothing
     but the panel split it always had. */
  /* --ride carries the whole sentence up with the machine as it climbs
     into frame, written by updateWhy off the feed's own progress so the two
     move on one clock. The glyphs travel individually afterwards, from
     wherever this has left them — see --gx/--gy/--gs on .why-q__ch. */
  transform: translateY(var(--ride, 0px))
             scale(calc(1 - var(--why-split, 0) * (1 - var(--why-q-scale, 0.78))));
  transform-origin: center center;
  /* OUT OF THE FRAME'S FLOW, and centred on its own. The frame is shared
     with the printer now, and .scene--feed .sticky-frame aligns to
     flex-start with a padding-top solved for the machine — which would
     have carried the heading up there with it. Absolute and centred on
     its own anchor, the two occupy the same frame without either one
     placing the other. Same trick .why-proof already used for the same
     reason. */
  position: absolute;
  left: 0;
  right: 0;
  top: 50%;
  translate: 0 -50%;
  /* 2, above .why-proof's 1: the proof panel is centred on this same point
     and the two overlap outright, and it comes later in the DOM — at equal
     z-index the phone would paint over the words. */
  z-index: 2;
  text-align: center;
  /* Dark, and unconditionally so: the section carries its own pale ground
     from the moment it appears, so there is no dark phase for this to
     survive. It was near-white when that ground was near-black; the butter
     palette inverts the whole relationship, and a near-white heading on
     #FFFBEE measures 1.05:1. This ink is the palette's own hue taken most
     of the way to black, so it reads as part of the composition rather
     than as type dropped on top of it. */
  color: #6E4A12;
  /* A DISPLAY SERIF, for this heading only. The page's --serif is aliased to
     --sans, so there is no serif in the type system to reach for, and pulling
     a webfont in for one line would be a new network request and a brand
     decision this change is not entitled to make. A system stack instead:
     picked for CHARACTER rather than for being a serif. The face in the
     piece this is drawn from is warm, rounded and low in contrast, so
     Georgia leads. Didot was tried first and is its exact opposite —
     hairline thins, high drama — and read as a fashion masthead. */
  /* The page's OWN face, not a borrowed one. The display serif here was
     chosen to match the artwork these shapes were traced from, which was
     built in indigo and blush; the palette has since moved to the brand's
     butter, so the type follows it home. Switzer at its heaviest, tracked
     tight, is what every other heading on this page is set in, and three
     words at this size do not need a second voice to carry them.
     Two duplicate declarations for weight and tracking were collapsed
     while this was rewritten. */
  font-family: var(--sans);
  font-weight: 800;
  /* BIGGER. Capped at 6.4rem rather than 5.4: at 1440 that is 102px
     against 86, and the longest phrase's ink goes 955px to about 1132,
     which still clears the 1268px content box so nothing wraps. The cap
     is what has to be watched here, not the vw term — a phrase that wraps
     puts two lines in a mask sized for one. The panel's own clearance
     needs no thought: measureWhySplit() solves --why-q-scale against
     whatever this resolves to. */
  font-size: clamp(3.2rem, 8vw, 6.4rem);
  line-height: 1.08;
  /* Opened up from -0.035em. At 102px with a 14px extrusion behind every
     glyph, the tighter tracking ran the sides of one letter into the face
     of the next and the word read as a block. */
  letter-spacing: -0.015em;
}
/* THE QUESTION ANSWERS ITSELF, AS A SLIDE, not a crossfade — four phrases
   now, not two: "Why BaiSed?" draws up and out, "Because we are the
   best." rises into its place, then "We cover physical." and "We cover
   online too." follow, each taking the slot the one before it held. Same
   odometer reveal as the numbered rows on lovebaised.com's own business
   page: a mask the height of one line, and a rail of stacked lines
   translated behind it. It reads as one phrase being replaced by the
   next rather than one dissolving into the other, which a crossfade
   can't do no matter how it's timed.

   The mask's height and the rail's own travel are both computed and
   written by updateWhyRail()/layoutWhyHeadline() in lucky-draw-scenes.js,
   not by this file — and as a PLAIN INLINE TRANSFORM, not a custom
   property fed to calc(). Two phrases can be flipped by one calc()
   expression; four phrases means three hand-offs of possibly different
   heights (longer in Thai, wrapping differently at narrow widths), which
   is a piecewise interpolation calc() cannot branch on. JS already has to
   measure every phrase to size the mask, so it carries the shift too.

   No transition property on the rail — its transform is written every
   scroll frame, the same way --wash is, so the motion is already
   continuous. A CSS transition racing a value already being animated by
   script is exactly the stutter every other scroll-scrubbed value on
   this page avoids by leaving transition unset. */
/* ── THE HAND-OFF IS A FADE-THROUGH NOW, NOT A RAIL ──────
   The odometer described above was right for four phrases of one shape
   each. It is wrong for two phrases as different as these: "Why BaiSed?"
   is two words and the answer is ten, so a window one line tall had the
   short one sliced off at the top while the long one came in sliced off
   at the bottom, and for a third of the scroll the screen held two
   beheaded headlines and no whole one.

   So nothing is clipped and nothing is stacked in flow. The phrases sit
   on top of each other in one slot, the slot is as tall as the tallest
   of them, and the driver (updateWhyRail) hands the slot from one to the
   next by opacity and a short drift: the outgoing phrase fades as it
   lifts, THEN the incoming one fades in rising into place. Sequential,
   not crossed, so the two are never on screen together. The direction
   is still up, which is what was asked for; what changed is that the
   window no longer eats whatever has not finished moving. */
.why-q__mask {
  display: block;
  overflow: visible;
}
.why-q__rail {
  display: block;
  position: relative;      /* the slot; its height is written by layoutWhyHeadline() */
}
/* ── THE PAINT FILLS THE LETTERS ───────────────────────────
   The glyphs are extruded blocks and the paint climbs up their faces as
   the scroll goes on. Two layers, and their ORDER is the whole trick.

   THE ITEM ITSELF is the extrusion: a dark silhouette plus a stack of
   one-step offset shadows, which is what reads as a solid side rather
   than a drop shadow.

   THE FACE IS ::before, laid over it, carrying the same string in a single
   flat tone. It used to be a background clipped to the glyphs, because a
   colour cannot be half one thing and half another and the face was half
   painted at any moment; the face no longer fills, so a plain colour is
   all it needs.

   THE FACE CANNOT BE THE ITEM'S OWN BACKGROUND. That was the first
   attempt, with the extrusion on a ::before at z-index -1, and it painted
   inside out — an element's own background is the BOTTOM layer of its
   stacking context, so a negative-z-index child lands on top of it and
   the silhouette covered the face completely. A positioned pseudo paints
   after its parent's inline content, so this way round needs no z-index
   and no stacking context at all. */
/* ── ASSEMBLED CHARACTER BY CHARACTER ─────────────────────
   The grammar the receipt's lines used to print in, moved up here. Each
   glyph pops up, scales up and pales to full, staggered along the line, so
   a phrase is typed into place rather than revealed.

   WORDS WRAP THE CHARACTERS, and that layer is not decoration. The glyphs
   have to be inline-block to be transformed at all, and a line can break
   between any two inline-blocks — so with characters alone the answer,
   which is ten words over two lines, would break mid-word wherever the
   measure happened to run out. Breaking is only allowed between words.

   Each glyph carries its own face. The phrase-level ::before below draws
   attr(data-q) as one flat coral block over the whole silhouette, which
   cannot follow characters that move independently; split, every glyph
   gets that same treatment at its own size and the block one is turned
   off. The plum side needs no such handling: text-shadow inherits, so
   every glyph is already extruded by the rule on the phrase. */
.why-q__w { display: inline-block; white-space: nowrap; }
.why-q__ch {
  display: inline-block;
  white-space: pre;
  position: relative;
  opacity: var(--chr, 1);
  /* One property drives all three effects. The easeOutBack overshoot past
     1 is what springs the transform, and on an opacity it simply clamps,
     invisibly — the same trick the receipt used. */
  /* Two drivers, one transform. --chr is the per-character build, written
     by updateWhyRail as the phrase types itself in; --cap is the swell the
     emphasis word's letters make as they stand up into capitals, written
     by updateWhy during the dwell. They multiply, so a letter can be
     arriving and standing up at once and neither clobbers the other. */
  /* THREE DRIVERS, ONE TRANSFORM. --chr is the per-character build as the
     phrase types itself in; --cap is the swell the emphasis word's letters
     make standing up into capitals; --gx/--gy/--gs carry the glyph to the
     printed character it becomes. They compose, so a letter can be
     arriving, standing up, or leaving for the paper without any of the
     three drivers knowing about the others. */
  transform: translate(var(--gx, 0px), var(--gy, 0px))
             translateY(calc((1 - var(--chr, 1)) * 0.85em))
             scale(calc((0.86 + var(--chr, 1) * 0.14) * var(--cap, 1) * var(--gs, 1)));
}
.why-q__ch::before {
  content: attr(data-c);
  position: absolute;
  left: 0; top: 0;
  color: var(--face, #FF7A59);
  -webkit-text-fill-color: var(--face, #FF7A59);
  text-shadow: none;
}
/* THE EMPHASIS WORD, once the phrase has landed and the reader keeps
   going. --why-hi is written by updateWhy() across the dwell after the
   rail has finished; the mix is what makes the change a move rather than
   a swap, since it is scrubbed by the same scroll as everything else.

   THE RATIO WITH THE SIDE IS THE CONSTRAINT, not the hue. This type is
   a face laid over a fourteen-step plum extrusion, and what makes it read
   as a solid object is the distance between the two: coral sits 6.6x the
   plum's luminance. A crimson was tried here first and looked wrong for a
   reason that is measurable rather than a matter of taste — at 3.4x the
   side stopped being a side, so the word lost its depth and went flat
   while every word around it stayed three-dimensional.

   Amber is 7.8x, which is the same relationship coral has, and it is a
   full step up in saturation, 65 to 100. So the word turns up rather than
   going dark, and it keeps its extrusion. It is also the warm end of the
   page's own --fire gradient, and it sits opposite the blue and lilac
   this heading is usually over, which is where it gets its pop.

   Set on the word, inherited by its glyphs, so the emphasis survives the
   per-character build without the driver having to know about it. */
.why-q__w--emph {
  --face: color-mix(in srgb, #2C2C2A calc(var(--exit, 0) * 100%),
          color-mix(in srgb, #FF9500 calc(var(--why-hi, 0) * 100%), #FF7A59));
}
.why-q__item.is-split::before { content: none; }

/* Only the first phrase shows with no script, so the section still reads;
   with it, every phrase's opacity and drift is inline from the driver. */
.why-q__item:first-child { opacity: 1; }
.why-q__item {
  display: block;
  text-align: center;
  position: absolute;
  left: 0; right: 0;
  top: 50%;
  translate: 0 -50%;       /* centred in the slot whatever its own height */
  /* Allowed to wrap. The cap on .why-q's font-size was solved for a
     four-word phrase and cannot fit ten on one line at any width. */
  white-space: normal;
  opacity: 0;
  will-change: opacity, transform;
  /* ROOM FOR ITS OWN EXTRUSION. The shadow stack below reaches 0.14em past
     the glyphs, which is about 14px at this size, and a line box has only
     its half-leading to spare — 4px here. So every line's extrusion hung
     out the bottom of its own box and into the window the NEXT line is
     shown through: parked on "Because we are the best." you could see the
     tail of the "y" from "Why BaiSed?" sitting above the w.
     The rail's SLIP of 4px was the only thing holding it back and it was
     tuned when this type was flat. Raising SLIP is not the fix — it is
     cumulative, so the fourth phrase would lose four times whatever the
     first one did off the top of its caps. Containing the shadow in the
     box that owns it is, and layoutWhyHeadline() measures these boxes to
     size the mask and space the rail, so both follow on their own.
     0.22em, not the 0.14em the shadow itself reaches: the deepest ink on a
     line is a descender's tail and the shadow starts from THERE, not from
     the baseline. 0.15em left a 1px sliver of the y in "Why BaiSed?"
     showing above the w. */
  padding-bottom: 0.22em;
  /* PLUM, not brown. The set used to be a cream face over a warm brown
     side on a peach ground — three warm neutrals within a few points of
     each other, which is why it read as flat rather than as type with a
     colour. The side is the half that seats the letters, so it carries
     the darkest, coolest value in the pairing.
     Held at the silhouette too, not just in the shadow: the ::before face
     covers this exactly, so what this colour actually paints is the last
     row of pixels around the glyph edge. */
  /* THE SIDE AS FIVE TOKENS, so the exit can dissolve it. The extrusion
     is what makes this type an object; on its way into the printer it has
     to stop being one, because what comes out the other side is flat ink
     on paper. --exit mixes every step toward transparent, and the
     declaration below reads them rather than the literals it used to. */
  --s1: color-mix(in srgb, transparent calc(var(--exit, 0) * 100%), #7A1F3D);
  --s2: color-mix(in srgb, transparent calc(var(--exit, 0) * 100%), #6B1A35);
  --s3: color-mix(in srgb, transparent calc(var(--exit, 0) * 100%), #5F1730);
  --s4: color-mix(in srgb, transparent calc(var(--exit, 0) * 100%), #56142B);
  --s5: color-mix(in srgb, transparent calc(var(--exit, 0) * 100%), #4A1226);
  /* The face lands on the receipt's own ink, so the last thing the reader
     sees of the headline is the colour the paper prints in. */
  --face: color-mix(in srgb, #2C2C2A calc(var(--exit, 0) * 100%), #FF7A59);
  color: var(--s1);
  /* In em, so the depth scales with the clamp above instead of being a
     depth tuned for one viewport. Fourteen one-step offsets rather than
     one big one: a single offset shadow is a drop shadow, a stack of them
     with no gaps is a side. Darkening down the stack, so the side reads as
     turning away from the light rather than as a flat slab. */
  text-shadow:
    0.010em 0.010em 0 var(--s1), 0.020em 0.020em 0 var(--s1),
    0.030em 0.030em 0 var(--s1), 0.040em 0.040em 0 var(--s1),
    0.050em 0.050em 0 var(--s2), 0.060em 0.060em 0 var(--s2),
    0.070em 0.070em 0 var(--s2), 0.080em 0.080em 0 var(--s3),
    0.090em 0.090em 0 var(--s3), 0.100em 0.100em 0 var(--s4),
    0.110em 0.110em 0 var(--s4), 0.120em 0.120em 0 var(--s5),
    0.130em 0.130em 0 var(--s5), 0.140em 0.140em 0 var(--s5);
}
.why-q__item::before {
  content: attr(data-q);
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  /* ONE FLAT TONE on the face. This used to carry a waterline: a two-stop
     gradient clipped to the glyphs, with --why-fill written per phrase by
     the scroll loop, so the flood's deepest magenta climbed the letters as
     the section painted itself. The extrusion below is untouched and still
     does the work of keeping the cream legible over a pale ground; only
     the rising colour is gone.

     Both properties, and the same value in each: -webkit-text-fill-color
     wins over color wherever it is supported, so setting one alone leaves
     the other engine painting a different tone.

     CORAL, the brand's own, rather than the cream this carried. Cream was
     six points off the peach ground behind it, so the letters were held
     together almost entirely by their extrusion; this reads as a colour in
     its own right and takes the plum side below as its shadow rather than
     as its outline. */
  color: #FF7A59;
  -webkit-text-fill-color: #FF7A59;
  text-shadow: none;
}
@media (prefers-reduced-motion: reduce) {
  /* Everything after the opening question, stacked and static: the rail
     never runs, so the reader gets the whole built-up claim as one still
     block of text instead of just whichever phrase the scroll happened
     to leave it on. The proof videos stay off — see .why-proof below,
     which is never told to show itself outside the scroll loop this
     media query has just switched off. */
  .why-q__mask { height: auto !important; overflow: visible; }
  .why-q__rail { transform: none; height: auto !important; }
  /* Back into flow: the phrases are absolutely stacked for the driver,
     and with the driver off they would all sit on one spot at opacity 0.
     !important because the driver's last inline values may be on them. */
  .why-q__item {
    position: relative; top: auto; translate: none;
    opacity: 1 !important; transform: none !important;
  }
  .why-q__item:first-child { display: none; }
}

/* THE PROOF PANEL — the last two phrases on the rail ("We cover
   physical.", "We cover online too.") each get a screen recording below
   them, the same two clips the channel-fit cards use further down the
   page: a paper receipt scanned, a marketplace order confirmed. Reusing
   the footage rather than shooting new proof for a claim the page
   already backs up elsewhere.

   Centred on the frame's own centre, the SAME point .why-q is centred on,
   so the phone and the words occupy one another's space rather than
   dividing it. Out of flow to do it: a flex sibling would push the
   heading off centre by exactly its own share of the column, which is
   the one thing this arrangement cannot have. The heading carries the
   higher z-index (see .why-q) and reads over the top of the card.

   Hidden below 1360px, NOT 861px where the road itself disappears —
   found by testing, not assumed. Between 861 and 1359px .why-q is pushed
   text-align:right and widened to max-width:56% specifically to dodge the
   pour (see the media query above); a stacked panel would inherit that
   same right-pushed column and sit lopsided under right-aligned text, so
   it stays off through that whole band too. 1360px is where .why-q goes
   back to being centred with room on both sides — the same number the
   pour-avoidance comment above calls out — so it is also where a centred
   column under it reads right. lucky-draw-scenes.js gates its own
   play/pause behind the same number (WHY_PROOF_MQ), so a video hidden
   here is never quietly decoding somewhere off screen either.

   Width capped by viewport HEIGHT, not a flat px, because the clip is a
   phone: 572:1180 means every px of width costs two of height, so height
   is what runs out first. calc((100vh - 146px) * 0.485) inverts that
   ratio to solve for the width whose derived height still leaves 146px
   behind — the 73px sticky nav twice over, since the card is centred and
   whatever it leaves over is split evenly top and bottom. So the top
   margin is always the bar's own height exactly, and the card grows into
   whatever the viewport actually has rather than clipping out of it: it
   lands around 420px on this machine's own 16" screen against the 280
   the panel used beside the text, and steps down on a short window
   instead of overflowing. Overlapping the heading rather than stacking
   under it is what buys the extra size — nothing else is competing for
   the same vertical run any more. */
.why-proof {
  display: none;
  position: absolute;
  left: 50%;
  top: 50%;
  translate: -50% -50%;
  /* SIZED BY HEIGHT, and that is the fix. It used to be a width capped
     by viewport height — min(420px, (100vh - 146px) * 0.485) — with the
     height falling out of the 572:1180 clip. That reads fine on a tall
     window and badly on a short one: at 900px it solved to a 754px
     panel, 84% of the viewport, which is a phone standing in the frame
     floor to ceiling. Asking for the height directly caps it wherever
     the window happens to be.

     72vh and 840px, up a fifth from the 60vh/700px this first settled on:
     the panel is the proof the section is built to show and it was
     reading as an inset rather than as the subject. It is still well
     short of the 84% that looked like a phone wedged floor to ceiling,
     and it does not need a matching guard on the heading, because
     measureWhySplit() derives --why-q-scale from the room actually left
     between the heading's centre and the panel's left edge: a wider
     panel shrinks the words on its own. */
  height: min(840px, 72vh);
  width: auto;
  aspect-ratio: 572 / 1180;
  z-index: 1;
  pointer-events: none;
}
/* The other half of the same move. .why-proof centres itself with the
   individual `translate` property, which applies before `transform`, so
   this rides on top of the centring instead of fighting it. */
/* IT COMES IN FROM OFF THE RIGHT. Two terms: its resting place at the
   right gutter, and an entrance that carries it 130% of its own width
   further right when the split has not happened yet. Both ride on
   --why-split, so as the heading walks left the phone walks left too,
   from off screen into place.

   It used to be the first term alone, which meant the panel faded up in
   the CENTRE of the frame — on top of the heading — and then slid
   right. Two things arriving in the same place and then separating
   reads as a mistake being corrected; one arriving from outside reads
   as a move. */
.why-proof {
  transform: translateX(calc(var(--why-p-shift, 0px)
                             + (1 - var(--why-split, 0)) * 130%));
}
/* PARKED, so the base display:none above stands at every width. The gate
   that used to reveal it was @media (min-width: 1360px), and that number
   is measured rather than picked — see the note on .why-proof. Restoring
   this rule alone is not enough: WHY_PROOF_MQ in lucky-draw-scenes.js
   holds the other half, and it is what keeps a hidden video from being
   told to play. */
/* Both items occupy the same spot and are told apart by which one is
   opaque — see poseProof() in lucky-draw-scenes.js, which writes opacity
   and transform here every scroll frame. Starting state is invisible and
   dropped a few px, so a reader who never triggers a repaint (no script,
   reduced motion) sees neither: the rail's own static text already
   carries the claim on its own in that case.

   top:50% + translate, same trick .why-proof itself is built on: two items
   need to sit on the exact same spot to cross-fade, and centring each one
   on its own anchor point keeps that spot stable regardless of which item
   ends up taller. The standalone `translate` property is what makes this
   safe to combine with poseProof()'s inline `transform` in JS — the
   fade's translateY/scale in the transform layers on top of this
   positioning translate instead of overwriting it. */
/* A real white card behind the phone, not just a matte inside it — the
   phone used to sit directly on the section's own diagonal colour with
   nothing of its own underneath it, which read as a mockup dropped onto
   the page rather than a piece of proof presented on it. border-box (the
   page's own global reset) keeps this at exactly .why-proof's own width,
   whatever that resolves to, with the padding taken out of the phone's
   own room rather than added on top of it, so .channel__phone —
   width:100% of ITS parent — settles at that width minus the padding on
   both sides instead of overflowing it. */
/* NO CARD. The phone used to sit on a white squircle with 22px of
   padding and a tinted drop shadow, which made the panel read as a
   product shot pinned to the page rather than as a phone standing in
   it. The video already carries its own bezel, so the card was a second
   frame around a frame. What is left here is position and the fade the
   driver writes; everything that drew a panel is gone. */
.why-proof__item {
  position: absolute;
  top: 50%;
  left: 0;
  width: 100%;
  translate: 0 -50%;
  opacity: 0;
}
/* A white backing, not the page's own butter: object-fit:contain can
   letterbox if the video's own frame ever falls slightly short of the
   572/1180 box, and preload="metadata" means the first paint can lag
   behind poseProof() making the panel visible — either way, what would
   otherwise show through is the section's amber ground, not the clean
   white chrome the recording itself is shot on. */
/* background stays: it is what the video's own rounded corners reveal
   underneath while it decodes. The radius drops to the phone's own so
   there is no second, larger curve behind it. */
.why-proof__item .channel__phone { margin-top: 0; max-width: none; background: #fff; border-radius: 28px; }
.why-proof__video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
  /* FULL STRENGTH. This carried a 0.62 white scrim for one reason: the
     heading used to cross the middle of the card, and #6E4A12 on the
     coral screen measured 1.86:1 at the worst frame, so the words were
     unreadable over it. The split moved the heading off that band — it
     walks fully left while the panel comes in from the right, and the
     two never meet. Measured at every width the panel is shown at, the
     heading's right edge to the panel's left edge: 161px of clear space
     at 1360, 275px at 1512, 183px at 1440, at every point in the move.
     Nothing to protect any more, so nothing is held back. */
  opacity: 1;
}
.card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 16px;
  padding: 30px 26px;
  display: flex;
  flex-direction: column;
  transition: border-color 0.25s, box-shadow 0.25s, transform 0.25s;
}
.card:hover { border-color: rgba(255,107,107,0.32); box-shadow: 0 10px 34px rgba(255,107,107,0.10); }
.card-ico { font-size: 1.5rem; margin-bottom: 14px; }
.card h3 { font-size: 1.2rem; margin-bottom: 10px; }
.card p { font-size: 0.88rem; color: var(--muted); font-weight: 300; flex-grow: 1; }
.card-dates {
  font-size: 0.74rem;
  color: var(--faint);
  margin-bottom: 12px;
  letter-spacing: 0.01em;
}
.card-dates time { font-variant-numeric: tabular-nums; }

.card-link {
  margin-top: 16px;
  font-size: 0.74rem;
  font-weight: 700;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--red);
  text-decoration: none;
  transition: opacity 0.2s;
}
.card-link:hover { opacity: 0.7; }

/* ── FAQ ────────────────────────────────────────────────── */
.faq { max-width: 820px; }
.faq details {
  border-bottom: 1px solid var(--line);
  padding: 4px 0;
}
.faq summary {
  cursor: pointer;
  list-style: none;
  padding: 20px 40px 20px 0;
  position: relative;
  font-family: var(--serif);
  font-size: clamp(1rem, 1.6vw, 1.15rem);
  font-weight: 500;
}
.faq summary { font-weight: 600; }
.faq summary::-webkit-details-marker { display: none; }
.faq summary::after {
  content: '+';
  position: absolute;
  right: 6px; top: 50%;
  transform: translateY(-50%);
  font-size: 1.4rem;
  font-weight: 300;
  color: var(--red);
  transition: transform 0.25s;
}
.faq details[open] summary::after { transform: translateY(-50%) rotate(45deg); }
.faq details p {
  padding: 0 40px 22px 0;
  color: var(--muted);
  font-size: 0.92rem;
  font-weight: 300;
}

/* ── CTA ────────────────────────────────────────────────── */
#cta {
  text-align: center;
  background: linear-gradient(160deg, #fff8f0 0%, #FFF3E8 45%, #FFF9EC 100%);
  position: relative;
  overflow: hidden;
}
#cta::before {
  content: '';
  position: absolute;
  bottom: -220px; left: 50%;
  transform: translateX(-50%);
  width: 900px; height: 560px;
  background: radial-gradient(ellipse, rgba(255,107,107,0.10) 0%, transparent 70%);
  pointer-events: none;
}
.cta-inner { position: relative; z-index: 1; max-width: 640px; margin: 0 auto; }
#cta h2 { font-size: clamp(1.9rem, 4.4vw, 3rem); margin-bottom: 16px; }
#cta > .cta-inner > p { color: var(--muted); font-weight: 300; margin-bottom: 30px; }
.badges { display: flex; gap: 14px; justify-content: center; flex-wrap: wrap; margin-top: 26px; }
.badges img { width: 150px; height: 48px; object-fit: contain; }
.badges a { opacity: 0.62; transition: opacity 0.2s, transform 0.2s; }
.badges a:hover { opacity: 0.9; transform: translateY(-2px); }
.fineprint { font-size: 0.74rem; color: var(--faint); margin-top: 30px; line-height: 1.8; }
.fineprint a { color: var(--muted); }

/* ── FOOTER ─────────────────────────────────────────────── */
/* NAVY, and the reason is that a footer is the least important block on the
   page while the coral it carried was the loudest colour on it. The page
   ends on a pale peach and a white talent block, so a mid-tone coral read as
   a wall rather than as a close.

   A DARK ground is what makes the hierarchy work again. The coral was a
   mid-tone, and that is the awkward case: white measures 2.8 on it and fails
   outright, so it had to keep dark ink, and dark ink only reached 6.4 there.
   Every transparent role then had to climb to about 0.82 just to stay
   legible, which left the links, the tagline and the copyright all close to
   solid, separating by size and weight rather than by weight of colour.
   Against this ground the solid tier measures 15.3 and 0.70 still measures
   8.0, so the two tiers are far apart and both clear AA with room.

   Measured on #1C2436: #EEF1F8 solid 13.7, at 0.70 7.5, the hairline at 0.16
   is decorative only. The gift in the wordmark is 4.2 and keeps its own
   colour.

   Written as literals because the two pages that carry their own copy of
   this footer have no access to the tokens. Those copies are still coral:
   see public/index.html and public/business.html. */
.app-footer {
  background: #1C2436;
  color: #EEF1F8;
  padding: 64px 6vw 32px;
  /* THE OVERSCROLL STRIP, and it is a box-shadow for a reason. Rubber-
     banding past the end of the page lifts the document off the bottom of
     the window, and the fixed layers behind it do not come with it, so what
     showed in the gap was .night-sky: the star field, several shades darker
     than this footer and quite clearly a different surface.

     A shadow is the one way to paint down there that costs nothing. It is
     part of the footer's own paint, so it travels with the document and is
     revealed exactly as the footer lifts away; it sits above the sky's
     negative z-index; and shadows are ignored when scrollable overflow is
     worked out, so unlike a positioned block it cannot make the page one
     pixel taller or give body's overflow-x: hidden something to scroll.

     The offset and the spread are EQUAL on purpose. Spread inflates the
     shadow on every side, so an equal pair puts its top edge exactly on the
     footer's own and leaves no seam between the two; the part that would
     land under the footer is knocked out, as it is for any outer shadow.
     200vh of it below, which no rubber band reaches. */
  box-shadow: 0 100vh 0 100vh #1C2436;
}
.footer-top {
  display: flex;
  justify-content: space-between;
  gap: 40px;
  flex-wrap: wrap;
  border-bottom: 1px solid rgba(238, 241, 248, 0.16);
  padding-bottom: 40px;
  margin-bottom: 28px;
}
.footer-app { display: flex; flex-direction: column; gap: 18px; max-width: 360px; }
.qr-box { display: flex; align-items: center; gap: 16px; }
/* No edge on the plate. It needed one on the coral, where a white square on
   a light mid-tone had nothing to separate it from the ground. Back on a
   dark ground white draws its own. */
.qr-box img { width: 88px; height: 88px; background: #fff; border-radius: 10px; padding: 7px; }
.qr-box h2 { font-size: 1.1rem; font-weight: 700; color: #EEF1F8; margin-bottom: 4px; }
.qr-box p { font-size: 0.86rem; color: rgba(238, 241, 248, 0.70); }
.footer-badges { display: flex; gap: 6px; flex-wrap: wrap; }
.footer-badges img { width: 132px; height: 42px; object-fit: contain; }
.footer-links { display: flex; gap: 64px; flex-wrap: wrap; }
.link-col { display: flex; flex-direction: column; gap: 12px; }
.link-col h2 {
  font-size: 0.9rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  color: #EEF1F8;
  margin-bottom: 4px;
}
.link-col a { color: rgba(238, 241, 248, 0.70); text-decoration: none; font-size: 0.86rem; transition: color 0.2s; }
.link-col a:hover { color: #EEF1F8; }
.footer-bottom {
  display: flex;
  justify-content: space-between;
  align-items: center;
  gap: 20px;
  flex-wrap: wrap;
}
.footer-bottom p { font-size: 0.76rem; color: rgba(238, 241, 248, 0.70); }
.f-logo { display: flex; align-items: center; gap: 8px; font-family: var(--serif); font-size: 1.3rem; font-weight: 700; }
.f-logo img { height: 40px; width: auto; }
/* The lockup again, scoped for the same reason as .nav-logo--lockup: the
   other two pages on this stylesheet still set their wordmark as text.
   34 puts it at 117 wide, which is what the old icon-plus-serif pair
   measured. gap is left alone above and simply has nothing to space. */
.f-logo--lockup img { height: 34px; }

/* ── REVEAL (subtle, never blocks content) ──────────────────
   Gated on the .js class so that without JavaScript — or if the
   script fails — every card is simply visible. Content must never
   depend on a script to be readable. */
.js .rise { opacity: 0; transform: translateY(16px); transition: opacity 0.55s ease, transform 0.55s ease; }
.js .rise.in { opacity: 1; transform: none; }

/* ── RESPONSIVE ─────────────────────────────────────────── */
@media (max-width: 980px) {
  .steps { grid-template-columns: 1fr 1fr; }
  .cards { grid-template-columns: 1fr; }
  .footer-links { gap: 40px; }
}
@media (max-width: 760px) {
  .band { padding: 60px 5vw; }
  .band--tight { padding: 48px 5vw; }
  /* Follows the band's own padding, or the ground stops short of the edge
     sideways and pins a few pixels late at the top. */
  .rainbowband__ground--tail { --tail-inset: 5vw; --tail-lift: 48px; }
  #hero { padding: 52px 5vw 52px; }
  .nav-logo { font-size: 23px; }
  .nav-logo img { height: 40px; }
  /* Same arithmetic at the small size: the old pair is 40 of icon, 8 of
     gap and 74 of 23px type, so 122 wide, and 36 of lockup is 123. */
  .nav-logo--lockup img { height: 36px; }
  #nav { padding: 8px 5vw; }
  .proof { gap: 9px; }
  .proof div { padding: 13px 8px; border-radius: 12px; }
  .proof span { font-size: 0.68rem; }
  .hero-cta .btn { width: 100%; max-width: 320px; }
  .sec-head { margin-bottom: 30px; }
  .badges img { width: 132px; height: 42px; }
  .footer-bottom { justify-content: center; text-align: center; }
}
@media (max-width: 460px) {
  .steps { grid-template-columns: 1fr; }
  .lang-switch a { padding: 6px 10px; font-size: 0.68rem; }
  .btn--sm { padding: 8px 14px; font-size: 0.74rem; }
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
  .rise { opacity: 1; transform: none; transition: none; }
  /* The tick is gated on .settled, which a reduced-motion reader never
     gets: the scroll engine does not run at all under reduced motion. */
  .js .fit-card__yes::before { opacity: 1; transform: none; transition: none; }
}

/* ══════════════════════════════════════════════════════════
   B2B ADDITIONS — end-to-end chain, owner tags, contact CTA
   ══════════════════════════════════════════════════════════ */

/* The "ครบจบที่เดียว" chain: numbered, connected, one owner per step */
.chain { display: flex; flex-direction: column; }
.chain-row {
  display: grid;
  grid-template-columns: 92px 1fr auto;
  gap: 26px;
  align-items: start;
  padding: 26px 0;
  border-top: 1px solid var(--line);
  position: relative;
}
.chain-row:last-child { border-bottom: 1px solid var(--line); }
.chain-n {
  font-family: var(--serif);
  font-size: clamp(2rem, 4vw, 3.1rem);
  font-weight: 800;
  line-height: 1;
  background: var(--fire);
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  background-clip: text;
  font-variant-numeric: tabular-nums;
}
.chain-body h3 { font-size: clamp(1.05rem, 1.9vw, 1.35rem); font-weight: 700; margin-bottom: 7px; }
.chain-body p { font-size: 0.9rem; color: var(--muted); font-weight: 300; max-width: 620px; }
.owner {
  align-self: center;
  white-space: nowrap;
  font-size: 0.68rem;
  font-weight: 700;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  padding: 7px 15px;
  border-radius: 100px;
  border: 1px solid transparent;
}
html[lang="th"] .owner { letter-spacing: 0.04em; text-transform: none; font-size: 0.74rem; }
.owner--us { background: var(--fire); color: #fff; box-shadow: 0 4px 14px rgba(255,107,107,0.24); }
.owner--you { background: transparent; border-color: var(--line); color: var(--faint); }

/* Compare cards used without a big number */


/* Contact block */
.contact-row { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; }

@media (max-width: 760px) {
  .chain-row { grid-template-columns: 56px 1fr; gap: 16px; padding: 22px 0; }
  .chain-n { font-size: 1.7rem; }
  .owner { grid-column: 2; justify-self: start; margin-top: 10px; align-self: start; }
}

/* ══════════════════════════════════════════════════════════
   B2B SERVICE PAGES (/th/*)
   Transactional layout: answer, scope, process, cost, contact.
   ══════════════════════════════════════════════════════════ */
.b2b-hero { padding: 64px 6vw 56px; position: relative; overflow: hidden; }
.b2b-hero::before {
  content: '';
  position: absolute; top: -220px; left: 50%;
  transform: translateX(-50%);
  width: 1100px; height: 620px; max-width: 150vw;
  background: radial-gradient(ellipse, rgba(255,189,113,0.20) 0%, rgba(255,107,107,0.06) 48%, transparent 72%);
  pointer-events: none;
}
.b2b-hero .shell { position: relative; z-index: 1; }
.b2b-hero h1 { font-size: clamp(1.75rem, 4.2vw, 3rem); margin-bottom: 18px; max-width: 900px; }
.b2b-lede { font-size: clamp(0.98rem, 1.4vw, 1.12rem); color: var(--muted); font-weight: 300; max-width: 720px; margin-bottom: 28px; }

/* Contact bar — repeated above and below the fold */
.contact-bar { display: flex; gap: 12px; flex-wrap: wrap; align-items: center; }
.contact-bar .btn { margin: 0; }
.contact-note { font-size: 0.8rem; color: var(--faint); width: 100%; margin-top: 6px; }

/* Scope: what's included, as a list not prose */
.scope-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px 26px; list-style: none; }
.scope-grid li {
  display: flex; gap: 10px; align-items: flex-start;
  font-size: 0.92rem; color: var(--muted);
  padding: 9px 0; border-bottom: 1px solid var(--line);
}
.scope-grid li::before { content: '✓'; color: var(--red); font-weight: 700; flex-shrink: 0; }

/* Numbered process */
.proc { list-style: none; counter-reset: proc; display: grid; gap: 12px; }
.proc li {
  counter-increment: proc;
  position: relative;
  padding: 18px 22px 18px 62px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 14px;
}
.proc li::before {
  content: counter(proc, decimal-leading-zero);
  position: absolute; left: 22px; top: 19px;
  font-family: var(--serif); font-weight: 800; font-size: 0.82rem;
  color: var(--orange); letter-spacing: 0.06em;
}
.proc b { display: block; font-weight: 700; margin-bottom: 3px; font-size: 1rem; }
.proc span { font-size: 0.88rem; color: var(--muted); font-weight: 300; }

/* Cost drivers table */
/* ── THE FEE STRIP ───────────────────────────────────────
   Five tiles, one per tier, the fee large and the length of campaign it
   buys underneath. No bars.

   It was a staircase before this, with the step heights on a square-root
   scale so the ฿300 step stayed readable against the ฿9,000 one. That was
   documented and deliberate, and it was still the wrong trade: a bar chart
   reads as linear whether or not a comment says otherwise, so the first
   step, drawn at 18% of the last, told the eye ฿1,600. A tile claims no
   proportion at all, so nothing here can be misread. The climb in cost
   is carried by the numbers and by the last tile taking the page's fire
   gradient as the top of the range.

   Below phone width the five tiles become five rows, fee on the right,
   which is how a price list is read one-handed. */
.fee-strip {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(5, minmax(0, 1fr));
  gap: 12px;
}
.fee-strip li {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: 6px;
  min-width: 0;
  padding: 22px 12px 20px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: #fff;
  text-align: center;
}
.fee-strip b {
  font-size: 1.45rem;
  font-weight: 800;
  letter-spacing: -0.02em;
  line-height: 1.1;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  white-space: nowrap;
}
.fee-strip span {
  font-size: 0.78rem;
  line-height: 1.3;
  color: var(--muted);
}
.fee-strip li:last-child {
  background-image: var(--fire);
  border-color: transparent;
}
.fee-strip li:last-child b,
.fee-strip li:last-child span { color: #fff; }
@media (max-width: 700px) {
  .fee-strip { grid-template-columns: 1fr; gap: 8px; }
  .fee-strip li {
    flex-direction: row;
    justify-content: space-between;
    align-items: baseline;
    padding: 13px 16px;
    text-align: left;
  }
  .fee-strip b { order: 2; font-size: 1.15rem; }
  .fee-strip span { order: 1; font-size: 0.86rem; }
}
.table-wrap { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* Callout for legal/regulatory notes */
.note {
  border-left: 3px solid var(--orange);
  background: rgba(255,189,113,0.10);
  padding: 16px 20px; border-radius: 0 12px 12px 0;
  font-size: 0.89rem; color: rgba(26,26,46,0.8); font-weight: 300;
}
.note b { font-weight: 700; }

/* Related reading */
.support-links { display: flex; flex-wrap: wrap; gap: 10px; list-style: none; }
.support-links a {
  display: inline-block; text-decoration: none;
  border: 1px solid var(--line); border-radius: 100px;
  padding: 9px 18px; font-size: 0.84rem; color: var(--muted);
  transition: border-color 0.2s, color 0.2s, background 0.2s;
}
.support-links a:hover { border-color: rgba(255,107,107,0.4); color: var(--ink); background: #fff; }

.breadcrumb { font-size: 0.78rem; color: var(--faint); margin-bottom: 18px; }
.breadcrumb a { text-decoration: none; }
.breadcrumb a:hover { text-decoration: underline; }

@media (max-width: 760px) {
  .b2b-hero { padding: 40px 5vw 40px; }
  .scope-grid { grid-template-columns: 1fr; gap: 0 0; }
  .contact-bar .btn { width: 100%; max-width: 340px; }
  .proc li { padding: 16px 16px 16px 54px; }
  .proc li::before { left: 16px; }
}

/* Thai line breaking: browsers will split mid-word (e.g. ยังไง -> ยัง / ไง).
   keep-all confines breaks to the spaces, which in Thai copy sit at phrase
   boundaries. break-word stays as the fallback for a run too long to fit. */
html[lang="th"] h1,
html[lang="th"] h2,
html[lang="th"] h3,
html[lang="th"] .hero-lede,
html[lang="th"] .proof b,
html[lang="th"] .faq summary {
  word-break: keep-all;
  overflow-wrap: break-word;
}

/* ── FAQ GROUP HEADINGS ─────────────────────────────────── */
.faq-group {
  font-size: 0.82rem;
  font-weight: 600;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--faint);
  margin: 34px 0 12px;
}
.faq-group:first-of-type { margin-top: 0; }

/* ── LEAD FORM ──────────────────────────────────────────── */
.lead-form {
  max-width: 760px;
  margin: 0 auto;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 30px;
  box-shadow: 0 18px 44px rgba(26,26,46,0.06);
}
.lead-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 16px;
}
.field { display: flex; flex-direction: column; gap: 6px; min-width: 0; }
.field--full { grid-column: 1 / -1; }
.field label {
  font-size: 0.82rem;
  font-weight: 500;
  color: var(--muted);
}
.field label span { color: var(--red); }
.field input,
.field select,
.field textarea {
  font-family: inherit;
  font-size: 0.94rem;
  color: var(--ink);
  background: var(--bg);
  border: 1px solid var(--line);
  border-radius: 12px;
  padding: 12px 14px;
  width: 100%;
  transition: border-color 0.2s, background 0.2s;
}
.field textarea { resize: vertical; min-height: 108px; line-height: 1.6; }
.field input:focus,
.field select:focus,
.field textarea:focus {
  outline: none;
  border-color: var(--red);
  background: var(--card);
}
.field input[aria-invalid="true"],
.field textarea[aria-invalid="true"] { border-color: var(--red); }

/* Honeypot. Kept in the DOM and reachable by nothing a human uses. */
.hp {
  position: absolute;
  width: 1px; height: 1px;
  overflow: hidden;
  clip: rect(0 0 0 0);
  white-space: nowrap;
}

.lead-actions {
  display: flex;
  flex-wrap: wrap;
  gap: 12px;
  align-items: center;
  margin-top: 22px;
}
.lead-actions .btn { border: none; cursor: pointer; font-family: inherit; }
.lead-actions .btn--ghost { border: 1px solid rgba(26,26,46,0.18); }
.lead-actions .btn[disabled] { opacity: 0.55; cursor: default; transform: none; }

.form-status {
  margin-top: 14px;
  font-size: 0.88rem;
  min-height: 1.2em;
}
.form-status.is-error { color: #c92a2a; }
.form-status.is-ok { color: #1f7a4d; font-weight: 500; }

.form-note {
  margin-top: 14px;
  font-size: 0.8rem;
  color: var(--faint);
  line-height: 1.7;
}
.form-note a { color: var(--muted); }

@media (max-width: 640px) {
  .lead-form { padding: 22px 18px; border-radius: 16px; }
  .lead-grid { grid-template-columns: 1fr; }
  /* 16px, not the 15px the desktop size resolves to. iOS Safari zooms the
     whole page in when a field under 16px takes focus, and does not zoom
     back out when it loses it, so the form left the reader stranded at
     110% with the bar half off screen. */
  .field input, .field select, .field textarea { font-size: 16px; }
}

/* ══════════════════════════════════════════════════════════
   IMAGE SLOTS
   Every slot reserves its final space with aspect-ratio, so
   dropping the real file in never shifts the layout. While a
   slot is empty it renders its own spec label.

   To fill one, replace the <span> inside with:
     <img src="/assets/lucky-draw/NAME.png" alt="..."
          width="W" height="H" loading="lazy">
   ══════════════════════════════════════════════════════════ */
.img-slot {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  overflow: hidden;
  border-radius: 18px;
  background:
    linear-gradient(135deg, rgba(255,107,107,0.05), rgba(255,189,113,0.07));
  border: 1.5px dashed rgba(26,26,46,0.16);
  color: var(--faint);
  text-align: center;
}
.img-slot > img {
  width: 100%;
  height: 100%;
  object-fit: cover;
  border-radius: inherit;
}
.img-slot:has(> img) { border: none; background: none; }

.img-spec {
  display: block;
  padding: 14px;
  font-size: 0.72rem;
  line-height: 1.7;
  letter-spacing: 0.01em;
  /* The destination paths are long unbreakable tokens; without this they
     push the label wider than a narrow slot and get clipped. */
  overflow-wrap: anywhere;
}
.img-spec b {
  display: block;
  font-size: 0.78rem;
  font-weight: 600;
  color: var(--muted);
  margin-bottom: 2px;
}
.img-spec code {
  font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
  font-size: 0.68rem;
  color: var(--red);
  background: rgba(255,107,107,0.08);
  border-radius: 5px;
  padding: 1px 5px;
}

/* Ratios. The number in each name is what the slot expects. */
.img-slot--phone   { aspect-ratio: 1 / 2; max-width: 460px; margin-inline: auto; border-radius: 32px; }
.img-slot--wide    { aspect-ratio: 16 / 9; }
/* The campaign banners the work deck shows. An empty slot has no image to
   give it a height, so without this the placeholder collapses to the height
   of its own label and the card it stands in for changes size when the art
   finally arrives. */
.img-slot--banner  { aspect-ratio: 1131 / 595; }
.img-slot--screen  { aspect-ratio: 8 / 5; }
.img-slot--doc     { aspect-ratio: 3 / 4; }
.img-slot--portrait{ aspect-ratio: 4 / 5; }
.img-slot--square  { aspect-ratio: 1 / 1; }
.img-slot--icon    { aspect-ratio: 1 / 1; width: 72px; border-radius: 16px; }
.img-slot--icon .img-spec { padding: 4px; font-size: 0.58rem; }
.img-slot--icon .img-spec b { font-size: 0.58rem; margin: 0; }
.img-slot--step    { aspect-ratio: 1 / 1; width: 48px; border-radius: 12px; flex: 0 0 48px; }
.img-slot--step .img-spec { padding: 2px; font-size: 0.5rem; }
.img-slot--step .img-spec b { display: none; }
.img-slot--avatar  { aspect-ratio: 1 / 1; width: 88px; border-radius: 100px; flex: 0 0 88px; }
.img-slot--avatar .img-spec { padding: 4px; font-size: 0.54rem; }
.img-slot--avatar .img-spec b { display: none; }
.img-slot--logo    { aspect-ratio: 75 / 28; border-radius: 12px; }
.img-slot--logo .img-spec { padding: 6px; font-size: 0.58rem; }
.img-slot--logo .img-spec b { font-size: 0.6rem; margin: 0; }

/* ── SPLIT SECTIONS (text one side, visual the other) ────── */
.split {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
  gap: 54px;
  align-items: center;
}
.split--narrow-visual { grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr); }
.split__visual { min-width: 0; }
.split__text { min-width: 0; }
.split__text .sec-head { text-align: left; margin-bottom: 26px; }
.split--flip .split__visual { order: -1; }

/* ── HERO SPLIT ─────────────────────────────────────────── */
#hero.hero--split { text-align: left; padding-bottom: 40px; }
#hero.hero--split .hero-inner { max-width: var(--shell); }
#hero.hero--split .hero-cta { justify-content: flex-start; }
#hero.hero--split .proof { margin-inline: 0; }

/* ── LOGO WALL ──────────────────────────────────────────── */
.logo-wall {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
  gap: 14px;
  align-items: center;
}
.logo-wall__note {
  text-align: center;
  font-size: 0.78rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
  margin-bottom: 22px;
}

/* ── CASE CARDS (campaign proof with a key visual) ───────── */
.case-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(290px, 1fr));
  gap: 22px;
}
.case {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 20px;
  overflow: hidden;
  display: flex;
  flex-direction: column;
}
.case__visual { border-radius: 0; border-left: none; border-right: none; border-top: none; }
.case__body { padding: 22px; display: flex; flex-direction: column; gap: 10px; flex: 1; }
.case__head { display: flex; align-items: center; gap: 12px; }
.case__head h3 { font-size: 1.06rem; margin: 0; }
.case__logo { width: 46px; flex: 0 0 46px; border-radius: 10px; aspect-ratio: 1 / 1; }
.case__logo .img-spec { padding: 2px; font-size: 0.48rem; }
.case__logo .img-spec b { display: none; }
.case__metrics {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 8px;
  margin-top: 4px;
}
.case__metrics div {
  background: var(--bg);
  border-radius: 12px;
  padding: 10px 8px;
  text-align: center;
}
.case__metrics b { display: block; font-size: 1.02rem; color: var(--ink); line-height: 1.3; }
.case__metrics span { font-size: 0.64rem; color: var(--faint); line-height: 1.4; }
.case .card-link { margin-top: auto; }

/* A number that still needs confirming before it goes live. */
.todo-num {
  display: inline-block;
  min-width: 44px;
  border-bottom: 1.5px dashed rgba(255,107,107,0.5);
  color: rgba(255,107,107,0.75);
  font-size: 0.9rem;
}

/* ── TESTIMONIAL ────────────────────────────────────────── */
.quote {
  max-width: 820px;
  margin-inline: auto;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 22px;
  padding: 34px;
  display: flex;
  gap: 22px;
  align-items: flex-start;
}
.quote blockquote { font-size: 1.12rem; line-height: 1.75; margin: 0 0 14px; }
.quote figcaption { font-size: 0.84rem; color: var(--muted); }
.quote figcaption b { display: block; color: var(--ink); font-weight: 600; }

/* ── MID-PAGE CTA STRIP ─────────────────────────────────── */
.cta-strip {
  display: flex;
  flex-wrap: wrap;
  gap: 18px;
  align-items: center;
  justify-content: space-between;
  background: var(--ink);
  color: #fff;
  border-radius: 22px;
  padding: 28px 34px;
}
.cta-strip h2 { color: #fff; font-size: 1.32rem; margin: 0 0 4px; }
.cta-strip p { color: rgba(255,255,255,0.62); font-size: 0.9rem; margin: 0; }

/* ── LEAD SECTION SPLIT ─────────────────────────────────── */
.lead-split {
  display: grid;
  grid-template-columns: minmax(0, 1.25fr) minmax(0, 0.75fr);
  gap: 40px;
  align-items: start;
}
.lead-split .lead-form { max-width: none; margin: 0; }
.lead-aside { display: flex; flex-direction: column; gap: 18px; }

/* ── STICKY MOBILE CTA ──────────────────────────────────── */
.sticky-cta { display: none; }

@media (max-width: 900px) {
  .split, .lead-split { grid-template-columns: 1fr; gap: 32px; }
  .split--flip .split__visual { order: 0; }
  #hero.hero--split { text-align: center; }
  #hero.hero--split .hero-cta { justify-content: center; }
  .img-slot--phone { max-width: 320px; }
  .quote { flex-direction: column; padding: 26px; }
}

@media (max-width: 640px) {
  .sticky-cta {
    display: flex;
    position: fixed;
    left: 12px; right: 12px; bottom: 12px;
    z-index: 60;
    gap: 10px;
    padding: 10px;
    border-radius: 100px;
    background: rgba(255,255,255,0.92);
    backdrop-filter: blur(14px);
    box-shadow: 0 10px 34px rgba(26,26,46,0.18);
  }
  .sticky-cta .btn { flex: 1; padding: 12px 16px; font-size: 0.84rem; }
  body { padding-bottom: 76px; }
  .cta-strip { padding: 24px; }
  .case__metrics { grid-template-columns: 1fr; }
}

/* Six nav links plus the CTA overflow between the tablet breakpoints,
   so tighten the pills there instead of hiding the anchors. */
@media (max-width: 1060px) {
  .nav-links { gap: 2px; }
  .nav-links a { padding: 8px 9px; font-size: 0.78rem; }
}

/* Once a split stacks, its visual would otherwise stretch to the full
   shell — a 3:4 document at 790px wide is a wall. Cap and centre it,
   which also keeps the source files the designer has to supply sane. */
@media (max-width: 900px) {
  /* width:100% is required — an auto inline margin on a grid item
     drops it out of stretch sizing and it collapses to fit-content. */
  .split__visual { width: 100%; max-width: 560px; margin-inline: auto; }
  .split__visual .img-slot--doc,
  .split__visual .img-slot--portrait { max-width: 420px; margin-inline: auto; }
  .lead-aside { max-width: 420px; margin-inline: auto; width: 100%; }
}

/* Logos must never be cropped — cover was clipping the wordmarks. */
.img-slot--logo > img,
.case__logo > img {
  object-fit: contain;
  padding: 6px 10px;
  background: var(--card);
}
.img-slot--logo:has(> img) { background: var(--card); border: 1px solid var(--line); }
.case__logo:has(> img) { background: var(--card); border: 1px solid var(--line); }
.case__logo > img { padding: 5px; }

/* ── VENDORS THE BRAND NO LONGER HIRES ──────────────────── */
.vendors {
  display: flex;
  flex-wrap: wrap;
  gap: 10px;
  justify-content: center;
}
.vendors span {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 100px;
  padding: 10px 18px;
  font-size: 0.86rem;
  color: var(--faint);
  text-decoration: line-through;
  text-decoration-color: rgba(255,107,107,0.6);
  text-decoration-thickness: 1.5px;
}
.vendors span::before {
  content: '✕';
  color: var(--red);
  font-size: 0.72rem;
  text-decoration: none;
}
.vendors-note {
  text-align: center;
  font-size: 0.86rem;
  color: var(--muted);
  margin-top: 18px;
}

/* The talent portrait would otherwise run 666px tall beside 388px of text. */
#celebrity .split__visual {
  width: 100%;
  max-width: 420px;
  margin-inline: auto;
}

/* Seven nav links no longer fit next to the logo and the CTA below ~900px,
   so hide the anchors earlier and give that band the sticky bar instead —
   otherwise a tablet loses its call to action along with the links. */
@media (max-width: 900px) {
  /* The anchors fold into a panel under the bar, opened by .nav-toggle.
     The bar itself is transparent at every width (see the ground notes
     further down), so the panel has to paint its own ground: --nav-bg is
     the bar colour the night driver writes, light by day and dark by
     night, and the link colour rides --nav-fg the same way. Over the one
     section that flips the bar to light type on its own (#nav.on-dark)
     the panel goes dark to match. */
  .nav-links {
    display: none;
    position: absolute;
    left: 0; right: 0; top: 100%;
    flex-direction: column;
    align-items: stretch;
    gap: 2px;
    margin: 0;
    padding: 8px 5vw 14px;
    background: var(--nav-bg, rgba(246, 247, 249, 0.97));
    backdrop-filter: blur(14px);
    -webkit-backdrop-filter: blur(14px);
    border-bottom: 1px solid var(--line);
    box-shadow: 0 18px 40px rgba(26, 26, 46, 0.10);
  }
  #nav.menu-open .nav-links { display: flex; }
  .nav-links a {
    display: block;
    padding: 13px 14px;
    font-size: 1rem;
    border-radius: 12px;
    color: var(--nav-fg-strong, var(--ink));
  }
  #nav.on-dark .nav-links { background: rgba(18, 14, 26, 0.96); border-bottom-color: rgba(255, 255, 255, 0.10); }
  #nav.on-dark .nav-links a { color: rgba(244, 246, 251, 0.92); }
  .nav-toggle {
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 4px;
    width: 42px; height: 42px;
    padding: 0;
    appearance: none;
    -webkit-appearance: none;
    border: 1px solid var(--line);
    border-radius: 100px;
    background: var(--nav-bg, rgba(246, 247, 249, 0.6));
    cursor: pointer;
    flex-shrink: 0;
  }
  .nav-toggle span {
    display: block;
    width: 16px; height: 2px;
    border-radius: 2px;
    background: var(--nav-fg-strong, var(--ink));
    transition: transform 0.25s ease, opacity 0.2s ease;
  }
  /* The button, too: light bars on the day-coloured pill vanished over the
     dark section, so the pill goes dark with them. */
  #nav.on-dark .nav-toggle { background: rgba(18, 14, 26, 0.9); border-color: rgba(255, 255, 255, 0.18); }
  #nav.on-dark .nav-toggle span { background: rgba(244, 246, 251, 0.92); }
  #nav.menu-open .nav-toggle span:nth-child(1) { transform: translateY(6px) rotate(45deg); }
  #nav.menu-open .nav-toggle span:nth-child(2) { opacity: 0; }
  #nav.menu-open .nav-toggle span:nth-child(3) { transform: translateY(-6px) rotate(-45deg); }
  .nav-toggle:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
  .sticky-cta {
    display: flex;
    position: fixed;
    left: 12px; right: 12px; bottom: 12px;
    z-index: 60;
    gap: 10px;
    padding: 10px;
    border-radius: 100px;
    background: rgba(255,255,255,0.92);
    backdrop-filter: blur(14px);
    box-shadow: 0 10px 34px rgba(26,26,46,0.18);
  }
  .sticky-cta .btn { flex: 1; padding: 12px 16px; font-size: 0.84rem; }
  body { padding-bottom: 76px; }
}

/* ══════════════════════════════════════════════════════════
   SCROLL SCENES
   Same pattern business.html uses: a tall .scene whose
   .sticky-frame pins for one viewport while a scroll-driven
   progress value (0→1) animates what is inside it.
   Everything below the scenes is ordinary flow.
   ══════════════════════════════════════════════════════════ */
.scene { position: relative; width: 100%; }
.sticky-frame {
  position: sticky;
  top: 0;
  height: 100vh;
  overflow: clip;
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 0 6vw;
  text-align: center;
}
/* 140vh, not 200. At 200 the headline had faded out by roughly a third
   of the runway and left more than a full screen of empty scrolling
   before the next section arrived. */
.scene--s1 { height: 140vh; }
.scene--s2 { height: 300vh; }
.scene--s3 { height: 340vh; }
/* 160vh, not 400. The 400 was runway for a row-by-row reveal that no
   longer exists: the table arrives whole, so everything past the screen it
   is read on was scrolling with nothing left to show. 60vh of pinned hold
   is enough to read it and then move on. */
.scene--s4 { height: 160vh; }
/* 220vh, down from 280. The scene's height IS its duration, and
   with every checkpoint on the page from the first frame there is
   no longer a stretch of scroll whose only job is waiting for the
   next card to appear. */
.scene--race { height: 220vh; }
/* Two beats and no list to read, so it needs far less runway than
   the race does: 180vh is 80vh of scrub, roughly 40 for the crawl
   and 40 for the overtake. (Two cars: th/lucky-draw only.) */
.scene--cars { height: 180vh; }
/* The printed version needs far MORE, and for the opposite reason:
   there IS a list to read here, and it is the video's whole receipt —
   about 1,390px of paper at desktop size, three windows' worth, which
   comes out of the machine a line at a time.

   700vh is 600vh of scrub, and 88% of that (see FEED_END) is ~4,750px
   of scrolling to print the lot: a rate of about 0.29px of paper per
   px of scroll, so an ordinary 400px flick advances it by roughly
   eight lines — one block of the receipt, not most of it.

   THIS IS THE PACING DIAL. Nothing else sets the speed: the driver is
   linear, so the rate is exactly stripHeight / (0.88 x runway). Raise
   this number to slow it down, lower it to speed it up.
   Only the ported page; .scene--cars keeps its own runway. */
.scene--feed { height: 700vh; }
/* HIDDEN, not deleted. The comparison table and the checkpoint race
   both made the same argument the cars section now makes, one after
   the other, three times in a row. Their substance moved into
   .cars-facts; the markup stays put so bringing either back is a
   one-line change rather than a rebuild.
   display:none and not visibility/opacity, so they cost no scroll
   height, take no place in the accessibility tree, and their cloud
   islands measure zero and are skipped by the canvas. */
/* #facts joins them. It was the ONLY place the capabilities were
   stated in words once the comparison table and the checkpoint race
   were hidden — but the receipt above now prints every one of them
   (8 SEC, SKU LEVEL, duplicates rejected, every entry tied to its
   receipt, 80,000+ scanning), so the section had become the same
   argument a third time, in a third form, immediately after it.

   HIDDEN, not deleted, like the two above and for the same reason:
   display:none costs no scroll height, takes no place in the
   accessibility tree, and its cloud island measures zero and is
   skipped by the canvas — so bringing it back is one line rather than
   a rebuild. */
#s4, #race, #facts { display: none; }
.scene--s6 { height: 100vh; }
/* One held line, so it wants the claim scene's runway rather than the
   longer ones: 80vh of pinned scroll is a screen to read it in. */
/* Opaque, and it is never seen: .why__wash covers it at z-index 0. It is
   here for the NAV, which picks its link colour by walking up from the
   point under the bar looking for the first background over 85% alpha. The
   wash is pointer-events:none so the hit test skips straight past it, and
   without this the bar sampled the body and the night sky behind it,
   decided it was over near-black, and went white on a butter ground. */
/* 340vh, not 180: the rail carries four phrases now instead of two, and
   FLOOD_END in lucky-draw-scenes.js hands the tail three hand-offs to get
   through instead of one. Kept proportional to the original — the single
   flip there got roughly 0.4 of a 180vh pin, about 72vh; three flips at
   similar generosity want on the order of 200vh, plus the flood's own
   share ahead of them. Tune together with FLOOD_END if either changes.

   1140vh, AND ALMOST ALL OF IT IS DWELL, not travel. Every phrase's
   panel is a tent that peaks at its own step, so on a rail that ran
   linearly each claim was at full strength for exactly one scroll
   position and was already fading on the next wheel notch. WHY_DWELL in
   lucky-draw-scenes.js parks the rail on each integer instead, and this
   height is the sum of that timetable plus the one screen the pinned
   frame occupies: flood 1.1, hand-off 0.5, "Because we are the best."
   0.8, hand-off 0.5, "Any paper receipt." 3.0, hand-off 0.5, "Any online
   order." 3.0, plus 1.0 for "Why BaiSed?" itself and 1 for the frame, all
   in screen heights. The last
   phrase gets the same three screens the one before it does: it is the
   end of the argument and the last thing between the reader and the next
   section, so leaving it should cost what reaching it did. The two phase
   marks the driver needs are derived from that same table, so this
   number and WHY_DWELL are the only two places to change, and they have
   to change together. */
/* BOTH PHASES IN ONE SECTION. 3.3 screens of runway for the two phrases
   and 6.0 for the printer, plus the one screen the pinned frame itself
   takes. The words' share is the sum of WHY_FLOOD, both WHY_DWELL entries
   and the one WHY_MOVE between them; the printer's is what #cars had. The
   split lives in one place, WHY_HEAD_END in lucky-draw-scenes.js, and it is
   derived from those same two numbers — change this height and change that. */
/* TRANSPARENT, so .rainbowband__ground shows through. The height is the
   only thing this rule still carries. */
/* 1080, up from 1030. The scene's height and the timetable in
   lucky-draw-scenes.js are one number counted twice: whyBudget() totals
   WHY_FLOOD + the dwells + steps * WHY_MOVE + WHY_FEED and gets screen
   heights of scroll, and this is that total plus the one screen the pin
   itself occupies. WHY_MOVE is 1.6, up from the 0.4 it started at, so the
   47 glyphs of the answer have room to assemble one at a time: +1.2
   screens, so this is +120vh on the original 1030. Change one and change
   the other or every phase drifts. */
.scene--why { height: 1150vh; background: transparent; }
/* AND NOT BELOW 1360px, where .why-proof is display:none and the split
   is pinned to 0. The dwell is long because there is a clip playing
   inside it; with no panel on screen the same runway is three screens
   of a motionless line of text. The timetable is proportional, so a
   shorter section compresses every phase by the same factor rather than
   putting any of them out of order. */
@media (max-width: 1359px) {
  .scene--why { height: 500vh; }   /* the same +1.2 screens, to scale */
}
/* Centred only where it can actually be centred AND clear. The ribbon is a
   fixed 320px and cannot go further left than its own column's edge, while
   the heading grows with the viewport — so below roughly 1360px there is no
   position where a centred heading and the pour do not overlap. Measured:
   at 1180px they collide by 49px and at 900px by 118px, and no font size
   that keeps the heading readable escapes it. Above that the pour clears
   the centred words on its own (21px at 1400, 22px at 1920).

   So: centred on wide screens, and on narrower ones the type steps to the
   right of the pour instead. Under 861px the road is display:none, so the
   question centres again with nothing to avoid. */
@media (min-width: 861px) and (max-width: 1359px) {
  .scene--why .sticky-frame { justify-content: flex-end; }
  .why-q { text-align: right; max-width: 56%; }
}
@media (prefers-reduced-motion: reduce) {
  /* After the height above, not before it. Equal specificity means the
     later rule wins, media query or not. */
  .scene--why { height: auto; }
  .why-q { opacity: 1; transform: none; }
  .why-proof { transform: none; }
  .why__wash { -webkit-mask-image: none; mask-image: none; }
  /* And after the 1360px gate, for the same reason. The panel is centred
     on the heading and overlaps it, so a copy of it left displayed here
     would sit permanently invisible behind the words: the videos only
     ever become visible through poseProof(), and this query has just
     switched off the scroll loop that calls it. Off outright instead,
     rather than an unreachable video decoding under the text. */
  .why-proof { display: none; }
}
.scene--s6 .sticky-frame { position: relative; height: auto; padding: 110px 6vw; }

/* ── S1 hero ─────────────────────────────────────────────── */
.sc-hero{max-width:1000px}
.sc-eyebrow{
  font-size:.6rem;letter-spacing:.2em;text-transform:uppercase;color:var(--faint);
  margin-bottom:22px;opacity:0;transform:translateY(10px);
  transition:opacity .9s ease,transform .9s ease}
.sc-eyebrow.in{opacity:1;transform:none}
.sc-h1{
  font-size:clamp(2.6rem,6.6vw,5.2rem);line-height:1.08;letter-spacing:-.03em;
  font-weight:700;margin-bottom:26px;transform-origin:center}
/* The long-form headline needs a smaller step than the three-word one,
   or it wraps into five lines on a laptop. */
.sc-h1--long{font-size:clamp(1.85rem,4.4vw,3.4rem);line-height:1.15;max-width:15ch;margin-inline:auto}
@media (min-width:900px){ .sc-h1--long{max-width:none} }
/* Under 900 the two hard breaks fought the 15ch box: "The easiest way to"
   filled it and "turn" dropped onto a line of its own, five lines in all.
   Let the phone wrap it, balanced, so the lines land where they do on a
   desktop whenever they fit and split evenly when they do not. */
@media (max-width:899px){ .sc-h1--long{max-width:none;text-wrap:balance} .sc-h1--long br{display:none} }
.sc-h1 em{font-style:normal;font-weight:800;
  background:var(--fire);-webkit-background-clip:text;background-clip:text;
  -webkit-text-fill-color:transparent;color:transparent}
.sc-h1-glow{
  position:relative;display:inline-block;
  animation:sc-h1-glow-pulse 2.6s ease-in-out infinite}
@keyframes sc-h1-glow-pulse{
  0%,100%{text-shadow:0 0 16px rgba(255,107,107,.3),0 0 34px rgba(255,189,113,.22)}
  50%{text-shadow:0 0 28px rgba(255,107,107,.55),0 0 52px rgba(255,189,113,.4)}
}
@media (prefers-reduced-motion: reduce){
  .sc-h1-glow{animation:none;text-shadow:0 0 22px rgba(255,107,107,.4),0 0 40px rgba(255,189,113,.3)}
}
.sc-sub{
  font-size:clamp(.95rem,1.5vw,1.12rem);color:var(--muted);max-width:56ch;margin:0 auto;
  opacity:0;transform:translateY(10px);transition:opacity .9s ease .15s,transform .9s ease .15s}
.sc-sub.in{opacity:1;transform:none}
.sc-hint{
  margin-top:44px;font-size:.62rem;letter-spacing:.22em;text-transform:uppercase;
  color:var(--faint);opacity:0;transition:opacity .9s ease .4s}
.sc-hint.in{opacity:1}
.sc-hint i{display:block;width:1px;height:34px;margin:12px auto 0;
  background:linear-gradient(180deg,rgba(26,26,46,.28),transparent)}

/* ── S2 phrase-by-phrase question ────────────────────────── */
/* width:100% as well as the cap. Without it the wrap is content-sized, and
   on a phone the cards below inherited the width of the question text
   rather than the width of the frame. */
.q-wrap{max-width:1000px;width:100%}
.q-line{display:block;font-size:clamp(2rem,5.4vw,4rem);line-height:1.18;
  letter-spacing:-.02em;font-weight:700;margin-bottom:.18em}
.q-line .word{display:inline-block;opacity:.14;
  transition:opacity .45s ease,color .45s ease}
.q-line .word.lit{opacity:1}
.q-line .word.fire.lit{background:var(--fire);-webkit-background-clip:text;
  background-clip:text;-webkit-text-fill-color:transparent;color:transparent;font-weight:800}

/* ── the two draw types, face down ────────────────────────
   Pick a card, from the UI lab, with the lab's own caveat answered: the
   name is on both faces, so nothing a reader needs to choose with is
   hidden behind the click. Only the answer is.

   Three transforms, three layers, because two of them would overwrite
   each other on one element: .pick-slot carries the reveal on scroll,
   .pick carries the flip, and hover is left to shadow and border so it
   never becomes a third. */
.picks {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px;
  max-width: 660px;
  margin: 0 auto;
  text-align: left;
}
/* .rise handles the reveal here. The slot stays because it keeps that
   reveal's transform off .pick. */
.pick-slot { will-change: transform, opacity; }
/* ── THE CARD IS THE PICTURE ──────────────────────────────
   It was a two-sided thing that turned: two faces in one grid cell, the
   card rotating 180deg between them, and a stack of machinery to survive
   the rotation — grid-area 1/1 on both faces, backface-visibility on
   each, a .revealed state that dropped the 3D afterwards because a card
   left mid-rotation hit-tests through its own transform, and a close
   button to turn it back. The turn went first; the panel of copy that had
   been the back went with it.

   What is left is the art and its two labels, and that is the whole
   control. Everything it used to say in prose the band below now says by
   changing: see .pickband. */
.pick { position: relative; height: 100%; display: flex; flex-direction: column; }

.pick__front {
  position: relative;
  border-radius: 22px;
  box-shadow: 0 10px 30px rgba(26,26,46,0.07);
  transition: box-shadow 0.35s ease, border-color 0.35s ease;
}

/* Face down: the PRIZE, and nothing else. A blank plate marked "?" asked
   the reader to spend a click on nothing they could see, and the two
   things being chosen between, a car and a phone or a concert and a fan
   meet, are exactly what makes the choice worth opening.

   The face is now the picture edge to edge with its labels laid on it,
   which is the Frans Hals Museum's card: a category on a pill in the top
   corner, the name across the bottom over a scrim, and the affordance as a
   circular arrow in the far corner, which is monks.com's. The caption
   strip it replaces was a band of flat colour doing the same job with less
   of the art. */
.pick__front {
  position: relative;
  z-index: 2;
  display: block;
  /* Explicit, because the art is absolutely positioned and leaves the
     button with no in-flow content to size against. A grid item is
     stretched to its cell either way, but under reduced motion the cell is
     unstacked to plain blocks, and a button with auto width shrink-wraps:
     the face measured 0x0 and the whole card vanished. */
  width: 100%;
  aspect-ratio: 16 / 10;
  padding: 0;
  border: none; cursor: pointer; overflow: hidden;
  font-family: inherit; color: #fff; text-align: left;
}
/* The scrim. White type over a bright podium needs a ground of its own,
   and a gradient keeps the art readable where the type is not. */
.pick__front::after {
  content: '';
  position: absolute; inset: auto 0 0 0; height: 54%;
  background: linear-gradient(to top,
    rgba(12, 6, 22, 0.82), rgba(12, 6, 22, 0.26) 56%, rgba(12, 6, 22, 0));
  pointer-events: none;
}
.pick__front--draw { background: var(--fire); }
/* The second type gets its own face or the pair reads as one product dealt
   twice, and now that the face carries the prize art the two hues are the
   art's own: fire under the gold and the car, violet under the lightstick
   and the mic. */
.pick__front--fan { background: var(--fan); }
.pick__front:hover { box-shadow: 0 18px 44px rgba(26,26,46,0.18); }
/* The art leans in under the cursor. On a face that is now mostly picture
   this is the clearest signal that the whole plate is a control. */
.pick__front:hover .pick__face { transform: scale(1.05); }
.pick__front:focus { outline: none; }
.pick__front:focus-visible { outline: 3px solid var(--red); outline-offset: 4px; }

/* Bled back out of the face's side padding to the card's edges, and
   clipped to the corners by the overflow above. The same 16/10 block the
   back opens with, from the same file, so the turn holds the picture still
   and brings the words in behind it rather than swapping one image for
   another. The source is exactly 16/10, so cover crops nothing. */
/* The art fills the face. max-width:none is load-bearing against the
   sheet's global img max-width:100%, and inset:0 beats the width/height
   attributes the tag carries for layout stability. */
.pick__face {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  max-width: none;
  object-fit: cover;
  transition: transform 0.5s cubic-bezier(.2,.8,.25,1);
}
/* The category, which the reader used to have to open the card to learn. */
.pick__kind {
  position: absolute; top: 13px; left: 14px; z-index: 2;
  padding: 6px 11px;
  border-radius: 100px;
  background: rgba(255, 255, 255, 0.94);
  color: #17131f;
  font-size: 0.57rem; font-weight: 700;
  letter-spacing: 0.13em; text-transform: uppercase;
}
html[lang="th"] .pick__kind { letter-spacing: 0.05em; }
.pick__name {
  /* right: 16px, not the 62 it carried. That gap was the room reserved for
     the circular arrow that used to sit in this corner; with the arrow gone
     the name has the full width of the card back. */
  position: absolute; left: 16px; right: 16px; bottom: 15px; z-index: 2;
  font-size: 1.18rem; font-weight: 700;
  letter-spacing: -0.015em; line-height: 1.16;
}
/* The circular arrow that used to sit in the bottom right corner is gone,
   markup and all. It read as a control on a card that is already a button
   end to end, and on a face that is otherwise all picture it was the one
   piece of chrome. The picked state is carried by the ring around the card
   (see .pick.is-picked), which says the same thing without drawing a
   second target on top of the first. */

/* The frame is one viewport and cannot scroll, so on a short screen the
   art and the paddings give back the room rather than the copy being cut.
   The nav sits on top of the frame at 73px, which was running over the
   question's first line. */
/* Still two across on a phone. Stacked, the pair plus the question runs
   past a single viewport, and this frame cannot scroll. */
@media (max-width: 620px) {
  .picks { gap: 10px; }
  .pick__front { border-radius: 16px; }
  .pick__kind { top: 9px; left: 9px; padding: 4px 8px; font-size: 0.5rem; letter-spacing: 0.09em; }
  .pick__name { left: 10px; right: 10px; bottom: 10px; font-size: 0.92rem; }
}
@media (prefers-reduced-motion: reduce) {
  /* All this block used to do was unstack the flip: the two faces were in
     one cell and it laid them out one after the other, the picture then
     the words. That IS the layout now, for everyone, so only the .rise
     override is left with a job to do here. */
  .pick-slot { opacity: 1; transform: none; }
}

/* ── built per campaign ───────────────────────────────────
   Three claims the page used to make in prose. Each panel is the thing
   itself rather than a description of it: real campaign pages, a clock
   that runs the eight seconds it claims, and the games that shipped. */
.tailor {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 24px;
  padding: 28px;
}
.tailor + .tailor-pair { margin-top: 18px; }
.tailor__head { margin-bottom: 18px; }
.tailor__head h3 { font-size: 1.25rem; letter-spacing: -0.01em; margin-bottom: 6px; }
.tailor__head p { font-size: 0.88rem; color: var(--muted); font-weight: 300; max-width: 56ch; }
.tailor-pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 18px; }

.chips { display: flex; flex-wrap: wrap; gap: 8px; margin-bottom: 18px; }
.chip {
  font-family: inherit; font-size: 0.82rem; font-weight: 500;
  padding: 8px 15px; border-radius: 100px; cursor: pointer;
  background: transparent; color: var(--muted);
  border: 1px solid var(--line);
  transition: background 0.25s ease, color 0.25s ease, border-color 0.25s ease;
}
.chip:hover { border-color: rgba(255,107,107,0.34); color: var(--ink); }
.chip.is-on { background: var(--fire); color: #fff; border-color: transparent; font-weight: 600; }
.chip:focus { outline: none; }
.chip:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
/* A finger, not a pointer. The pills are 30 to 33px tall as drawn, under
   the 44 a fingertip needs; the extra height goes on the tap target only,
   the type and padding stay as they are. */
@media (pointer: coarse) {
  .chip { min-height: 42px; }
}

/* the brand panel */
.brand-card { display: grid; grid-template-columns: minmax(0, 1.35fr) minmax(0, 1fr); gap: 22px; align-items: center; }
.brand-card[hidden] { display: none; }
.brand-card__art {
  width: 100%; height: auto; border-radius: 16px; display: block;
  border: 1px solid var(--line);
}
.brand-card__tag {
  display: inline-block; font-size: 0.62rem; font-weight: 600;
  letter-spacing: 0.14em; text-transform: uppercase; color: var(--red);
  margin-bottom: 8px;
}
.brand-card__meta b { display: block; font-size: 1.3rem; letter-spacing: -0.02em; margin-bottom: 8px; }
.brand-card__rule { font-size: 0.88rem; line-height: 1.55; color: var(--muted); font-weight: 300; }

/* the clock */
.wait { display: flex; flex-direction: column; gap: 14px; }
.wait__clock { display: flex; align-items: baseline; gap: 8px; }
.wait__clock b {
  font-size: 2.6rem; font-weight: 800; letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
  background: var(--fire); -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
}
.wait__clock span { font-size: 0.78rem; color: var(--faint); letter-spacing: 0.08em; text-transform: uppercase; }
.wait__steps { list-style: none; display: flex; flex-direction: column; gap: 7px; }
.wait__steps li {
  position: relative; padding-left: 26px;
  font-size: 0.85rem; color: var(--faint); font-weight: 300;
  opacity: 0.45; transition: opacity 0.3s ease, color 0.3s ease;
}
.wait__steps li::before {
  content: '○'; position: absolute; left: 0; top: -1px;
  color: rgba(26,26,46,0.24); font-size: 0.9rem;
}
.wait__steps li.done { opacity: 1; color: var(--ink); }
.wait__steps li.done::before { content: '✓'; color: var(--red); }
.wait__count { font-size: 0.88rem; color: var(--muted); }
.wait__count b {
  font-size: 1.15rem; color: var(--ink); font-variant-numeric: tabular-nums;
  transition: transform 0.3s cubic-bezier(.34,1.56,.64,1); display: inline-block;
}
.wait__count b.bump { transform: scale(1.35); }
.wait__replay {
  align-self: flex-start; font-family: inherit;
  font-size: 0.78rem; font-weight: 600; color: var(--red);
  background: none; border: none; padding: 0; cursor: pointer;
}
.wait__replay:hover { text-decoration: underline; }
.wait__replay:focus { outline: none; }
.wait__replay:focus-visible { outline: 2px solid var(--red); outline-offset: 3px; }

/* the games */
.game-card[hidden] { display: none; }
.game-card__art { aspect-ratio: 16 / 10; margin-bottom: 12px; }
.game-card p { font-size: 0.86rem; line-height: 1.55; color: var(--muted); font-weight: 300; }

@media (max-width: 900px) {
  .tailor-pair { grid-template-columns: 1fr; }
  .brand-card { grid-template-columns: 1fr; gap: 16px; }
}
@media (max-width: 620px) { .tailor { padding: 20px; } }
@media (prefers-reduced-motion: reduce) {
  /* The clock is the claim, so it still shows the answer; it just does
     not count up to it. */
  .wait__steps li { opacity: 1; color: var(--ink); }
  .wait__steps li::before { content: '✓'; color: var(--red); }
  .wait__count b.bump { transform: none; }
}

/* ── the campaign dashboard ───────────────────────────────
   The four views a brand gets, rebuilt as static markup so the section
   shows the thing instead of reserving a screenshot slot for it. Drawn in
   our palette rather than a partner's, and every figure on it is made up,
   which the badge on the card says out loud. */
/* This 128px top-padding hack is gone, and the note above used to warn
   exactly that it would need re-checking here: it existed because #data's
   .shell used to sit directly under #facts's, only 195 of the 219px a rim
   needs to clear its neighbour's without the top lobes clipping flat.
   #data-demo now sits between the two, and it carries no .shell of its
   own (see the comment on .datastage below), so it contributes no island
   for the cloud script to clip against at all. The genuine gap between
   #facts's rim and #data's is now #data-demo's whole height, hundreds of
   px on every breakpoint, which clears 219 with room to spare. */

/* ── #data-demo · a claim, then the proof, at the same spot ──
   Went through two shapes before this one. First the laptop shared a row
   with the words about it. Then, split apart, it got a full band to
   itself, on the reasoning that the demo was worth more room. Reasonable,
   and reader feedback on the live page said otherwise: given the whole
   band, the READING still came first, a screen of text before the machine
   that was supposed to make the words unnecessary.

   This is the third shape. .scene--data-zoom pins the reader on a plain
   white field for a runway of scroll: a short restated claim, decorative
   and read by nobody's screen reader twice (the real, accessible heading
   is #data-h, below, in the section that still carries the long form of
   this same claim), fading out as the machine it is a claim ABOUT scales
   down out of an oversized, half-transparent blur and settles at its
   resting size, fully opaque, fully clickable. Reader and machine now
   arrive at the same moment instead of one after the other. The zoom out
   of the section name is a description of what the READER sees, not the
   laptop: the laptop starts too close to read and the frame pulls back
   from it, which is the standard sense of the word.

   The white field is deliberate too, not a placeholder. Every other scene
   on this page sits on the painterly cloud canvas; this one is a plain
   product surface on purpose, the same way a phone held up in a photo
   sits on nothing in particular — the point is the screen, not the room
   it is in. */
.scene--data-zoom {
  /* A tall runway, because the reader paces this scene with their own
     scroll. It was briefly converted to a one-viewport section playing on
     a 24s clock; that removed the scroll runway entirely, so the camera
     could not be driven at all, and the sequence fired exactly once per
     page load and sat finished for every visit after it. Scrubbing is
     what this scene is.

     800vh of it. The extra over 500 went almost entirely to the HOLDS:
     at 500 each reading beat was about 145px of scroll, which one flick
     of a trackpad clears, so the camera moved off the standfirst or the
     list before either could be read. They are 480 to 600px each now, and
     the closing beat carries a button the pointer has to travel to and
     press before the machine pulls back at all. */
  height: 800vh;

  /* THIS SCENE IS PERMANENTLY DAY. The comment above says outright that it
     is meant to be "a plain product surface... on nothing in particular,"
     isolated from the page's day/night story — and it never actually was,
     because every surface in it was drawn with the shared tokens that
     lucky-draw-clouds.js's night driver rewrites on :root as the page is
     descended: --card and --bg2 mix toward navy, --ink flips to near
     white, and --muted/--faint/--line follow. So the section went grey at
     its own edges AND the laptop's screen went dark behind the dashboard,
     both while the scene was still mid-run and nowhere near unpinning.
     What triggers it is not this scene's own progress at all: night is
     anchored to #channels-fit, three sections further down, and its window
     opens early enough to catch the last ~15% of this scene's 800vh.

     Re-declared here at their :root daytime values, so the whole subtree
     inherits day whatever the driver has written above it. Scoping them
     rather than pinning one literal colour is what covers the screen
     interior too — the dashboard inside the aperture is built from the
     same tokens as the section around it. Keep this list in step with the
     six the driver touches (see the removeProperty list in clouds.js);
     --nav-* is deliberately not among them, since the bar is outside this
     section and samples the ground under itself. */
  --ink: #1a1a2e;
  --muted: rgba(26,26,46,0.66);
  --faint: rgba(26,26,46,0.52);
  --line: rgba(26,26,46,0.10);
  --card: #ffffff;
  --bg2: #EDEEF2;
  /* TRANSPARENT, so .rainbowband__ground shows through. This section sits
     inside that band now, and the white it used to paint is still the
     right value for everything INSIDE it — the dashboard's own cards read
     --card — so only the section's own fill is given up, not the token. */
  background: transparent;
}
@media (prefers-reduced-motion: reduce) {
  /* After the height above, not before it: equal specificity means the
     later rule wins regardless of media query.

     Flattened to exactly what the phone gets, and for the same reason:
     with no runway there is no scrub, so the machine renders at rest and
     the decorative claim is dropped rather than left stacked in the same
     grid cell as the thing it was supposed to hand over to. */
  .scene--data-zoom { height: auto; }
  .scene--data-zoom .sticky-frame { position: relative; height: auto; padding: 64px 6vw; }
  .scene--data-zoom .laptop__intro { display: none; }
}
@media (max-width: 900px) {
  .scene--data-zoom { height: auto; }
  .scene--data-zoom .sticky-frame { position: relative; height: auto; padding: 64px 6vw; }
}

/* ── THE CLAIM, ON THE SCREEN ────────────────────────────────
   Sits INSIDE .laptop, laid over the screen aperture, rather than in a
   grid cell of its own stacked over the whole machine. That single move
   is what the scene is about: the reader opens on a screen filling the
   window with a sentence on it, the frame pulls back to reveal the screen
   belongs to a laptop, and the sentence dissolves into the dashboard that
   was behind it all along. Laid over the SECTION instead, the words were
   just a caption that happened to be in front, and the zoom behind them
   was a separate event they had nothing to do with.

   Positioned on the same four measured aperture percentages the frame
   cut-out uses — re-measure these together if the art is ever replaced.

   Opaque background, and that is what makes the hand-off work: there is
   no cross-fade to co-ordinate here, no second element to bring up in
   step. The dashboard is already sitting behind this at full opacity from
   the first frame. Wiping this one layer off IS the reveal.

   Wiping, not fading, and in two sub-beats. Ramping the whole sheet's
   opacity spent the middle of its beat showing a screen of type and a
   screen of numbers at half strength each, unreadable as either. Instead
   the words leave first, against this sheet's own white, and only then
   does a soft mask edge travel down the now-blank sheet and uncover the
   dashboard — so type is never cross-faded over type. The mask is
   declared below at full black, which is the same as no mask at all:
   that is the resting state, and lucky-draw-scenes.js moves the edge
   through it per frame.

   Decorative, and aria-hidden in the markup: these are the SAME words
   #data-h carries below, restated as a graphic rather than read twice by
   a screen reader. pointer-events: none so it can never take a click
   meant for the dashboard, including in the frames where it is nearly
   transparent but not yet gone. */
/* Two pages laid side by side across the screen, each one a viewport of
   its own while the scene is zoomed in: the camera opens hard on the left
   page — screen filling the window, no bezel anywhere in it — then pulls
   back to the machine's top-left corner, tracks across to the right page,
   and only after that pulls out to the whole laptop.

   Which means the two pages are READ AT DIFFERENT ZOOMS, and each has to
   be sized for its own. --zo is the opening zoom and --zt the tracking
   one, both written per viewport by lucky-draw-scenes.js; each page picks
   whichever it is read at into --s and divides its own lengths by that.
   The type is then the size it should be on the reader's screen at the
   moment it is meant to be read, and grows or shrinks with the machine
   either side of that, which is what a real screen would do. The footnote
   is the third case: it is read at rest, so it is plain px. */
.laptop__intro {
  position: absolute;
  left: 10.720%;
  top: 2.045%;
  width: 78.407%;
  height: 74.091%;
  --zo: 3.4;
  --zt: 2.1;
  --zc: 2.1;
  /* A shade off white, so the windows laid on it read as windows. The
     dashboard that replaces it is white, so the wipe also lightens the
     screen, which is the machine going from idle to showing the thing. */
  --desk: #F5F6F8;
  overflow: hidden;
  /* Its own compositor layer, and this is what keeps the type sharp. The
     laptop around it is promoted too, so the artwork is rasterised once at
     layout size and scaled as a texture — cheap, and no one can tell on a
     smooth illustration. Type cannot survive that: magnifying a 13px
     raster 3.5x is a smear. Given its own layer, this sheet is rasterised
     at the scale it is actually composited at, so the words are drawn at
     the size they are read at. Both promoted is also the fastest of every
     arrangement tried — median 33ms a frame, against 117ms with the
     promotion off and 525ms with the artwork on a layer of its own. */
  will-change: transform;
  /* Above .laptop__screen (also 1, and earlier in the DOM) but below
     .laptop__frame at 2, so the bezel's own anti-aliased inner edge still
     draws over this rather than the other way round. */
  z-index: 1;
  background: var(--desk);
  /* Full black is a no-op mask, so this IS the resting look. The edge that
     travels through it is written per frame by lucky-draw-scenes.js, which
     falls back to the opacity ramp on any engine that ignores this. */
  -webkit-mask-image: linear-gradient(to bottom, #000, #000);
  mask-image: linear-gradient(to bottom, #000, #000);
  pointer-events: none;
}
/* The layer the words live on. Separate from the sheet above it because
   the two leave at different times and in a fixed order: the ink fades
   out first against its own white, and only then does the blank sheet get
   wiped off the dashboard. Type is never cross-faded over type. */
/* Just the layer that fades: the pages inside it place themselves. */
.laptop__ink {
  position: absolute;
  inset: 0;
}
/* Each page is a full-glass box whose content is then shifted to the spot
   that lands DEAD CENTRE OF THE READER'S WINDOW at that page's own beat.
   Both --page-l and --page-r are solved per viewport in
   lucky-draw-scenes.js from the camera framing they will be read under,
   because "centre of the screen" and "centre of the window" are not the
   same place once the camera is parked on a corner of the machine: at a
   third and two thirds of the glass, which is where these used to sit, the
   copy ended up low and off to one side of the window at both corner
   beats. They stay fixed to the glass, so they still travel with the
   machine while it moves; they are simply positioned for the frame they
   are meant to be read in. */
.laptop__slide {
  position: absolute;
  inset: 0;
  opacity: 1;
  transform: translate(calc(var(--px, 50%) - 50%), calc(var(--page-y, 50%) - 50%));
}
.laptop__slide {
  display: grid;
  place-content: center;
  justify-items: center;
  gap: calc(15px / var(--s));
  padding: 0 calc(34px / var(--s));
  text-align: center;
  min-width: 0;
}
/* Each page takes the zoom, and the spot on the glass, that it is actually
   read at. The heading opens the scene on the white page; the standfirst
   is a beat of its own at the top-left corner, so the corner reveal shows
   the reader something new rather than the sentence they have just read,
   shrunk. Both sit at the same place on the glass, since the shot that
   opens on one is the shot that pulls back to the other. */
.laptop__slide--intro { --s: var(--zo); --px: var(--page-l, 30%); }
.laptop__slide--lede  { --s: var(--zt); --px: var(--page-l, 30%); }
.laptop__slide--claim { --s: var(--zt); --px: var(--page-r, 70%); }
/* Holds a line length that reads, since each page is nearly the full
   window wide once the zoom is applied. */
.laptop__slide > * { max-width: calc(720px / var(--s)); }

/* THE WINDOW. Every beat is now a panel on the machine's screen rather
   than a line of type floating on it, and it is deliberately the SAME
   panel the dashboard is: the same hairline border, the same radius, and
   the same title bar carrying the same live dot, brand and date range. So
   the reader is looking at one application the whole way down, and the
   wipe at the end does not introduce a new object, it just fills the
   window that has been on screen since the first frame. */
.laptop__card {
  width: 100%;
  background: #fff;
  border: calc(1px / var(--s)) solid var(--line);
  border-radius: calc(16px / var(--s));
  box-shadow: 0 calc(20px / var(--s)) calc(48px / var(--s)) rgba(26, 26, 46, 0.10);
  overflow: hidden;
  text-align: left;
  /* The ring struck around a panel as the pointer's click lands. Driven by
     --pop from the script, and drawn as an outline rather than a second
     box-shadow so it cannot clobber the card's own (which is divided by
     the zoom, and an inline override would not be). Outlines take no part
     in layout either, so nothing shifts while it flashes.

     Deliberately NO will-change here: this sheet is already its own
     compositor layer, and promoting things inside it is what took the
     median frame from 33ms to 525ms when it was tried on the artwork. */
  outline: calc(var(--pop, 0) * 3.6px / var(--s)) solid rgba(255, 107, 107, calc(var(--pop, 0) * 0.62));
  outline-offset: calc(var(--pop, 0) * 2px / var(--s));
}
.laptop__card-bar {
  display: flex;
  align-items: center;
  gap: calc(10px / var(--s));
  padding: calc(12px / var(--s)) calc(18px / var(--s));
  border-bottom: calc(1px / var(--s)) solid var(--line);
  background: rgba(26, 26, 46, 0.02);
  font-size: calc(13px / var(--s));
  line-height: 1;
}
.laptop__card-bar i {
  width: calc(8px / var(--s));
  height: calc(8px / var(--s));
  border-radius: 50%;
  background: #10B981;
  box-shadow: 0 0 0 calc(3px / var(--s)) rgba(16, 185, 129, 0.16);
  flex: 0 0 auto;
}
.laptop__card-bar b { font-weight: 700; letter-spacing: -0.01em; color: var(--ink); }
.laptop__card-bar span { color: var(--faint); }
.laptop__card-bar em {
  margin-left: auto;
  font-style: normal;
  font-size: calc(9px / var(--s));
  font-weight: 600;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--muted);
}
.laptop__card-body { padding: calc(24px / var(--s)) calc(24px / var(--s)) calc(26px / var(--s)); }
.laptop__intro h3 {
  margin: 0;
  font-size: calc(40px / var(--s));
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.18;
  color: var(--ink);
}
.laptop__intro .eyebrow {
  margin: 0;
  font-size: calc(13px / var(--s));
}
/* Its own beat now, so it is set to be read rather than skimmed under a
   headline. */
.laptop__lede {
  margin: 0;
  font-size: calc(22px / var(--s));
  line-height: 1.6;
  font-weight: 300;
  color: var(--muted);
}
/* ── the three points ────────────────────────────────────
   This beat used to be a sixty-word paragraph, which is a lot of reading
   to put on a screen the reader is watching go by rather than sitting
   with. It is three lines now, and each one carries its own colour so the
   three read as three things at a glance rather than as a block of text
   that happens to have breaks in it.

   The colour is one custom property per row, set on the row and used three
   times: the rule down its left edge at full strength, the dot at full
   strength, and the panel behind it at a tenth. One value per row means a
   fourth point is one more nth-child line, not four more declarations.

   Everything is divided by --s, the same scale the rest of this screen
   uses, so the block holds its proportions as the camera zooms. */
.laptop__points {
  list-style: none;
  margin: 0;
  padding: 0;
  text-align: left;
  display: grid;
  gap: calc(12px / var(--s));
  font-weight: 400;
}
/* Three grid items, not two: the dot, the label and the line each place
   themselves, so they need telling where to go. Left to auto-placement the
   line wrapped onto a second row UNDER the dot, in the dot's column. */
.laptop__points li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  grid-template-rows: auto auto;
  align-items: start;
  /* SPLIT, because the two axes were doing different jobs on one number.
     The column gap holds the dot off the label and 14px is right for that.
     The row gap sat between a label and its OWN sentence, where 14px reads
     as two separate things rather than one: measured, each row was 103px
     tall to carry 73px of text, with 17px of that stranded between
     "Verified" and the line explaining it. */
  column-gap: calc(14px / var(--s));
  row-gap: calc(3px / var(--s));
  padding: calc(11px / var(--s)) calc(18px / var(--s));
  border-radius: calc(14px / var(--s));
  border-left: calc(4px / var(--s)) solid rgb(var(--pt));
  background: rgb(var(--pt) / 0.09);
  color: var(--ink);
  font-size: calc(20px / var(--s));
  line-height: 1.45;
}
/* Sits on the cap height of the label rather than the top of the box, so
   it reads as a bullet against the word instead of floating above it. */
.laptop__points li::before {
  content: '';
  grid-column: 1;
  grid-row: 1;
  width: calc(11px / var(--s));
  height: calc(11px / var(--s));
  margin-top: calc(7px / var(--s));
  border-radius: 50%;
  background: rgb(var(--pt));
}
/* Both in the second column, stacked, so the line sits under its own
   label rather than under the dot. Alignment is stated rather than
   inherited: this block is dropped inside a centred card. */
.laptop__points b {
  grid-column: 2;
  grid-row: 1;
  font-weight: 700;
  letter-spacing: 0.01em;
  text-align: left;
}
.laptop__points span {
  grid-column: 2;
  grid-row: 2;
  color: var(--muted);
  /* The sentence, not the label, is what the reader is here to read, so it
     is set close to the label's own size rather than as a caption under
     it. It was 16 against the label's 18 and read as small print on a
     screen this scene zooms right into. */
  font-size: calc(19px / var(--s));
  line-height: 1.45;
  text-align: left;
  /* The row gap above is the whole separation now. This was 3px on top of
     it, which is the kind of number that is invisible on its own and adds
     up across three rows. */
  margin-top: 0;
}
/* Brand coral, a deepened brand amber, and the periwinkle the campaign
   decks already use. The amber is darkened from --orange: that value is a
   pale fill colour and it disappears as a 4px rule on a white screen. */
.laptop__points li:nth-child(1) { --pt: 255 107 107; }
.laptop__points li:nth-child(2) { --pt: 232 137 12; }
.laptop__points li:nth-child(3) { --pt: 126 126 255; }
/* ── THE CLAIM ───────────────────────────────────────────
   The third card was a ring with three equal arcs and a legend beside it.
   The ring divided no quantity, so the thirds asserted a proportion that
   did not exist, and the three lines the legend carried are said again by
   the closing note a few seconds later, better and at a stronger moment.

   What is left is the one sentence the whole section is arguing, set large
   enough to be the only thing on the machine for its beat. A card that
   says one thing does not need a chart to hold the eye.

   Everything divides by --s, the scale the rest of this screen uses, so it
   holds its proportions through the camera's zoom. */
.laptop__claim {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  gap: calc(10px / var(--s));
  padding: calc(34px / var(--s)) calc(26px / var(--s)) calc(36px / var(--s));
  border-radius: calc(20px / var(--s));
  text-align: center;
  /* Two backgrounds and a transparent border: the inner one is clipped to
     the padding box and the gradient to the border box, which paints a
     gradient edge without a wrapper element. The three colours are the
     three rows of the card before it, so the two beats are visibly the
     same family rather than two unrelated screens. */
  border: calc(2px / var(--s)) solid transparent;
  background:
    linear-gradient(176deg, #FFFDFB, #FAFAFF) padding-box,
    linear-gradient(104deg,
      rgb(255 107 107 / 0.60),
      rgb(232 137 12 / 0.52) 48%,
      rgb(126 126 255 / 0.62)) border-box;
}
/* The same three colours again, as a row of dots. It lands just before the
   sentence does, so the block assembles rather than arriving whole. */
.laptop__claim-dots {
  display: flex;
  gap: calc(7px / var(--s));
  margin-bottom: calc(12px / var(--s));
}
.laptop__claim-dots i {
  width: calc(9px / var(--s));
  height: calc(9px / var(--s));
  border-radius: 50%;
}
.laptop__claim-dots i:nth-child(1) { background: #FF6B6B; }
.laptop__claim-dots i:nth-child(2) { background: #E8890C; }
.laptop__claim-dots i:nth-child(3) { background: #7E7EFF; }
/* Two lines, two weights. The first sets the condition and the second is
   the payoff, so the second carries the size and the ink and the first
   steps back out of its way. */
.laptop__claim-line {
  margin: 0;
  /* Sized to the widest this card can hold on one line. At the design's
     first pass these were 30 and 40, which came out SMALLER on screen than
     the 19px bullet copy on the card before it, because this slide is
     scaled by --zt rather than the lede's own scale. A statement card
     whose statement is set smaller than the list it follows reads as a
     footnote. */
  font-size: calc(33px / var(--s));
  line-height: 1.22;
  letter-spacing: -0.012em;
  font-weight: 600;
  color: var(--muted);
  text-wrap: balance;
}
.laptop__claim-line--key {
  font-size: calc(48px / var(--s));
  font-weight: 800;
  letter-spacing: -0.022em;
  color: var(--ink);
}

/* Left-aligned inside a centred block: five ragged-centre lines read as a
   poem, not a list of what you get. */
.laptop__scope {
  list-style: none;
  margin: 0;
  padding: 0;
  text-align: left;
}
/* Rows with a rule between them, the way a panel of results is set, rather
   than a bulleted list on a slide. */
.laptop__scope li {
  display: grid;
  grid-template-columns: auto minmax(0, 1fr);
  align-items: center;
  gap: calc(13px / var(--s));
  padding: calc(12px / var(--s)) 0;
  border-bottom: calc(1px / var(--s)) solid var(--line);
  font-size: calc(18px / var(--s));
  line-height: 1.4;
  color: var(--ink);
}
.laptop__scope li:first-child { padding-top: 0; }
.laptop__scope li:last-child { padding-bottom: 0; border-bottom: 0; }
.laptop__scope li::before {
  content: '✓';
  display: grid;
  place-items: center;
  width: calc(21px / var(--s));
  height: calc(21px / var(--s));
  border-radius: 50%;
  background: var(--fire);
  color: #fff;
  font-size: calc(11px / var(--s));
  font-weight: 700;
  line-height: 1;
}
/* The closing line, and it gets a shot of its own: the camera comes back
   off the right-hand page to the middle of the screen, which at the
   tracking zoom is glass edge to edge again — no bezel, no machine, the
   same white page the scene opened on. So this is read at --zc, the centre
   zoom, not at rest.

   Centred on the glass with no offset of its own, and that is already the
   centre of the window: the shot it is read in is the one that centres the
   screen in the frame.

   And it is the one beat that breaks the window language deliberately. The
   panels have gone, the screen has faded to near black (driven per frame
   by lucky-draw-scenes.js), and this types itself out in light type with a
   caret. The machine reads as idle for a moment, which is what makes the
   dashboard arriving next read as it switching on. */
.laptop__note {
  position: absolute;
  inset: 0;
  padding: 0 8%;
  display: grid;
  place-content: center;
  justify-items: center;
  margin: 0;
  /* Hero scale, not footnote scale. This is the only thing on the machine
     for its whole beat, so it is set to be the last thing said rather than
     a caption under something. Ink, not white: the screen is desk white for
     the whole scene now, where it used to fade to near black for this beat
     alone and carry white text for it. */
  font-size: calc(36px / var(--zc));
  line-height: 1.5;
  letter-spacing: -0.015em;
  text-align: center;
  color: rgba(26, 26, 46, 0.72);
}
.laptop__note-text { max-width: calc(900px / var(--zc)); }
/* The one thing on the screen that looks pressable, and the pointer
   presses it: the pull-back does not begin until it has. Decorative like
   everything else on this layer — it is an <i>, not a button, so no fake
   control is hidden from assistive tech inside an aria-hidden overlay.
   A solid fire pill with white text, so it reads on the desk white. */
.laptop__cta {
  margin-top: calc(34px / var(--zc));
  display: inline-block;
  padding: calc(14px / var(--zc)) calc(30px / var(--zc));
  border-radius: 100px;
  background: var(--fire);
  color: #fff;
  font-size: calc(18px / var(--zc));
  font-style: normal;
  font-weight: 600;
  line-height: 1;
  white-space: nowrap;
  opacity: 0;
  box-shadow: 0 calc(10px / var(--zc)) calc(28px / var(--zc)) rgba(255, 107, 107, 0.32);
}
.laptop__note b { color: var(--ink); font-weight: 600; }

/* THE POINTER. Someone is using this machine: the cursor travels the glass
   between beats, presses, and the press is what brings the next panel up.
   Built by lucky-draw-scenes.js and positioned in glass percentages, so it
   belongs to the screen and travels with it when the camera moves. Sized
   against --zt because the track beats are where it does its work. */
.laptop__cursor {
  position: absolute;
  z-index: 3;
  width: calc(27px / var(--zt));
  height: calc(27px / var(--zt));
  margin: calc(-2px / var(--zt)) 0 0 calc(-3px / var(--zt));
  pointer-events: none;
  filter: drop-shadow(0 calc(3px / var(--zt)) calc(6px / var(--zt)) rgba(26, 26, 46, 0.35));
}
.laptop__cursor svg { display: block; width: 100%; height: 100%; }
/* The press. A ring struck at the pointer's tip that expands and fades,
   which is the whole reason a click reads as a click rather than a jump. */
.laptop__ripple {
  position: absolute;
  z-index: 2;
  border-radius: 50%;
  border: calc(2px / var(--zt)) solid #FF6B6B;
  pointer-events: none;
  opacity: 0;
}

/* The nav used to be forced white wherever the machine's screen filled the
   window, because that screen was near black. It is desk white for the
   whole scene now, so the ordinary dark links are the readable ones and the
   override is gone with the thing it was compensating for. The transition
   stays: --nav-fg still changes under the night driver elsewhere. */
.nav-links a { transition: color 0.35s ease; }

/* Every character gets its own box so the line can be revealed one at a
   time. Hidden with visibility, NOT display: the whole line reserves its
   space from the first frame, so the block never reflows and the centred
   text does not shuffle sideways with each keystroke. */
.laptop__ch { visibility: hidden; }
/* Sits before the first character not yet typed. Zero net width, so
   moving it through the line costs nothing in layout. */
/* Shared by both typed lines. --s is whatever zoom the line it sits in is
   read at, and the note is not inside a page so it falls back to --zc.
   currentColor, so the same caret is dark on the white card and light on
   the black screen. */
.laptop__caret {
  display: inline-block;
  width: calc(3px / var(--s, var(--zc)));
  margin-right: calc(-3px / var(--s, var(--zc)));
  height: 1em;
  vertical-align: -0.16em;
  background: currentColor;
  animation: laptop-caret 1.05s steps(1) infinite;
}
@keyframes laptop-caret { 0%, 49% { opacity: 1; } 50%, 100% { opacity: 0; } }
@media (prefers-reduced-motion: reduce) {
  .laptop__caret { display: none; }
}
@media (max-width: 900px) {
  /* No pin, no zoom: the machine renders at rest immediately, the same
     state it settles into at the end of the desktop transition, and the
     frame around the screen is gone too. A claim laid over a bare
     dashboard card, with no zoom to justify it and nothing to dissolve
     into, would just be the same sentence said twice in a row. */
  .laptop__intro { display: none; }
}

/* .datagrid is gone. #data and #data-demo used to be one section, the
   words and the machine sharing a row at 1.3fr/1fr, and .datagrid was the
   grid that held them that way.

   .datastage centres itself here too, inside .data-zoom's shared grid
   cell rather than a .datagrid column: a max-width so the laptop does not
   sprawl on an ultra-wide monitor, place-items so it stays centred without
   that max-width forcing a text-align anywhere. */
.datastage {
  position: relative;
  display: grid;
  place-items: center;
  padding: 30px 0 64px;
  max-width: 1300px;
  margin-inline: auto;
}
/* THE GRADIENT, furthest back. Blurred and inset so it reads as light in
   the air behind the cloud rather than a panel with edges. */
.datastage::before {
  content: '';
  position: absolute;
  inset: -6% -10% 2%;
  z-index: 0;
  border-radius: 50%;
  background:
    radial-gradient(58% 54% at 38% 34%, rgba(255, 189, 113, 0.62), transparent 72%),
    radial-gradient(56% 52% at 72% 56%, rgba(255, 107, 107, 0.42), transparent 74%),
    radial-gradient(74% 64% at 32% 78%, rgba(var(--tint), 0.72), transparent 76%);
  filter: blur(30px);
}
/* ── THE LAPTOP ─────────────────────────────────────────────
   laptop.png is 653x440 with a TRANSPARENT screen, so the frame is laid
   OVER the dashboard and the dashboard shows through the cut-out. That is
   what keeps the tabs clickable: a flat screenshot inside a frame would
   have killed the one interactive thing in the section.

   The aperture numbers are MEASURED from the file by flood-filling the
   transparent region, not eyeballed. It is 512x326 at (70,9):

     left   70/653  = 10.720%      width   512/653 = 78.407%
     top     9/440  =  2.045%      height  326/440 = 74.091%

   Re-measure these if the art is ever replaced, and the derived centre a
   few rules down with them: (10.720 + 78.407/2, 2.045 + 74.091/2) =
   (49.92%, 39.09%). */
/* It floats, and that is ALL it does. The pointer does not touch it.

   An earlier pass also tilted it toward the cursor, the way the work deck
   tilts its artwork. That is gone: the deck's cards are things you pick
   between, so answering the pointer tells you which one you are on, but
   this is a single object being shown to you and leaning at the mouse only
   made it restless.

   The float uses `translate` and `rotate` rather than `transform`. Nothing
   competes for those properties now, but keeping them separate is what
   lets a transform be added later without silently erasing the drift. */
@keyframes laptop-float {
  0%, 100% { translate: 0 0;      rotate: 0deg; }
  50%      { translate: 0 -14px;  rotate: 0.55deg; }
}
.laptop {
  position: relative;
  z-index: 2;
  /* 1080, not the 820 this carried when it was sharing a row with a
     text column at 1.3fr of it. Given the whole band, min(1080px, 92vw)
     is close to as large as it can go before the floating drop-shadow
     starts pressing on the band's own side padding. */
  width: min(1080px, 92vw);
  aspect-ratio: 653 / 440;
  animation: laptop-float 8.5s ease-in-out infinite;
  will-change: translate;
  /* Anchors the #data-demo zoom's `scale` on the SCREEN's centre (see the
     aperture comment on .laptop__screen below) rather than the whole
     element's default 50% 50%. Centred on the whole laptop, growing the
     scale just makes a bigger laptop: the bezel is still there at every
     size, only closer. Centred on the screen, growing the scale expands
     the screen outward from where it already is and pushes the bezel and
     the keyboard away from that same point in every direction, which is
     what lets the start of the zoom read as a close-up ON THE SCREEN
     rather than a close-up of the device it lives in. */
  transform-origin: 49.92% 39.09%;
  /* No negative lap any more: it sat on a DRAWN cloud that is gone. The
     machine now stands on the page's own cloud layer. */
  margin-bottom: 0;
}
/* Used for one synchronous frame at a time, by measureDataZoom, so the
   machine can be measured without the float's translate and rotate baked
   into the rect. NOT a per-frame state: taking the promotion away while
   the scene scrubs costs a full re-raster of the laptop every frame, and
   at these zooms that meant resampling the artwork to ~4600px wide each
   time. Measured at 1400px it took the median frame from 33ms to 117ms
   and the 95th from 67ms to 700ms. */
.laptop.is-measuring {
  animation: none;
  will-change: auto;
}

/* On top, and inert. Without pointer-events: none the bezel would sit over
   the dashboard and eat every click meant for a tab. */
.laptop__frame {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  z-index: 2;
  pointer-events: none;
  user-select: none;
  /* drop-shadow, not box-shadow: the art is an irregular silhouette on a
     transparent field, and a box-shadow would print the rectangle it is
     delivered in rather than the machine. Stacked the way the deck stacks
     its three, so the lift reads at both close and far range. */
  filter:
    drop-shadow(0 2px 4px rgba(26, 26, 46, 0.16))
    drop-shadow(0 14px 26px rgba(26, 26, 46, 0.18))
    drop-shadow(0 44px 70px rgba(26, 26, 46, 0.14));
}
@media (prefers-reduced-motion: reduce) {
  .laptop { animation: none; transition: none; }
}
.laptop__screen {
  position: absolute;
  left: 10.720%;
  top: 2.045%;
  width: 78.407%;
  height: 74.091%;
  z-index: 1;
  overflow: auto;
  /* NOT overscroll-behavior: contain, which would trap the reader's scroll
     inside the dashboard on the way down the page. */
  background: var(--card);
}
/* Laid out AT the aperture width, not scaled down to it.

   The earlier version held the dashboard at its 1120px design width and
   scaled it by 0.396, on the assumption that narrowing it would collapse
   the four-column KPI rows. Measured, that assumption was wrong: the grid
   stays four across all the way down to 560px and the tab row never wraps.
   So reflowing costs nothing and scaling cost everything — it put the body
   labels at about 5px, which is a picture of a dashboard nobody can read.
   Reflowed, they render at their real 10.24px. */
.laptop .dash {
  width: 100%;
  min-height: 100%;
  border: 0;
  border-radius: 0;
  box-shadow: none;
  margin-bottom: 0;
}

/* ── THE HINT · tells the reader the laptop answers to a click ──────
   Sits above the machine, in the page's own chrome, not inside the
   mockup: put it on the screen itself and it reads as one more piece of
   sample data, which is the opposite of what it is there to say.

   The dash-tabs row (Overview / Daily / Users / National footprint) is
   the section's one real control, per the note on .laptop__screen above,
   and this is what tells a reader it exists before they scroll past it
   without trying it. */
.datastage__hint {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  margin: 0 0 22px;
  padding: 9px 18px 9px 14px;
  border-radius: 100px;
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: 0 10px 24px rgba(26, 26, 46, 0.07);
  font-size: 0.86rem;
  font-weight: 500;
  color: var(--ink);
}
/* A simple cursor-arrow silhouette, not a hand or a finger: it reads at
   15px where a more detailed glyph would just be a smudge. */
.datastage__hint-icon {
  width: 15px;
  height: 15px;
  flex-shrink: 0;
  fill: currentColor;
  color: var(--red);
  animation: hint-nudge 2.6s ease-in-out infinite;
}
/* A little click, not a wander: two short moves and a long hold, so it
   reads as a nudge that keeps happening rather than motion that never
   stops. Holding position and scale at 0% AND 100% is what lets the
   infinite loop repeat without a jump on the wrap. */
@keyframes hint-nudge {
  0%, 18%, 100% { translate: 0 0; scale: 1; }
  7%  { translate: -2px 2px; scale: 0.9; }
  12% { translate: 0 0;      scale: 1.05; }
}
@media (prefers-reduced-motion: reduce) {
  .datastage__hint-icon { animation: none; }
}
@media (max-width: 900px) {
  .datastage__hint { font-size: 0.8rem; padding: 8px 14px 8px 11px; margin-bottom: 18px; }
}

/* #data is a plain .shell now: no more .datatext hook to neutralise its
   centring, because there is no grid column deciding the width instead
   of it any more. Ordinary .sec-head / .scope-grid spacing applies
   unmodified, the same as on every other text-only band on the page. */
/* Under 900 the frame is more bezel than screen, so it goes and the
   dashboard is a plain card again, which is what it was before. */
@media (max-width: 900px) {
  .datastage { padding: 0; }
  .datastage::before { display: none; }
  .laptop {
    width: 100%;
    aspect-ratio: auto;
    margin: 0 0 30px;
  }
  .laptop__frame { display: none; }
  .laptop__screen {
    position: static;
    width: 100%;
    height: auto;
    overflow: visible;
    border-radius: 22px;
  }
  .laptop .dash {
    width: 100%;
    min-height: 0;
    border: 1px solid var(--line);
    border-radius: 22px;
    box-shadow: 0 14px 40px rgba(26,26,46,0.07);
  }
}

.dash {
  border: 0;
  border-radius: 0;
  box-shadow: none;
  margin-bottom: 0;
  min-height: 100%;
}

/* Under 900 the frame is more bezel than screen, so it goes and the
   dashboard is a plain card again, which is what it was before. */
@media (max-width: 900px) {
  .laptop {
    --lap: 100%;
    width: 100%;
    margin: 0 0 30px;
    aspect-ratio: auto;
  }
  .laptop__frame { display: none; }
  .laptop__screen {
    position: static;
    width: 100%;
    height: auto;
    overflow: visible;
    border-radius: 22px;
  }
  .laptop .dash {
    border: 1px solid var(--line);
    border-radius: 22px;
    box-shadow: 0 14px 40px rgba(26,26,46,0.07);
    min-height: 0;
  }
}

.dash {
  border: 0;
  border-radius: 0;
  box-shadow: none;
  margin-bottom: 0;
  min-height: 100%;
}

/* Under 900 the frame is more bezel than screen, so it goes and the
   dashboard is a plain card again, which is what it was before. */
@media (max-width: 900px) {
  /* The dashboard's day strip is a no-wrap row that scrolls sideways, and
     a block's min-content ignores its own overflow, so the strip handed the
     whole card a 624px floor: .datastage is a flex item, its automatic
     minimum is that min-content, and on a 390px phone the laptop sat 120px
     past both edges of the window. Zeroing the floor lets the card take
     the window's width and the strip scroll inside it as it was built to. */
  .datastage { width: 100%; min-width: 0; }
  .laptop, .laptop__screen, .laptop .dash { min-width: 0; max-width: 100%; }
  .laptop { --lap: 100%; width: 100%; margin: 0 0 30px; }
  .laptop__lid { background: none; padding: 0; box-shadow: none; }
  .laptop__lid::before, .laptop__base { display: none; }
  .laptop__screen { aspect-ratio: auto; overflow: visible; border-radius: 22px; }
  .laptop .dash {
    border: 1px solid var(--line);
    border-radius: 22px;
    box-shadow: 0 14px 40px rgba(26,26,46,0.07);
    min-height: 0;
  }
}

@media (max-width: 640px) {
  /* Four KPIs across a 343px card are 85px each, which wraps every label
     and jams the deltas against the numbers. Two by two, with the row
     rule the four-across border layout did not need. */
  .dash__kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash__kpi:nth-child(2n) { border-right: none; }
  .dash__kpi:nth-child(-n+2) { border-bottom: 1px solid var(--line); }
  .dash__view { padding: 14px 14px 16px; }
  .dash__bar, .dash__tabs { padding-inline: 14px; }
  .dash__chips-row { grid-template-columns: 1fr; }
}

.dash {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 22px;
  overflow: hidden;
  box-shadow: 0 14px 40px rgba(26,26,46,0.07);
  margin-bottom: 30px;
  text-align: left;
}
.dash__bar {
  display: flex; align-items: center; gap: 10px;
  padding: 14px 20px; border-bottom: 1px solid var(--line);
  background: rgba(26,26,46,0.02);
}
.dash__live {
  width: 8px; height: 8px; border-radius: 50%; background: #10B981;
  flex: 0 0 auto; box-shadow: 0 0 0 3px rgba(16,185,129,0.16);
}
.dash__brand { font-weight: 700; font-size: 0.95rem; letter-spacing: -0.01em; }
.dash__range { font-size: 0.78rem; color: var(--faint); }
.dash__badge {
  margin-left: auto; font-size: 0.62rem; font-weight: 600;
  letter-spacing: 0.12em; text-transform: uppercase; color: var(--muted);
  background: rgba(26,26,46,0.06); padding: 4px 10px; border-radius: 100px;
}
.dash__tabs {
  margin: 0; padding: 12px 20px; gap: 6px;
  border-bottom: 1px solid var(--line);
}
.dash__tabs .chip { font-size: 0.78rem; padding: 6px 13px; }
.dash__view { padding: 18px 20px 20px; }
.dash__view[hidden] { display: none; }

/* KPI row */
.dash__kpis {
  display: grid; grid-template-columns: repeat(4, minmax(0, 1fr));
  border: 1px solid var(--line); border-radius: 14px; overflow: hidden;
  margin-bottom: 14px;
}
.dash__kpis--soft { background: rgba(26,26,46,0.02); }
.dash__kpi { padding: 14px 16px; border-right: 1px solid var(--line); }
.dash__kpi:last-child { border-right: none; }
.dash__kpi p {
  font-size: 0.64rem; font-weight: 600; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--faint); margin-bottom: 7px;
}
.dash__kpi b {
  display: block; font-size: 1.45rem; font-weight: 800;
  letter-spacing: -0.03em; font-variant-numeric: tabular-nums; margin-bottom: 6px;
}
.dash__delta {
  display: inline-block; font-size: 0.66rem; font-weight: 700;
  padding: 2px 7px; border-radius: 6px;
}
.dash__delta--up { color: #047857; background: rgba(16,185,129,0.12); }
.dash__delta--down { color: #B91C1C; background: rgba(220,38,38,0.10); }
.dash__hint { font-size: 0.68rem; color: var(--faint); }

/* panels */
.dash__panel {
  border: 1px solid var(--line); border-radius: 14px;
  padding: 16px; margin-bottom: 14px;
}
.dash__panel:last-child { margin-bottom: 0; }
.dash__panel-head {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; flex-wrap: wrap; margin-bottom: 12px;
}
.dash__panel-head b { font-size: 0.9rem; letter-spacing: -0.01em; }
.dash__panel-head span { font-size: 0.74rem; color: var(--faint); }
.dash__pair { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
.dash__pair .dash__panel { margin-bottom: 14px; }
.dash__foot { font-size: 0.72rem; color: var(--faint); margin-top: 10px; }

/* pace chips */
.dash__chips-row { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; }
.dash__chip { background: rgba(26,26,46,0.03); border-radius: 12px; padding: 12px 14px; }
.dash__chip p {
  font-size: 0.62rem; font-weight: 600; letter-spacing: 0.1em;
  text-transform: uppercase; color: var(--faint); margin-bottom: 5px;
}
.dash__chip b { font-size: 1.3rem; font-weight: 800; font-variant-numeric: tabular-nums; }

/* trend chart */
.dash__chart { overflow-x: auto; -webkit-overflow-scrolling: touch; }
.dash__svg { width: 100%; height: auto; display: block; overflow: visible; }
.dash__grid { stroke: rgba(26,26,46,0.09); stroke-width: 1; stroke-dasharray: 3 3; }
.dash__ax { font-size: 9px; fill: var(--faint); font-variant-numeric: tabular-nums; }
.dash__area { fill: url(#dashRev); }
.dash__line { fill: none; stroke-width: 2.4; stroke-linejoin: round; stroke-linecap: round; }
.dash__line--rev { stroke: #F59E0B; }
.dash__line--scans { stroke: #10B981; }
.dash__dot-rev { fill: #F59E0B; }
.dash__dot-scans { fill: #10B981; }
.dash__legend { display: inline-flex; align-items: center; gap: 6px; font-size: 0.72rem; color: var(--muted); }
.dash__key { width: 15px; height: 3px; border-radius: 2px; display: inline-block; }
.dash__key--rev { background: #F59E0B; }
.dash__key--scans { background: #10B981; margin-left: 8px; }

/* daily */
.dash__days { display: flex; gap: 6px; overflow-x: auto; padding-bottom: 6px; margin-bottom: 14px; }
.dash__day {
  flex: 0 0 auto; font-family: inherit; font-size: 0.74rem; font-weight: 500;
  padding: 7px 13px; border-radius: 100px; white-space: nowrap;
  background: transparent; color: var(--muted); border: 1px solid var(--line);
}
.dash__day i { font-style: normal; color: var(--faint); margin-left: 5px; font-size: 0.68rem; }
.dash__day.is-on { background: var(--fire); color: #fff; border-color: transparent; font-weight: 600; }
.dash__day.is-on i { color: rgba(255,255,255,0.82); }
.dash__stats { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 10px; }
.dash__stat { border: 1px solid var(--line); border-radius: 12px; padding: 12px 14px; }
.dash__stat b {
  font-size: 1.25rem; font-weight: 800; font-variant-numeric: tabular-nums;
  letter-spacing: -0.02em; margin-right: 7px;
}
.dash__stat p {
  font-size: 0.62rem; font-weight: 600; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--faint); margin-top: 7px;
}

/* time of day */
.dash__tod { list-style: none; display: flex; flex-direction: column; gap: 10px; }
.dash__tod li { display: grid; grid-template-columns: 108px 1fr 28px; align-items: center; gap: 10px; }
.dash__tod-l b { display: block; font-size: 0.82rem; font-weight: 600; }
.dash__tod-l i { font-style: normal; font-size: 0.66rem; color: var(--faint); }
.dash__tod-bar { height: 12px; border-radius: 100px; background: rgba(26,26,46,0.06); overflow: hidden; }
.dash__tod-bar span { display: block; height: 100%; border-radius: 100px; background: var(--fire); }
.dash__tod-n { font-size: 0.82rem; font-variant-numeric: tabular-nums; text-align: right; color: var(--muted); }

/* tables */
.dash__table { width: 100%; border-collapse: collapse; font-size: 0.8rem; }
.dash__table th, .dash__table td { padding: 8px 6px; text-align: right; }
.dash__table thead th {
  font-size: 0.62rem; font-weight: 600; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--faint);
  border-bottom: 1px solid var(--line);
}
.dash__table tbody th {
  text-align: left; font-weight: 600;
}
.dash__table thead th:first-child { text-align: left; }
.dash__table tbody tr + tr th, .dash__table tbody tr + tr td { border-top: 1px solid var(--line); }
.dash__table td { font-variant-numeric: tabular-nums; color: var(--muted); }
.dash__table tbody th i { display: block; font-style: normal; font-size: 0.66rem; font-weight: 400; color: var(--faint); }
.dash__table--wide { min-width: 460px; }
/* The six column table is wider than a phone panel, and without a
   scroller on its own box the last columns were simply unreachable. The
   page itself still never scrolls sideways. :has() is already how
   .img-slot detects a filled slot in this file. */
.dash__panel:has(.dash__table--wide) { overflow-x: auto; -webkit-overflow-scrolling: touch; }

/* users */
.dash__donut-wrap { display: flex; align-items: center; gap: 18px; flex-wrap: wrap; }
.dash__donut { width: 150px; height: 150px; flex: 0 0 auto; transform: rotate(-90deg); }
.dash__ring { fill: none; stroke-width: 22; }
.dash__donut-n {
  font-size: 20px; font-weight: 800; fill: var(--ink);
  transform: rotate(90deg); transform-origin: 90px 90px;
  font-variant-numeric: tabular-nums;
}
.dash__donut-l {
  font-size: 8px; font-weight: 600; letter-spacing: 0.14em; fill: var(--faint);
  transform: rotate(90deg); transform-origin: 90px 90px;
}
.dash__legend-list { list-style: none; display: flex; flex-direction: column; gap: 9px; flex: 1 1 150px; }
.dash__legend-list li { display: flex; align-items: center; gap: 8px; font-size: 0.8rem; }
.dash__legend-list i { width: 10px; height: 10px; border-radius: 50%; flex: 0 0 auto; }
.dash__legend-list b { font-weight: 500; }
.dash__legend-list span { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--muted); }
.dash__legend-list em { font-style: normal; font-size: 0.74rem; color: var(--faint); width: 40px; text-align: right; }
.dash__ages { list-style: none; display: flex; flex-direction: column; gap: 7px; }
.dash__ages li { display: grid; grid-template-columns: 46px 1fr 44px 34px; align-items: center; gap: 9px; }
.dash__age-l { font-size: 0.76rem; color: var(--muted); font-variant-numeric: tabular-nums; }
.dash__age-bar { height: 11px; border-radius: 100px; background: rgba(26,26,46,0.06); overflow: hidden; }
.dash__age-bar span { display: block; height: 100%; border-radius: 100px; background: var(--fire); }
.dash__age-n { font-size: 0.76rem; text-align: right; font-variant-numeric: tabular-nums; }
.dash__age-p { font-size: 0.72rem; text-align: right; color: var(--faint); font-variant-numeric: tabular-nums; }
.dash__heat { width: 100%; height: auto; display: block; overflow: visible; min-width: 380px; }

/* footprint */
.dash__chains { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 14px; }
.dash__chains-l { font-size: 0.78rem; font-weight: 600; margin-right: 4px; }
.dash__chains-l i { font-style: normal; font-weight: 400; color: var(--faint); }
.dash__chain {
  display: inline-flex; align-items: center; gap: 7px;
  font-size: 0.76rem; color: var(--muted);
  border: 1px solid var(--line); border-radius: 100px; padding: 5px 12px;
}
.dash__chain i { width: 8px; height: 8px; border-radius: 50%; }
.dash__chain b { color: var(--ink); font-variant-numeric: tabular-nums; }
.dash__geo { display: grid; grid-template-columns: minmax(0, 1fr) minmax(0, 1.15fr); gap: 14px; align-items: start; }
.dash__map {
  border: 1px solid var(--line); border-radius: 14px; padding: 10px;
  background: rgba(26,26,46,0.015);
}
.dash__map svg { width: 100%; height: auto; display: block; max-height: 420px; }
.dash__land { fill: #EDEEF2; stroke: rgba(26,26,46,0.14); stroke-width: 0.5; }
.dash__pin { fill-opacity: 0.78; stroke: #fff; stroke-width: 1.2; }
.dash__note { margin-top: 26px; }

@media (max-width: 900px) {
  .dash__kpis { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash__kpi:nth-child(2) { border-right: none; }
  .dash__kpi:nth-child(-n+2) { border-bottom: 1px solid var(--line); }
  .dash__stats { grid-template-columns: repeat(2, minmax(0, 1fr)); }
  .dash__pair, .dash__geo { grid-template-columns: 1fr; }
  .dash__chips-row { grid-template-columns: 1fr; }
}
@media (max-width: 620px) {
  .dash__view { padding: 14px; }
  .dash__bar { flex-wrap: wrap; }
  .dash__badge { margin-left: 0; }
  .dash__svg { min-width: 560px; }
  .dash__tod li { grid-template-columns: 92px 1fr 24px; }
  .dash__ages li { grid-template-columns: 42px 1fr 40px 30px; }
  .dash__table { font-size: 0.74rem; }
}

/* ── S3 five vendors collapse into one team ──────────────── */
.collapse{position:relative;width:100%;max-width:900px;height:min(70vh,520px)}
.vend{
  position:absolute;left:50%;top:50%;
  background:var(--card);border:1px solid var(--line);border-radius:100px;
  padding:11px 22px;font-size:.86rem;color:var(--muted);white-space:nowrap;
  box-shadow:0 8px 26px rgba(26,26,46,.07);will-change:transform,opacity}
.one-team{
  position:absolute;left:50%;top:50%;transform:translate(-50%,-50%) scale(.9);
  opacity:0;background:var(--card);border:1px solid var(--line);border-radius:26px;
  padding:34px 42px;box-shadow:0 26px 70px rgba(255,107,107,.16);
  min-width:min(84vw,380px)}
.one-team b{display:block;font-size:clamp(1.3rem,3vw,2rem);font-weight:700;
  letter-spacing:-.02em;margin-bottom:6px}
.one-team span{font-size:.86rem;color:var(--muted)}
.collapse-cap{
  position:absolute;left:50%;bottom:-6px;transform:translateX(-50%);
  font-size:.82rem;color:var(--faint);opacity:0;transition:opacity .5s ease;white-space:nowrap}
.collapse-cap.show{opacity:1}

/* ── S4 · WHAT IT USUALLY RUNS ON ─────────────────────────
   Names the two tools brands actually use today and shows, row by
   row, what each cannot do. Rows fade in on scroll.
   ────────────────────────────────────────────────────────── */
.vs-stage { width: 100%; max-width: 1000px; text-align: left; }
.vs-head {
  font-size: clamp(1.5rem, 3.2vw, 2.5rem);
  line-height: 1.2; letter-spacing: -0.025em; font-weight: 700;
  margin-bottom: 26px;
}
.vs-head em { font-style: normal; font-weight: 800; }

/* Each row is its own card rather than a band between two hairlines, so a
   capability reads as one object you can take in whole. border-collapse
   has to be separate for that: collapsed borders cannot carry a per-cell
   background with rounded ends. */
/* The sideways drag is for the 640px minimum width; there is nothing to
   scroll vertically. It has to be said out loud, because overflow-x on its
   own leaves overflow-y computing to `auto`, and the table's row spacing
   rounds a few sub-pixels past its own box — enough for a scrollbar to
   appear down the side of a table that is entirely visible, which reads as
   content being withheld. */
.vs-wrap { overflow-x: auto; overflow-y: hidden; }
.vs-table {
  border-collapse: separate; border-spacing: 0 8px;
  width: 100%; min-width: 640px; font-size: 0.88rem;
}
.vs-table thead th {
  font-size: 0.72rem; letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--faint); font-weight: 600; padding: 0 12px 4px; text-align: left;
  white-space: nowrap;
}
.vs-table thead th:first-child { padding-left: 18px; }
.vs-table thead th.vs-mine { color: var(--red); }
.vs-table tbody tr {
  opacity: 0; transform: translateY(14px);
  transition: opacity .5s ease, transform .5s ease;
}
.vs-table tbody tr.in { opacity: 1; transform: none; }
/* The card is painted per cell, because a tr cannot take a background and
   a border-radius that its cells will respect. */
.vs-table tbody th, .vs-table tbody td {
  background: var(--card);
  padding: 15px 12px;
  vertical-align: middle;
  transition: background .3s ease;
}
.vs-table tbody th[scope=row] {
  text-align: left; font-weight: 600; color: var(--ink);
  width: 34%; padding-left: 18px;
  border-radius: 14px 0 0 14px;
}
.vs-table tbody td { color: var(--faint); }
.vs-table tbody td:last-child { border-radius: 0 14px 14px 0; padding-right: 18px; }
.vs-no span[aria-hidden] { color: rgba(26,26,46,0.28); font-weight: 600; }
/* Scoped to beat the card rule above. `.vs-part` alone is one class, the
   card rule is a class plus two element names, so the bare selectors were
   losing their colour and the winning cell was losing its tint. */
.vs-table tbody td.vs-part { color: var(--muted); }
/* The tint sits on top of the card rather than replacing it, so the
   winning cell still reads as part of the same row. */
.vs-table tbody td.vs-yes {
  color: var(--ink); font-weight: 500;
  background:
    linear-gradient(90deg, rgba(255,107,107,0.09), rgba(255,189,113,0.09)),
    var(--card);
}
.vs-yes span[aria-hidden] { color: var(--red); font-weight: 700; margin-right: 4px; }
.vs-sr {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* ══════════════════════════════════════════════════════════
   THE TWO CARS
   The one-line version of the comparison: same budget, one of
   them wins. Two beats, both scrubbed from the scroll — the
   slow car crawls and stops short, then the price line lands
   and the fast one goes past it and over the flag.

   It shares no CSS with the race section below, on purpose.
   The race is a five-checkpoint instrument; this is a single
   sentence with a picture, and coupling them would drag the
   race's card, leader and lane machinery into something that
   needs none of it.
   ══════════════════════════════════════════════════════════ */
.scene--cars, #facts { --cars-red: #A32B2B; }

/* ── THE SECTION IS THE FILM'S OWN GROUND ──────────────────
   The film is rendered on a flat #d3d3d3 field and that grey is
   now the section, so the video has no edge: no card, no rim,
   no shadow, just the printer standing on the page.

   It works because the cloud canvas sits at z-index -2 — above
   body::before and below every block background — so an opaque
   background on the section paints straight over it. The
   scalloped opening is gone as well: [data-cloud-plain] on the
   section (index.html) tells collectGuards() in
   lucky-draw-clouds.js to cut no island here, so there is no
   rim to bleed lobes into the sections either side. #cars keeps
   its --tint further down this file but nothing reads it now. */
/* .scene--feed, NOT .scene--cars: th/lucky-draw still runs the two-car
   version off this same stylesheet, and painting #cars grey there put
   grey cars on a grey ground and took away the peach cloud island the
   section is designed around. The modifier is only on the ported page. */
/* THE PAINT SECTION'S OWN GROUND. #FFFBEE, the butter that .scene--why
   directly above declares — sampled off the live page rather than read
   off the stylesheet, because #whystack is the other section the paint
   passes through and it has no ground of its own at all (it shows the
   body's #F6F7F9 straight through).

   Matching it means #why and this section become one continuous field
   with no seam between them, which is the point.

   It has been three different colours: #D3D3D3 first, which was never a
   design choice at all — it was sampled off the source video so the
   video would sit on it seamlessly, and the video is long gone. Then a
   warm stone. This is warmer and lighter than either, so the paper
   needs its shadow to exist at all: white on this is 1.04:1. The
   machine gains from it — the lid reads 6.56:1 against this ground and
   the base 8.30:1, against 4.54 and 5.74 on the original grey. */
.scene--feed {
  /* One printed line. On the SECTION, not on .feed__strip: the ink
     band that develops the type is a pseudo-element of .feed__paper,
     which is the strip's PARENT, and custom properties only inherit
     downward. Declared on the strip it was invisible to the band, the
     calc was invalid, the declaration dropped, and the band rendered
     at zero height. Declared as an absolute length rather than 1.5em
     so children with their own font-size land on the same grid. */
  --grid: calc(var(--rig) * 0.0185 * 1.5);
  --feed-ground: #FFFBEE;
  --feed-ground-0: rgba(255,251,238,0);
  background: var(--feed-ground);
}

/* ── THE NUMBERS THIS SECTION IS BUILT FROM ────────────────
   On the SECTION, not on .feed. Custom properties inherit downward
   only, and .sticky-frame is .feed's PARENT — declaring --rig and
   --feed-window on .feed left the frame unable to see either, its
   padding calc invalid, and the whole placement silently dropped back
   to the centred default. */
.scene--feed {
  /* Three limits, whichever binds first. 624px is as large as
     the machine wants to be drawn; 88vw keeps it off the gutters
     on a phone; 60vh is the one that binds on a short laptop.
     TWENTY PERCENT UP ON 520, AND THAT IS HOW THE TYPE GOT BIGGER. Every
     size on the receipt is a fraction of this one length — the strip's
     font-size is 0.0185 rigs, --grid is 1.5 of that, the line padding is
     0.016 — so this is the only place that moves all of them at once and
     keeps them in proportion.
     Scaling the type against the paper instead was the obvious idea and it
     does not work: 0.0185 is a CEILING, set so the longest line on the
     receipt (38 characters) just fits the paper's width, and 20% past it
     wraps that line and breaks the grid the whole scene is built on.
     Widening the paper to match is not available either — its width is
     traced from the machine's own slot.
     So the machine is drawn 20% larger too. It is one drawing and the
     type is sized from it; they cannot move apart. On a short window 60vh
     still binds first, so nothing here can push the composition off a
     laptop screen. */
  --rig: min(624px, 88vw, 60vh);

  /* HOW MUCH PAPER MAY BE ON SCREEN AT ONCE. The receipt is the
     video's full one, about 75 lines, and it was never going to
     fit: it is roughly two and a half viewports long. So the
     paper gets a window, and once the printed length outgrows
     the window the strip inside slides up and the earlier lines
     travel off the top — which is exactly what the source does
     from its eighth second onward.

     The number is what is left of the viewport once the machine
     has taken its share. The machine stands 0.818 rigs tall and
     its slot is 0.164 rigs below its top, so it occupies 0.653
     rigs below the paper; 86vh rather than 100 keeps the section
     off both edges. */
  /* How far the camera comes in. A plain number, so the driver can read
     it back with getPropertyValue and there is one source of truth for
     it — a length here would come back as its unresolved calc() text.

     Raising this does NOT push the receipt into the nav: the driver
     divides the window by the live zoom, so the band the paper covers
     is the same at any zoom and what changes is the size of the type.
     This number is the "how close do we get" dial and nothing else has
     to move with it. */
  --zoom-to: 2.8;

  /* HOW MUCH RECEIPT IS ON SCREEN, and this is a SCREEN height, not a
     paper height. The driver divides it by whatever the zoom currently
     is, so the paper covers the same band of the viewport from the first
     frame to the last and the push-in only changes the size of the type.

     It used to be a paper height divided by the FINAL zoom, which meant
     the band only reached its intended size at the very end — early in
     the section the receipt was less than half this tall with a screen
     of empty grey above it.

     Where the whole composition sits is solved together with it, so the
     top of the receipt lands just clear of the nav. The nav has no
     background of its own, so a receipt that reaches it gets the links
     printed across the paper. Two constraints, two unknowns:

       slot     = X + W + 0.1646R     X is the offset below; 0.1646 rigs
                                      below the rig's top is the slot
       paperTop = slot - W            the band is W tall, ending at the slot

       want slot = 0.66V and paperTop = 88 (clear of the nav)
       =>  W = 0.66V - 88   and   X = 0.66V - 0.1646R - W

     Centring the box instead — which is what .sticky-frame does for
     every other scene — leaves X as whatever falls out of the box's own
     height, and since the window is IN that height the two fight: a
     bigger window pushes the slot down, and the slot is the very point
     the zoom is anchored to. Hence placing it rather than centring it.

     0.66 rather than 0.70: the machine hangs 0.799 of its own height
     below the slot, and much past this its feet leave the frame before
     the push-in has even started. */
  --feed-window: max(150px, calc(0.66 * 100vh - 88px));
}
.cars-stage { width: 100%; max-width: 1120px; text-align: left; }
.cars-head {
  font-size: clamp(1.5rem, 3.2vw, 2.5rem);
  line-height: 1.18; letter-spacing: -0.025em; font-weight: 700;
  margin-bottom: 5vh;
}
.cars-head em { font-style: normal; font-weight: 800; }

/* A road each, and one flag across both. One shared road was
   the first idea and the wrong one twice over: the two cars
   land on the same stretch of it during the overtake and sit
   on top of each other, and the second car's label has nowhere
   to go that is not the road itself. Two roads to one flag
   still says "same distance, same finish", which is the part
   that has to survive. */
.cars-strip {
  --reserve: 120px;
  position: relative;
  padding-right: var(--reserve);
  min-height: 296px;
}
/* The flag sits at the end of the road and behind the cars, so
   a car crosses in front of it rather than under it.
   Anchored by its LEFT edge. `right: var(--reserve)` put its
   right edge where the road ends, which hung the whole 14px of
   it back over the lane and painted out the last word of the
   outcome line underneath. */
.cars-flag {
  position: absolute; left: calc(100% - var(--reserve)); top: 0; bottom: 0;
  width: 14px; border-radius: 3px;
  background:
    conic-gradient(var(--ink) 0 25%, #fff 0 50%, var(--ink) 0 75%, #fff 0) 0 0 / 14px 14px;
  box-shadow: 0 3px 14px rgba(26,26,46,0.18);
}

/* ── THE FEED ─────────────────────────────────────────────
   Where the video was. Everything is expressed against ONE
   length, --rig, which is the printer's drawn width: the paper,
   the type on it and the travel above it are all fixed fractions
   of that, so the whole picture scales as one object and reads
   identically on a phone and a 27". Nothing here is sized in
   absolute px except that single number.

   The fractions are not chosen, they are measured off the source
   video's own frame: the printer box is 626x512 of its 1920x1080,
   the paper is 298 of those 626 units wide (47.604%) starting at
   164 (26.198%), and its bottom edge sits at 103 units down —
   409 of 512 up from the foot, hence 79.883%. Those five numbers
   are the entire registration between the SVG and the paper. */
.feed {
  width: var(--rig);
  margin: 0 auto;
  /* THE PUSH IN. The scene starts on the whole machine and ends close on
     the paper, so the type roughly half again its size by the end and the
     receipt is actually readable. Scaled rather than re-laid-out: growing
     --rig itself would change the strip's height, which would change the
     length of paper the scroll maps onto, and the pacing with it.

     The origin is the SLOT, so the read line does not move while the
     camera comes in: the machine grows down out of frame and the paper
     grows up, around a fixed point.

     0.1646 rigs below the top of the machine, NOT 0.6536. The paper's
     bottom is 79.883% UP from the machine's foot, so it is the
     complement — 0.20117 of the machine's height, and the machine is
     itself 0.818 rigs, giving 0.1646. Using the uncomplemented figure
     put the anchor below the machine's middle and the zoom drove the
     whole composition upward off the top of the screen. */
  /* IT SLIDES IN. The machine used to stand in the frame from the section's
     first pixel, alongside the question — see updateFeed(), which now walks
     --feed-in from a screen below up to 0 once the question has left. The
     translate is written OUTSIDE the scale on purpose: transforms apply
     right to left, so the composition is scaled about the slot below and
     only then carried, which keeps the push-in anchored where it was while
     the whole thing travels.
     .sticky-frame is overflow: clip, so a screen of translate is genuinely
     hidden rather than hanging over the section beneath. */
  transform: translateY(var(--feed-in, 0px)) scale(var(--zoom, 1));
  transform-origin: 50% calc(var(--feed-window) + 0.1646 * var(--rig));
  /* Room above the machine for the paper, so the MACHINE does not
     move while it prints. The paper is absolutely positioned (it
     has to be — an in-flow paper would resize this box on every
     frame and slide the printer down the screen as it fed), so
     the space it needs is reserved here instead. It is the window
     now, not the strip's height, which is why this is a real
     length and no longer a percentage that JS has to correct. */
  padding-top: var(--feed-window);
}
/* Placed, not centred — see the window derivation above. */
.scene--feed .sticky-frame {
  align-items: flex-start;
  padding-top: max(0px, calc(0.66 * 100vh - 0.1646 * var(--rig) - var(--feed-window)));
}
.feed__rig {
  position: relative;
  width: 100%;
  aspect-ratio: 626 / 512;
  overflow: visible;
}
.feed__machine { position: absolute; inset: 0; width: 100%; height: 100%; }

/* Sampled from frame 0 of the source, not picked by eye. */
.feed__lid   { fill: #5B5B5B; }
.feed__base  { fill: #4C4C4C; }
.feed__tray  { fill: #444444; }
.feed__slot  { fill: #2C2E2F; }
.feed__panel { fill: #2D2D2D; }
.feed__lamp  { fill: #7C0005; }
.feed__knob  { fill: #2F2F2F; }
.feed__foot  { fill: #2D2D2D; }

/* THE RISER. The straight run out of the machine, and a clip: the
   receipt is one continuous strip that simply translates up through it,
   so the type is dead upright and never drifts. Its height grows with
   the paper until it reaches the top of the run, and then holds.

   Per-line placement was tried here and is the wrong tool. Hiding the
   lines that had passed the top took them out of FLOW, which collapsed
   every remaining line's natural position, so the offsets measured up
   front no longer described where anything was — and since the driver's
   own arithmetic reduces to translateY(-fed) for a line in its natural
   place, every visible line ended up with an identical transform and the
   whole strip sat one window too high. One translate on the parent is
   both correct and two orders of magnitude cheaper. */
.feed__paper {
  position: absolute;
  left: 50%;
  width: calc(var(--rig) * 0.47604);
  bottom: 79.883%;
  height: 0;
  overflow: hidden;
  z-index: 2;
  transform: translateX(-50%);
  /* White paper on this ground is 1.04:1 — the receipt has no edge of
     its own at all, so the shadow is not decoration, it is the only
     thing separating the paper from the section. Three parts: a tight
     one for the edge, a wide soft one for the lift, and a hairline ring
     so the boundary survives even where the soft shadow is thinnest.
     It was missing entirely until now — it went when the paper briefly
     became a path and never came back — which worked only because the
     ground happened to be dark enough to show the paper's edge. */
  box-shadow: 0 14px 30px -10px rgba(58,44,20,0.30),
              0 3px 8px -3px rgba(58,44,20,0.24),
              0 0 0 1px rgba(58,44,20,0.07);
  /* ROUNDED AT THE LEADING EDGE. The two top corners only, because they
     are the only ones the reader ever sees as corners: the bottom of the
     paper is inside the printer's mouth for the whole run.

     Off --rig rather than a fixed pixel value, so it holds its proportion
     to the paper's own width at every size the rig is clamped to - and,
     more to the point, so the whole feed's zoom scales it with everything
     else instead of leaving a corner that grows blunter as the paper gets
     bigger. The clip and the hairline ring in the shadow above both
     follow border-radius, so the white, the top fade and the edge all
     turn the same corner. */
  border-top-left-radius: calc(var(--rig) * 0.014);
  border-top-right-radius: calc(var(--rig) * 0.014);
}
/* The top of the window is the paper's real cut edge while the receipt
   is shorter than it, and a clip after that — so the softening is 0
   until the driver has something to soften and ramps in exactly as the
   receipt outgrows it.

   Painted as a wash in the ground's own colour rather than done with
   mask-image, and that is the reason --feed-ground is a token. A mask's
   painting area is the border box, so it takes the element's OUTER box
   shadow with it — the paper would have faded correctly and had no
   shadow at all. An overlay inside the clip leaves the shadow alone. */
.feed__paper::before {
  content: "";
  position: absolute; left: 0; right: 0; top: 0;
  height: var(--fade, 0px);
  background: linear-gradient(to bottom, var(--feed-ground), var(--feed-ground-0));
  pointer-events: none;
  z-index: 5;
}

/* The ink band that used to live here is gone: it faded the last two
   lines with a white wash, and the per-line pop below does the same job
   with motion as well as tone. Two overlapping fades read as neither. */
.js .feed__paper { height: var(--fed-h, 0px); }

/* The strip is the receipt itself; the driver slides it through the
   clip above.

   IT IS ON A STRICT LINE GRID. Every element is an exact multiple of
   --grid, spacing is blank lines the way a real receipt does it rather
   than CSS margins, and the dividers are printed rows of dashes rather
   than borders. --grid is declared as an absolute length, not 1.5em, so
   that children with their own font-size (the brand) land on it too. */
.feed__strip {
  position: absolute;
  top: 0; left: 0; width: 100%;
  font-family: "SFMono-Regular", ui-monospace, "SF Mono", Menlo, Consolas, monospace;
  /* Sized off the LONGEST line on the receipt, which is the 38
     characters of NO DUPLICATE RECEIPTS SLIPPING THROUGH. At a
     monospace advance of ~0.6em that needs 22.8 characters' worth of em
     inside 0.444 rigs of paper, so 0.0185 is the ceiling. Change the
     copy and this is the number to re-check: a line that wraps breaks
     the grid. */
  font-size: calc(var(--rig) * 0.0185);
  line-height: var(--grid);
  color: #2C2C2A;
  letter-spacing: 0.02em;
}
/* THE PAPER CARRIES THE WHITE, not the lines. It used to be per line,
   which was fine while lines only ever slid — but a line that fades or
   moves takes its own background with it, so every one of them would
   have punched a hole in the paper on its way in. The clip is exactly
   the printed length already, so putting the white here is also more
   honest: the paper is the paper. */
.feed__paper { background: #FFFFFF; }
.feed__strip p {
  margin: 0; height: var(--grid); line-height: var(--grid);
  padding: 0 calc(var(--rig) * 0.016);
  box-sizing: border-box;
  /* No --pop, no --print and no --rule. A row used to fade and lift in as
     it cleared the head, with its hairline ramping up underneath; the feed
     itself is the animation now and a row is fully printed by the time the
     paper carries it out.

     A HAIRLINE UNDER EVERY ROW. A gradient rather than a border, because
     a border is 1px of extra box on every one of a hundred and one rows
     and the whole strip is built on an exact line grid — this paints
     inside the row and changes no geometry at all.

     0.7px rather than 1: the section scales to 2.8x by the end, so a
     whole pixel here lands as nearly three and stops reading as a
     hairline. */
  background-image: linear-gradient(to top,
    rgba(28,24,16, 0.085) 0 0.7px, transparent 0.7px);
}
.feed__strip .feed__stamp { background: none; }

/* THE PAPER COMES OUT BLANK. These two lines are the sentence the headline
   becomes, and they must not exist until it has become them: printed from
   the moment the paper emerged, there were two copies of the sentence on
   screen at once, a huge one still falling and a small one already sitting
   under it. That is a duplicate being crossfaded away, not a thing turning
   into another thing, and it is the whole reason the move did not read.

   --land is written by updateWhy() across the last of the flight, so the
   print appears on the frame the headline arrives at it. Defaulting to 1
   keeps them visible with no script, where there is no flight to wait
   for. */
.feed__strip .feed__landed { opacity: var(--land, 1); }
/* Each printed character waits for the headline character that becomes it.
   Written per glyph by updateWhy; 1 by default so the line is simply there
   with no script, where there is no flight to wait for. */
.feed__strip .feed__g { opacity: 1; }

.feed__ln { white-space: nowrap; overflow: hidden; }
.feed__ln--mid { text-align: center; }
/* Scoped, and it has to be: the grid reset above is `.feed__strip p`,
   a class AND an element, which beats a lone class. Unscoped, the brand
   took height: var(--grid) from it and sat two-line type in a one-line
   box — which also silently made the first pleat nine lines instead of
   ten and put a fold half a line out of step with the rest. */
.feed__strip .feed__brand {
  font-size: 1.5em; font-weight: 700; text-align: center; letter-spacing: -0.01em;
  height: calc(var(--grid) * 2); line-height: calc(var(--grid) * 2);
}
.feed__rule { color: #C6C6C2; letter-spacing: 0; }

/* THE PAYOFF. The last thing the machine prints is the mark itself, so
   scrolling past the closing line brings the logo up out of the slot
   rather than the section simply stopping. Four grid lines tall and
   centred, so it sits on the same rhythm as every printed row and the
   receipt stays one continuous strip. */
/* ── THE TWO CLIP BUTTONS, printed on the end of the receipt ──
   Every length here is a fraction of --rig, the same as every other size
   on this paper, so they travel with the push-in instead of sitting at a
   fixed pixel size while the type around them grows.

   A div rather than a <p>, for the reason .feed__stamp is one: the paper's
   whiteness comes from `.feed__strip p`, so anything else in the strip has
   to carry its own or it prints onto the section's grey. */
.feed__watch-lbl {
  letter-spacing: 0.12em;
  opacity: 0.62;
}
.feed__watch {
  height: calc(var(--grid) * 3.4);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(var(--rig) * 0.016);
  background: #FFFFFF;
  padding: 0 calc(var(--rig) * 0.016);
  box-sizing: border-box;
}
.feed__watch-btn {
  appearance: none;
  -webkit-appearance: none;
  flex: 1 1 0;
  min-width: 0;
  cursor: pointer;
  font: inherit;
  font-size: calc(var(--rig) * 0.0168);
  letter-spacing: 0.04em;
  white-space: nowrap;
  color: #2C2C2A;
  background: #FFFFFF;
  /* A printed box, not a web button: one hairline in the receipt's own ink
     and square corners, because nothing else on this paper is rounded. */
  border: 1px solid rgba(44, 44, 42, 0.55);
  border-radius: 0;
  padding: calc(var(--rig) * 0.011) calc(var(--rig) * 0.008);
  transition: background 0.18s ease, color 0.18s ease;
}
.feed__watch-btn:hover { background: #2C2C2A; color: #FFFFFF; }
.feed__watch-btn:focus-visible { outline: 2px solid #C0392F; outline-offset: 2px; }

/* ── THE CLIP, PRINTED ON THE PAPER ─────────────────────────
   Closed to nothing until asked for. Opening it does two things at once:
   this block grows to its content, and lucky-draw-scenes.js feeds exactly
   the same number of pixels of extra paper, so the picture rises into the
   slot rather than appearing inside a receipt that is standing still.

   No transition here on purpose. The height is written frame by frame by
   the same tween that drives the feed, because a CSS transition and a
   scripted feed cannot be kept in step - they would drift apart by a few
   pixels and the paper would appear to stretch. */
.feed__clip {
  height: 0;
  overflow: hidden;
  background: #FFFFFF;
}
.feed__clip-inner {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: calc(var(--grid) * 0.26);
  /* Tight, because the whole block has to clear the paper's window. That
     window is a fixed screen height, so every pixel spent on padding is a
     pixel off the picture. Measured at full push-in: the window is 547px
     on screen and the block was using 511 of it, so the padding here is
     the only place the picture could grow from. */
  padding: calc(var(--grid) * 0.28) 0 calc(var(--grid) * 0.4);
}
/* The caption and the close, on ONE printed line rather than two. Both are
   sized in rig units, so at full push-in they were costing 131px of a 547px
   window between them; on a row they cost 86 and the picture takes the
   rest. */
.feed__clip-foot {
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(var(--rig) * 0.014);
  width: 100%;
  box-sizing: border-box;
  padding: 0 calc(var(--rig) * 0.012);
}
/* SIZED AGAINST THE WINDOW, NOT THE PAPER. The clips are portrait phone
   recordings, so height is what binds: the paper's window is a fixed
   screen height whatever the push-in is doing, and dividing by the live
   --zoom is what makes this land at a real 46vh on screen instead of
   growing with the camera until it is taller than the slot. */
.feed__clip-video {
  display: block;
  /* 40vh, and that is the ceiling, not a taste. The binding frame is full
     push-in, where the paper's own window is 195px of paper and this block
     has to fit inside it: 40vh of picture is 137 of those, the caption row
     and the padding are 46, and 183 against 195 leaves about a dozen paper
     pixels in hand. 42 was tried first and left four, which is close enough
     to the edge that a font landing late could clip the control. */
  height: calc(40vh / var(--zoom, 1));
  width: auto;
  max-width: 82%;
  background: #000;
  border-radius: calc(var(--rig) * 0.012);
}
.feed__clip-cap {
  font-size: calc(var(--rig) * 0.0155);
  letter-spacing: 0.06em;
  color: rgba(44, 44, 42, 0.66);
  text-align: center;
  /* On the row it is the part that gives: the close button is a fixed
     control and must never be squeezed out of its own padding. Its own
     side padding goes with it - that is .feed__strip p's gutter, and the
     row already carries one. Left in, the caption measured 665px of a
     769px paper and shrank HIDE to 43, which is less than that button's
     padding alone. */
  flex: 0 1 auto;
  min-width: 0;
}
/* TWO CLASSES ON PURPOSE. `.feed__strip p` is (0,1,1) and sets every
   printed line's gutter, so a bare `.feed__clip-cap` at (0,1,0) loses to
   it and the padding stays. */
.feed__clip-foot .feed__clip-cap { padding: 0; }
/* The button that opened it reads as pressed, and is also how it closes. */
.feed__watch-btn[aria-expanded="true"] { background: #2C2C2A; color: #FFFFFF; }
/* Narrower than the two that opened it: this one is a dismissal, not an
   offer, and it should not read as a third thing to try. */
.feed__clip-hide {
  flex: 0 0 auto;
  white-space: nowrap;
  padding: calc(var(--rig) * 0.008) calc(var(--rig) * 0.024);
  font-size: calc(var(--rig) * 0.0155);
}

.feed__stamp { background: none; }

/* THE PAPER COMES OUT BLANK. These two lines are the sentence the headline
   becomes, and they must not exist until it has become them: printed from
   the moment the paper emerged, there were two copies of the sentence on
   screen at once, a huge one still falling and a small one already sitting
   under it. That is a duplicate being crossfaded away, not a thing turning
   into another thing, and it is the whole reason the move did not read.

   --land is written by updateWhy() across the last of the flight, so the
   print appears on the frame the headline arrives at it. Defaulting to 1
   keeps them visible with no script, where there is no flight to wait
   for. */
.feed__strip .feed__landed { opacity: var(--land, 1); }
/* Each printed character waits for the headline character that becomes it.
   Written per glyph by updateWhy; 1 by default so the line is simply there
   with no script, where there is no flight to wait for. */
.feed__strip .feed__g { opacity: 1; }

.feed__ln { white-space: nowrap; overflow: hidden; }
.feed__ln--mid { text-align: center; }
/* Scoped, and it has to be: the grid reset above is `.feed__strip p`,
   a class AND an element, which beats a lone class. Unscoped, the brand
   took height: var(--grid) from it and sat two-line type in a one-line
   box — which also silently made the first pleat nine lines instead of
   ten and put a fold half a line out of step with the rest. */
.feed__strip .feed__brand {
  font-size: 1.5em; font-weight: 700; text-align: center; letter-spacing: -0.01em;
  height: calc(var(--grid) * 2); line-height: calc(var(--grid) * 2);
}
.feed__rule { color: #C6C6C2; letter-spacing: 0; }

/* THE PAYOFF. The last thing the machine prints is the mark itself, so
   scrolling past the closing line brings the logo up out of the slot
   rather than the section simply stopping. Four grid lines tall and
   centred, so it sits on the same rhythm as every printed row and the
   receipt stays one continuous strip. */
/* ── THE TWO CLIP BUTTONS, printed on the end of the receipt ──
   Every length here is a fraction of --rig, the same as every other size
   on this paper, so they travel with the push-in instead of sitting at a
   fixed pixel size while the type around them grows.

   A div rather than a <p>, for the reason .feed__stamp is one: the paper's
   whiteness comes from `.feed__strip p`, so anything else in the strip has
   to carry its own or it prints onto the section's grey. */
.feed__watch-lbl {
  letter-spacing: 0.12em;
  opacity: 0.62;
}
.feed__watch {
  height: calc(var(--grid) * 3.4);
  display: flex;
  align-items: center;
  justify-content: center;
  gap: calc(var(--rig) * 0.016);
  background: #FFFFFF;
  padding: 0 calc(var(--rig) * 0.016);
  box-sizing: border-box;
}
.feed__watch-btn {
  appearance: none;
  -webkit-appearance: none;
  flex: 1 1 0;
  min-width: 0;
  cursor: pointer;
  font: inherit;
  font-size: calc(var(--rig) * 0.0168);
  letter-spacing: 0.04em;
  white-space: nowrap;
  color: #2C2C2A;
  background: #FFFFFF;
  /* A printed box, not a web button: one hairline in the receipt's own ink
     and square corners, because nothing else on this paper is rounded. */
  border: 1px solid rgba(44, 44, 42, 0.55);
  border-radius: 0;
  padding: calc(var(--rig) * 0.011) calc(var(--rig) * 0.008);
  transition: background 0.18s ease, color 0.18s ease;
}
.feed__watch-btn:hover { background: #2C2C2A; color: #FFFFFF; }
.feed__watch-btn:focus-visible { outline: 2px solid #C0392F; outline-offset: 2px; }


.feed__stamp {
  height: calc(var(--grid) * 4);
  /* gap went with the <span>: there is one child now, and flex is here
     only to centre it in the four-line box. */
  display: flex; align-items: center; justify-content: center;
  /* It carries its own white. The paper's whiteness comes from
     `.feed__strip p`, and this is the one block in the receipt that is
     not a <p> — without this it printed as a logo floating on the
     section's grey with the paper ending above it. */
  background: #FFFFFF;
  padding: 0 calc(var(--rig) * 0.016);
  box-sizing: border-box;
}
/* SIZED OFF THE GRID, like everything else on this paper, and off the
   HEIGHT rather than the width: the mark is 3.43 wide to 1 tall, so a
   width in grid units would make its height a fraction of a line and put
   it off the rhythm the whole strip is built on.

   2.2 rather than the 2.6 the bare icon wore. The file carries the icon
   and the wordmark at their real relative sizes, and the wordmark's cap
   height is about 0.59 of the whole: at 2.6 the type came out half again
   as large as the live text it replaces, at 1.8 it matched but shrank the
   icon by a third. 2.2 lands the pair at 113px against the old lockup's
   117, so the mark keeps the weight it had.

   max-width is the backstop and it should never fire: 2.2 grid units of
   height is 113px of width inside 277px of usable paper. It is here
   because the one thing that would break this silently is the file being
   swapped for one at a different aspect. */
.feed__stamp img {
  height: calc(var(--grid) * 2.2);
  width: auto;
  max-width: 100%;
  display: block;
}
.feed__row { display: flex; justify-content: space-between; }
.feed__row--meta { color: #6E6E6C; }
.feed__row--in { padding-left: 1.4em; }
.feed__row--us, .feed__head { font-weight: 700; }

/* ── FOUR VOICES, NOT ONE ─────────────────────────────────
   The whole receipt printed at one size in one colour, which is
   honest to a real till and useless as a piece of writing: 99 lines
   of identical grey with nothing telling the eye where to stop.

   Sizes stay on the grid. A section head is two printed lines tall
   rather than 1.3 lines, so the strip's rhythm survives and nothing
   can wrap unexpectedly. The measured line count changes, which is
   fine — the pacing is derived from the strip's measured height, not
   from a number written down anywhere. */

/* 1. SECTION HEADS. Half again the size, two grid lines tall, and
      nearly black.

      The closing line used to be excluded from this, via a .feed__close
      that appears nowhere else: at 36 characters it was the second
      longest line on the receipt and 1.3em of it overran the paper. The
      close is 16 characters now, the same as FRAUD PROTECTION, which has
      room to spare, so the exception went with the copy. 1.3em fits 29
      characters on this paper; anything longer needs it back. */
.feed__strip .feed__head {
  font-size: 1.3em;
  height: calc(var(--grid) * 2);
  line-height: calc(var(--grid) * 2);
  color: #141412;
  letter-spacing: 0.01em;
}

/* 2. THE BAISED SIDE OF EVERY COMPARISON, in the brand's own red
      taken deep enough to hold on paper. The page's --red is #FF6B6B
      and measures 2.78:1 on white, which is a tint, not a colour you
      can set type in. This is 5.97:1. */
/* DIRECT CHILD. It was load-bearing when every glyph on the line was
   wrapped in its own span: the last glyph of the LABEL half — the "d" of
   BaiSed — was also a span:last-child, and the descendant form painted it
   red along with the value. There are no glyph spans any more, so the two
   forms now select the same thing; the > stays because the row's own two
   children are what this rule actually means. */
.feed__strip .feed__row--us > span:last-child { color: #B23A1E; }

/* 3. THE GOOGLE FORM SIDE RECEDES. Same size, same weight, less
      contrast — the comparison reads without either half having to
      shout, and the eye lands on the answer rather than the setup. */
.feed__strip .feed__row--in:not(.feed__row--us) { color: #86867F; }

/* 4. THE FOUR NUMBERS THAT ARE THE ARGUMENT. Everything else on the
      receipt is a capability; these are the only figures on it. */
.feed__strip .feed__hi { color: #B23A1E; font-weight: 700; }

/* And the litany of NOs sits back: eighteen consecutive lines at full
   contrast is a wall, and the point of them is the accumulation, not
   any one of them. */
/* The strip is a flat list of <p>; there is no .feed__block in it, so
   the selector this replaces matched nothing and the NOs were never
   actually stepped back. The plain single-column lines ARE the NO list:
   heads, rows, rules, blanks and centred lines all carry their own
   class. */
.feed__strip .feed__ln:not(.feed__head):not(.feed__row):not(.feed__rule):not(.feed__blank):not(.feed__ln--mid) {
  color: #55554F;
}

/* ── THE NO LIST READS ON ITS SECOND HALF ─────────────────
   Eighteen lines that all open on the same word. Flat, they are a wall;
   the fix is a difference between the halves, and which half gets the
   contrast is the whole question.

   The repeated half loses it. NO is the part the reader has already got
   by line three, and the eighteen phrases after it are the part they are
   scanning for. Putting the contrast on the phrases would mean marking up
   nine tenths of the block, which is the same as marking up none of it,
   so the recession does the work instead: NO steps back a tone and the
   phrases stay where they are, plus a shade.

   NOT the receipt's red on the NO, which was the other candidate. It
   makes a strong left rail and it draws the eye straight down the one
   word with nothing to say, and red is already spoken for on this strip:
   .feed__row--us and .feed__hi use it for BaiSed's own answer and for the
   numbers worth stopping on. A third meaning for it, on the thing being
   REMOVED, is the opposite of the two it has.

   Both tones are AA on paper white: 4.57:1 for the NO and 10.6:1 for the
   phrase, so the receded half is receded, not failed. Set on the spans so
   they beat the line's own colour by matching the element rather than by
   out-specifying the :not() chain above, which is seven classes deep. */
.feed__strip .feed__no > span:first-child { color: #767670; }
.feed__strip .feed__no > span:last-child  { color: #3A3A36; }

/* NO JS, AND NO MOTION. Both end on the printed receipt, but it
   cannot stay absolutely positioned to do it: with no height to
   clamp it the paper is its full ~2.5 viewports and grows upward
   out of the section and over everything above. So the scene
   un-pins, the paper drops into normal flow above the machine,
   and the receipt is simply read as a tall picture. This is the
   same way every other scene on the page degrades. */
html:not(.js) .scene--feed { height: auto; }
html:not(.js) .scene--feed .sticky-frame { position: relative; height: auto; padding: 90px 6vw; }
html:not(.js) .feed { padding-top: 0; }
html:not(.js) .feed__paper {
  position: static; height: auto; margin: 0 auto; overflow: visible;
  width: calc(var(--rig) * 0.47604);
  box-shadow: none;
}
/* The STRIP has to come out of absolute flow as well. Leaving it
   positioned left the paper — now static and auto-height — with no
   in-flow content to take its height from, so the whole receipt
   rendered at zero height and the fallback showed nothing at all. */
html:not(.js) .feed__strip { position: static; }
html:not(.js) .feed { transform: none; }

@media (prefers-reduced-motion: reduce) {
  /* The scene's own height and un-pinning are NOT repeated here. The
     section carries .scene--cars too, and that class's reduced-motion
     rules land later in this file than the mobile override does, so
     they already win on a small screen. A .scene--feed copy here would
     sit BEFORE the mobile 520vh and lose to it. */
  .feed { padding-top: 0; }
  .js .feed__paper {
    position: static; height: auto; margin: 0 auto; overflow: visible;
    width: calc(var(--rig) * 0.47604);
    box-shadow: none;
  }
  /* Same reason as the no-JS case: without this the paper has no
     in-flow content and collapses to nothing. */
  .js .feed__strip { position: static; }
  .feed { transform: none; }
}

/* NOTHING here is withheld. Both roads, both cars, both labels
   and the closing line are on the page from the first frame,
   and the only thing the scroll does is move the cars.
   Fading the second car and its label in was the earlier
   behaviour and it was wrong for the same reason the staggered
   comparison table was: a reader who arrives at a claim should
   be able to read the claim, not scroll to earn it. The picture
   is "same price, one of them wins", and that is a sentence you
   want in front of someone immediately — the animation is
   there to dramatise it, not to ration it. */
.cars-lane {
  position: absolute; left: 0;
  width: calc(100% - var(--reserve));
  height: 50%;
}
.cars-lane--slow { top: 0; }
.cars-lane--fast { top: 50%; }
/* The road this car runs on, at the foot of its own half. */
.cars-lane::after {
  content: ""; position: absolute;
  left: 0; right: 0; bottom: 14px; height: 2px;
  background: repeating-linear-gradient(90deg,
    rgba(26,26,46,0.16) 0 14px, transparent 14px 30px);
}

/* --run is the share of the road covered, handed over by the
   scroll pass. 0.92 leaves the car's own length inside the
   road, so 100% puts its nose on the flag rather than its
   middle, and anything past 100% is the whole car through. */
.cars-car {
  position: absolute; bottom: 15px;
  left: calc(var(--run, 0%) * 0.92);
  width: clamp(84px, 9vw, 124px);
  transition: left .18s linear;
}
.cars-body { display: block; width: 100%; height: auto; }
.cars-lane--slow .cars-body path { fill: #9d9ca6; }
.cars-lane--fast .cars-body path { fill: var(--red); }
.cars-tyre { fill: #2C2C2A; }
.cars-hub { fill: rgba(255,255,255,0.82); }

/* The car's name at display size, not as a 0.74rem all-caps
   tag. It is the loudest word in its half of the picture and
   it was being whispered. */
.cars-name {
  position: absolute; left: 0; top: 0;
  margin: 0;
  font-size: clamp(1.05rem, 1.7vw, 1.45rem);
  font-weight: 800; letter-spacing: -0.015em; line-height: 1.1;
}
.cars-lane--slow .cars-name { color: var(--muted); }
.cars-lane--fast .cars-name { color: var(--cars-red); }
.cars-note {
  position: absolute; left: 0; top: 30px; margin: 0;
  font-size: clamp(0.74rem, 1.05vw, 0.86rem);
  color: var(--muted); font-weight: 500;
}
.cars-lane--fast .cars-note { color: var(--cars-red); font-weight: 600; }

/* The outcome, parked at the FAR end of the road, level with
   the name at the near end. That is the whole point of moving
   it: the section had every word in one corner with a third of
   the panel empty beside it, so the road ran from a wall of
   text into nothing. Now each road reads as a sentence with a
   beginning on the left and an ending on the right, and the
   car travels between them.
   It sits in the same band as the name rather than just above
   the road, because a car is 30px tall and would drive
   straight through it there. */
.cars-out {
  position: absolute; right: 12px; top: 4px; margin: 0;
  max-width: 46%; text-align: right;
  font-size: clamp(0.78rem, 1.05vw, 0.92rem);
  font-weight: 600; line-height: 1.35;
}
.cars-lane--slow .cars-out { color: var(--faint); }
.cars-lane--fast .cars-out { color: var(--cars-red); }

/* Closed on the right, so the section reads on a diagonal —
   headline top left, the cars across the middle, the verdict
   bottom right — instead of everything hugging one margin. */
.cars-punch {
  margin: 5vh 0 0 auto; max-width: 44ch;
  text-align: right;
  font-size: clamp(0.9rem, 1.4vw, 1.12rem);
  line-height: 1.5; color: var(--muted);
}

@media (max-width: 760px) {
  .scene--cars { height: 170vh; }
  /* The same ~0.29px of paper per px of scroll as desktop. The strip
     is shorter here (smaller type on a narrower rig) so it needs less
     runway to hold that rate. It has to be repeated inside this query,
     not just set above it: the .scene--cars rule here is the same
     specificity and lands later in the file, so it would otherwise
     take the feed's height back. */
  .scene--feed { height: 520vh; }
  .cars-head { margin-bottom: 3.4vh; }
  .cars-strip { --reserve: 56px; min-height: 264px; }
  .cars-car { width: 74px; }
  .cars-name { font-size: 1rem; }
  .cars-note { font-size: 0.72rem; top: 24px; }
  /* A phone lane is about 280px, which is not enough to put the
     name at one end and the outcome at the other. It stacks
     under the note instead and keeps its colour. */
  .cars-out {
    position: absolute; left: 0; right: auto; top: 44px;
    max-width: 100%; text-align: left;
    font-size: 0.76rem;
  }
  .cars-punch { margin-top: 3.4vh; text-align: left; max-width: none; }
}

/* No JS and no motion: both cars sit where they end up, which
   is the whole point of the picture anyway. */
@media (prefers-reduced-motion: reduce) {
  .scene--cars { height: auto; }
  .scene--cars .sticky-frame { position: relative; height: auto; padding: 90px 6vw; }
  .cars-car { transition: none; }
  .cars-lane--slow { --run: 68%; }
  .cars-lane--fast { --run: 108%; }
}

.sr-only {
  position: absolute; width: 1px; height: 1px; overflow: hidden;
  clip: rect(0 0 0 0); white-space: nowrap;
}

/* The substance, under the metaphor. Two plain columns, every
   line visible at once, no reveal: this is the only place the
   actual capabilities are stated now, and a claim a reader has
   to scroll to earn is a claim they will not read. */
.cars-facts {
  width: 100%;
  display: grid; grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: clamp(20px, 4vw, 56px);
}
.cars-facts__head {
  margin: 0 0 14px;
  font-size: 0.72rem; font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase;
  padding-bottom: 10px; border-bottom: 1px solid var(--line);
}
.cars-facts__col--bad .cars-facts__head { color: var(--muted); }
.cars-facts__col--good .cars-facts__head { color: var(--cars-red); }
.cars-facts ul { list-style: none; margin: 0; padding: 0; }
.cars-facts li {
  position: relative;
  padding: 7px 0 7px 24px;
  font-size: clamp(0.78rem, 1.05vw, 0.9rem);
  line-height: 1.4;
}
/* The same tick and cross the hidden table used, so the argument
   keeps its vocabulary even though its container changed. */
.cars-facts li::before {
  position: absolute; left: 0; top: 7px;
  font-weight: 800; font-size: 0.92em;
}
.cars-facts__col--bad li { color: var(--muted); }
.cars-facts__col--bad li::before { content: "\2715"; color: rgba(26,26,46,0.62); }
.cars-facts__col--good li { color: var(--ink); font-weight: 500; }
.cars-facts__col--good li::before { content: "\2713"; color: var(--cars-red); }

@media (max-width: 760px) {
  .cars-facts {
    grid-template-columns: minmax(0, 1fr);
    gap: 26px; padding-top: 4vh;
  }
  .cars-facts li { font-size: 0.78rem; padding-left: 20px; }
}

/* ══════════════════════════════════════════════════════════
   THE RACE
   The comparison table as a picture. Two lanes, the scroll is
   the clock, and the finish line is the campaign closing.

   The track runs LEFT TO RIGHT on a wide screen and TOP TO
   BOTTOM on a narrow one, and that is the only difference
   between the two layouts. Everything else — the fill, the
   runner, the checkpoint cards, the finish, the colours — is
   shared, and a checkpoint's distance along the track arrives
   from script as --at without the script knowing or caring
   which axis that is.

   The horizontal layout is the one with a labelling problem:
   five checkpoints across one track leaves about 160px each,
   so the cards stack above the fast lane and below the slow
   one rather than sitting beside the track as they do when it
   runs vertically. That is also why the vertical layout is
   what a phone gets: at 375px, 160px of label becomes 60px.
   ══════════════════════════════════════════════════════════ */
/* 1120px, matching --shell, which is what every other section on the
   page measures. The cloud opening behind this section is cut around
   the stage's own box, so the stage's width IS the width of the warm
   panel: at 940 the race sat narrower than the comparison table above
   it, which is the one section it is meant to line up with. */
.race-stage { --race-red-text: #A32B2B; width: 100%; max-width: 1120px; text-align: left; }
.race-head {
  font-size: clamp(1.4rem, 3vw, 2.3rem);
  line-height: 1.2; letter-spacing: -0.025em; font-weight: 700;
  margin-bottom: 3.4vh;
}
.race-head em { font-style: normal; font-weight: 800; }

/* Lane identity in one place. Every difference between the two
   lanes resolves through these, so the fast lane can be made to
   feel quick and the slow one laboured without either treatment
   being written out twice. */
.race-lane--fast { --lane-fill: linear-gradient(90deg, #FFBD71, #FF6B6B); --lane-ink: var(--race-red-text); --over-max: 46px; }
.race-lane--slow { --lane-fill: linear-gradient(90deg, rgba(26,26,46,0.30), rgba(26,26,46,0.50)); --lane-ink: rgba(26,26,46,0.42); --over-max: 30px; }

.race-name {
  display: flex; align-items: center; gap: 8px;
  font-size: 0.74rem; font-weight: 700; letter-spacing: 0.09em;
  text-transform: uppercase; color: var(--muted);
  margin-bottom: 10px; white-space: nowrap;
  overflow: hidden; text-overflow: ellipsis;
}
.race-dot { width: 9px; height: 9px; border-radius: 50%; flex: none; }
.race-lane--fast .race-name { color: var(--race-red-text); }
.race-lane--fast .race-dot { background: var(--fire); }
.race-lane--slow .race-dot { background: rgba(26,26,46,0.26); }

/* ── the track ───────────────────────────────────────────── */
.race-rail {
  --track-w: clamp(20px, 2.4vw, 26px);
  --lane-bed: rgba(26,26,46,0.05);
  --lane-edge: rgba(26,26,46,0.11);
  position: relative;
}
/* The bed: a lane you could drive down, with an edge and a
   dashed centre line, rather than a rule someone drew. The
   empty track is NEUTRAL in both lanes, and that is the whole
   reason you can see where either runner has got to — tinting
   the fast lane's bed made its own fill invisible against it. */
.race-rail::before {
  content: ""; position: absolute; inset: 0;
  border-radius: 999px;
  background: var(--lane-bed);
  box-shadow: inset 0 0 0 1px var(--lane-edge);
}
/* Ground covered, painted over the bed. This is what reads as
   "ahead": a length you can measure against the other lane,
   which a token floating on a line never gave you. */
.race-rail::after {
  content: ""; position: absolute; left: 0; top: 0;
  border-radius: 999px;
  background: var(--lane-fill);
}

/* ── the runner ──────────────────────────────────────────── */
.race-runner {
  position: absolute;
  width: clamp(24px, 2.7vw, 32px); height: clamp(24px, 2.7vw, 32px);
  /* How far past the line this lane comes to rest. A fixed distance, not
     a share of the track, so it reads the same whichever way the track
     runs. The floor is the checkered band: it is 13px wide and sits 15px
     out, so anything under about 30px leaves the runner straddling it. */
  border-radius: 50%;
  background: var(--lane-fill);
  box-shadow: 0 0 0 4px var(--card), 0 8px 20px rgba(26,26,46,0.18);
  z-index: 2;
}
.race-lane--fast .race-runner {
  box-shadow: 0 0 0 4px var(--card), 0 0 22px 2px rgba(255,107,107,0.55),
              0 8px 20px rgba(255,107,107,0.35);
}
.race-lane--slow .race-runner {
  background: #b9bac6;
  box-shadow: 0 0 0 4px var(--card), 0 4px 10px rgba(26,26,46,0.16);
}
/* A glint, not a disc. A centred white circle at this size
   washed the runner out to a pale ring and lost the brand
   colour entirely. */
.race-runner::after {
  content: ""; position: absolute;
  top: 16%; left: 24%; width: 30%; height: 24%;
  border-radius: 50%;
  background: rgba(255,255,255,0.55);
}

/* ── checkpoints ─────────────────────────────────────────── */
/* Every checkpoint is on the page from the first frame, as an
   empty dashed slot that fills in when the runner reaches it.
   Cards that appeared out of nowhere left the section between
   9% and 47% full depending on where you had scrolled to, which
   is what made the whole thing read as empty. A slot that is
   already there costs no copy and makes the layout complete the
   moment the reader arrives. */
.race-mark {
  position: absolute;
  display: grid; gap: 2px; align-content: start;
  background: transparent;
  border: 1px dashed var(--lane-edge);
  border-radius: 10px;
  padding: 8px 10px;
  font-size: clamp(0.62rem, 0.86vw, 0.72rem);
  line-height: 1.3; color: transparent;
  box-shadow: none;
  transition: color .3s ease, background .3s ease, border-color .3s ease,
              box-shadow .3s ease, transform .3s cubic-bezier(.2,.9,.3,1.3);
}
.race-mark b { font-weight: 600; }
/* The qualifier, straight out of the comparison table above so
   the two sections cannot drift apart. It is what takes a card
   from one line to two, which is most of the density the section
   was missing. */
.race-mark i {
  font-style: normal; font-weight: 400; font-size: 0.88em;
  color: inherit; opacity: 0.72;
}
.race-mark::before {
  content: "✓"; font-size: 0.9em; font-weight: 800;
  color: transparent; transition: color .3s ease;
}
.race-mark--bad::before { content: "✕"; }
.race-mark.on {
  color: var(--ink);
  background: var(--card);
  border: 1px solid var(--line);
  box-shadow: 0 6px 18px rgba(26,26,46,0.08);
}
.race-mark.on::before { color: var(--race-red-text); }
/* Flat and cool, not a white card. A roadblock is not a thing
   that happened for you, so it does not get the raised, lit
   treatment the milestones have. */
.race-mark--bad.on {
  border: 1px solid rgba(26,26,46,0.10);
  background: rgba(26,26,46,0.045);
  box-shadow: none;
  color: var(--muted);
}
.race-mark--bad.on b { font-weight: 500; }
/* 0.62, not 0.42: at the lighter value the cross measured
   2.47:1 on its own card, under the 3:1 a meaningful glyph
   needs. */
.race-mark--bad.on::before { color: rgba(26,26,46,0.62); }
/* The leader. Without it a card floats near the track and the
   reader has to infer which point it belongs to; the line says
   it. */
.race-mark::after {
  content: ""; position: absolute;
  background: var(--lane-ink); opacity: 0.4;
  border-radius: 999px;
}

.race-status {
  font-size: clamp(0.7rem, 1.1vw, 0.82rem); font-weight: 600; line-height: 1.35;
  opacity: 0; transform: translateY(6px);
  transition: opacity .4s ease, transform .4s ease;
}
/* --faint is the page's tertiary tier and lands at 3.32:1 here,
   which is fine for a caption and not for a sentence someone is
   meant to read. --muted is the body tier and clears the floor. */
.race-status--won { color: var(--race-red-text); }
.race-status--stuck { color: var(--muted); font-weight: 500; }
.race-status.on { opacity: 1; transform: none; }

/* ══ HORIZONTAL — the wide-screen layout ══════════════════ */
@media (min-width: 761px) {
  /* The two lanes stack, and the finish is one vertical band at
     the right end of both. RESERVE is the strip kept clear to
     its right so a runner has somewhere to be once it is past
     the line, and so the FINISH label has a home. */
  .race-track {
    --reserve: 78px;
    position: relative;
    display: grid; grid-template-columns: minmax(0, 1fr);
    gap: 22px;
    padding-right: var(--reserve);
  }
  .race-lane { position: relative; }
  /* Room for the cards: above the fast lane, below the slow one,
     so neither lane's labels can collide with the other's.
     The reserve is deep enough to seat the lane's NAME clear of
     its own cards too. A left-hand gutter for the name was the
     obvious alternative and the wrong one: it costs about 110px
     of track, which drops the gap between checkpoints below the
     card width and puts them back on top of each other. Vertical
     room is the cheap thing here now. */
  .race-lane--fast { padding-top: 120px; }
  .race-lane--slow { padding-bottom: 146px; }
  .race-lane--fast .race-name { position: absolute; top: 0; left: 0; margin: 0; }
  .race-lane--slow .race-name { position: absolute; bottom: 0; left: 0; margin: 0; }

  .race-rail { width: 100%; height: var(--track-w); }
  .race-rail::before {
    background:
      repeating-linear-gradient(90deg,
        var(--lane-edge) 0 10px, transparent 10px 22px) 0 50% / 100% 2px no-repeat,
      var(--lane-bed);
  }
  .race-rail::after {
    height: 100%; width: var(--run, 0%);
    transition: width .18s linear;
  }
  .race-runner {
    left: calc(var(--run, 0%) + var(--over, 0) * var(--over-max));
    top: 50%;
    transform: translate(-50%, -50%);
    transition: left .18s linear;
  }

  /* The wider stage buys the cards room back. Five checkpoints 18%
     apart on the 1042px track are 188px from centre to centre, so
     160px still leaves 28px of air between them, and the extra 24px
     of card takes most of the titles down from two lines to one. */
  .race-mark {
    left: var(--at, 0%); width: 160px;
    transform: translateX(-50%) scale(0.99);
    transform-origin: center bottom;
  }
  .race-mark.on { transform: translateX(-50%) scale(1); }
  .race-lane--fast .race-mark { bottom: calc(100% + 16px); }
  .race-lane--slow .race-mark { top: calc(100% + 16px); }
  .race-lane--slow .race-mark { transform-origin: center top; }
  .race-mark::after { left: 50%; width: 2px; height: 16px; margin-left: -1px; }
  .race-lane--fast .race-mark::after { top: 100%; }
  .race-lane--slow .race-mark::after { bottom: 100%; }

  /* ONE finish line, spanning both lanes rather than a band per
     lane. Two bands read as two separate races that happen to be
     side by side; a single line through both is what makes "same
     finish, one of them checked nothing" legible at a glance. */
  .race-track::after {
    content: ""; position: absolute;
    right: var(--reserve); top: 0; bottom: 0; width: 13px;
    border-radius: 3px;
    background:
      conic-gradient(var(--ink) 0 25%, #fff 0 50%, var(--ink) 0 75%, #fff 0) 0 0 / 13px 13px;
    box-shadow: 0 3px 14px rgba(26,26,46,0.18);
  }
  /* Above the band, not beside it: level with the lanes it sat exactly
     where both runners come to rest once they are past the line, and
     disappeared behind them. */
  .race-finish__flag {
    position: absolute; right: 6px; top: -22px;
    font-size: 0.6rem; font-weight: 800; letter-spacing: 0.14em;
    text-transform: uppercase; color: var(--race-red-text);
    opacity: 0; transition: opacity .3s ease;
  }
  .race-finish__flag.on { opacity: 1; }

  .race-verdict {
    display: grid; grid-template-columns: 1fr 1fr;
    gap: clamp(16px, 3vw, 34px);
    margin-top: 2.6vh;
  }
}

/* ══ VERTICAL — the narrow-screen layout ══════════════════ */
/* The track turns and runs down the page. Five checkpoints
   across a horizontal track leaves each about 150px, and a
   375px phone has 330px of track to share between them, which
   is 60px each. Turning the track vertical hands each card the
   lane's full width instead, which is the only way the
   qualifier line survives. Same numbers, same --at, same
   script: only the axis changes. */
@media (max-width: 760px) {
  .scene--race { height: 220vh; }
  .race-track {
    position: relative;
    display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
    padding-right: 0;
  }
  .race-lane { position: relative; display: grid; grid-template-rows: auto 1fr; padding: 0; }
  .race-lane .race-name {
    position: static; margin: 0 0 8px;
    font-size: 0.6rem; letter-spacing: 0.06em; gap: 5px;
  }

  .race-rail {
    --track-w: 14px;
    width: 100%; height: clamp(320px, 54vh, 460px);
    padding-left: calc(var(--track-w) + 10px);
  }
  .race-rail::before {
    inset: 0 auto 0 0; width: var(--track-w);
    background:
      repeating-linear-gradient(180deg,
        var(--lane-edge) 0 10px, transparent 10px 22px) 50% 0 / 2px 100% no-repeat,
      var(--lane-bed);
  }
  .race-rail::after {
    width: var(--track-w); height: var(--run, 0%);
    transition: height .18s linear;
  }
  .race-runner {
    left: calc(var(--track-w) / 2);
    top: calc(var(--run, 0%) + var(--over, 0) * var(--over-max));
    width: 20px; height: 20px;
    transform: translate(-50%, -50%);
    transition: top .18s linear;
  }

  .race-mark {
    left: calc(var(--track-w) + 10px); right: 0; width: auto;
    top: var(--at, 0%); bottom: auto;
    transform: translateY(-50%) scale(0.99);
    transform-origin: left center;
    padding: 5px 7px; border-radius: 8px;
    font-size: 0.58rem; line-height: 1.2; gap: 1px;
  }
  .race-mark.on { transform: translateY(-50%) scale(1); }
  .race-mark::after {
    right: 100%; top: 50%; left: auto; bottom: auto;
    width: calc(10px + var(--track-w) / 2); height: 2px; margin: -1px 0 0;
  }

  /* The finish turns with the track: one horizontal band under
     both lanes instead of one vertical band beside them. */
  .race-track::after {
    left: 0; right: 0; top: auto; bottom: -15px;
    width: auto; height: 13px;
  }
  .race-finish__flag {
    right: 0; top: auto; bottom: -34px; transform: none;
  }
  .race-verdict {
    display: grid; grid-template-columns: 1fr 1fr; gap: 10px;
    margin-top: 62px;   /* clears the band and the runners past it */
  }
}

/* A short screen cannot hold five two-line cards without them
   touching: at 700px of viewport the rail is 322px, which leaves
   58px between checkpoints for a 52px card. The qualifier is the
   part that can go — the checkpoint itself still reads. */
@media (max-height: 780px) {
  .race-mark i { display: none; }
}

/* No JS and no motion: the race is already over. Both runners sit at their
   finishing distance and every marker is up, so the section still makes its
   point as a still picture. */
@media (prefers-reduced-motion: reduce) {
  .scene--race { height: auto; }
  .scene--race .sticky-frame { position: relative; height: auto; padding: 90px 6vw; }
  /* Every checkpoint reads as reached, since the race is already
     over. The transform is left alone on purpose — it differs by
     layout, and pinning the vertical value here threw every card
     half its own width sideways on a wide screen. */
  .race-mark {
    transition: none;
    color: var(--ink); background: var(--card);
    border: 1px solid var(--line);
  }
  .race-mark::before { color: var(--race-red-text); }
  .race-mark--bad { color: var(--muted); background: rgba(26,26,46,0.045); }
  .race-mark--bad::before { color: rgba(26,26,46,0.62); }
  .race-status, .race-finish__flag { opacity: 1; transform: none; transition: none; }
  .race-lane--fast .race-rail { --run: 100%; --over: 1; }
  .race-lane--slow .race-rail { --run: 100%; --over: 1; }  /* both over the line */
}


/* Narrow: the card stops being a row and becomes a block, since a card
   you have to drag sideways is worse than the table it replaced. The
   column headings are dropped and each cell carries its own label. */
@media (max-width: 760px) {
  .vs-wrap { overflow: visible; }
  .vs-table { min-width: 0; font-size: 0.86rem; border-spacing: 0; }
  .vs-table thead { display: none; }
  .vs-table tbody, .vs-table tbody tr, .vs-table tbody th, .vs-table tbody td { display: block; }
  .vs-table tbody tr {
    background: var(--card);
    border-radius: 16px;
    padding: 14px 16px;
    margin-bottom: 10px;
  }
  .vs-table tbody th, .vs-table tbody td { background: none; padding: 0; }
  .vs-table tbody th[scope=row] {
    width: auto; padding: 0 0 10px; border-radius: 0;
    font-weight: 700; font-size: 0.94rem;
  }
  .vs-table tbody td {
    display: flex; align-items: baseline; justify-content: space-between; gap: 14px;
    padding: 6px 0 0; border-radius: 0; text-align: right;
  }
  .vs-table tbody td::before {
    content: attr(data-label);
    flex: 0 0 auto; text-align: left;
    font-size: 0.72rem; letter-spacing: 0.08em; text-transform: uppercase;
    color: var(--faint); font-weight: 600;
  }
  .vs-table tbody td.vs-yes {
    background: none;
    margin-top: 8px; padding-top: 10px;
    border-top: 1px solid var(--line);
  }
  .vs-table tbody td.vs-yes::before { color: var(--red); }
}
@media (prefers-reduced-motion: reduce) {
  .vs-table tbody tr { opacity: 1; transform: none; }
}


@media (max-width:760px){
  .scene--s1{height:130vh}.scene--s2{height:240vh}.scene--s3{height:280vh}
  /* The comparison stops being a table here and becomes eight stacked
     cards, which come to about twice the height of a phone screen. Pinned
     inside a 100vh frame that clips, the middle three were simply gone —
     measured 354px cut off the top and 354px off the bottom on a 375x812
     screen, with no way to reach them. So this one scene flows at its own
     height on narrow screens instead of being pinned. */
  .scene--s4{height:auto}
  .scene--s4 .sticky-frame{position:relative;height:auto;padding:90px 6vw}
  .stats-row{grid-template-columns:1fr;gap:12px}
  .vend{font-size:.76rem;padding:9px 16px}
  .collapse{height:min(64vh,420px)}
}
@media (prefers-reduced-motion: reduce){
  .scene--s1,.scene--s2,.scene--s3,.scene--s4{height:auto}
  .warp{display:none}
  .sticky-frame{position:relative;height:auto;padding:90px 6vw}
  .solo,.stats-final{position:relative;opacity:1!important;transform:none!important}
  .q-line .word{opacity:1}
}

/* ── who it works for ─────────────────────────────────────
   Text only, so the three shapes are separated by rules rather than by
   boxes. A <dl> because that is what a term and its definition are, and
   the row wrapper is what lets the pair sit side by side on a wide
   screen and stack on a narrow one. */
/* ── WHO IT WORKS FOR ─────────────────────────────────────
   Was a two-column term list with rules between the rows, which read as a
   table of copy rather than as the choice it actually is. It is now the
   three choices as the app's own spin button, with the chosen one's
   description under them.

   The button is a port of SpinButton3D (lib/features/spinwheel/
   spinWheelAnimation.dart), same anatomy in the same order: a pedestal, a
   cylinder wall, a face with a white rim, a gloss ellipse, and an italic
   black-weight white label. The app's own numbers are kept where they
   still mean something: 25px of wall, an 8px press, a -4..+2 idle pulse.

   The one deliberate difference is what "pressed" means. In the app the
   press is momentary and says "I am spinning now". Here it is the
   selected state and it latches, because a 3D button already owns the
   perfect vocabulary for chosen: it is the one pushed into its base. */
/* ── WHO IT WORKS FOR ─────────────────────────────────────
   Three shapes of business, all three on screen at once.

   This was a picker: three of the app's 3D spin buttons, two of them
   bobbing on an 800ms loop, revealing one definition at a time. It argued
   against the section it introduced. A reader who sells only on Shopee
   arrives asking whether they are eligible, and a control that hides two
   of the three answers makes them click to find out that the answer never
   changes. So the three are laid out saying it, with the promise that used
   to be a footnote moved up into the lead.

   The card is after the Frans Hals Museum's activity row: a rounded
   surface with its category on a pill in the top corner, the visual and
   the copy under it. The header split is monks.com's, where a small
   section label sits in a left gutter beside the type it introduces. */
/* The section is a flat plum band across the full width, not a panel
   floating on the night sky. It is opaque, so it covers the star layer the
   way the white talent block below it does, and the dark run either side
   is broken deliberately rather than by a gradient that half shows the sky
   through it. Every colour on the page is warm or near-black, so the one
   cool block reads as a change of subject.

   No tokens are redefined here, unlike the white block further down: plum
   is dark, so the night driver's own light ink already lands on it. */
#channels-fit {
  position: relative;
  /* Cards deliberately run past the column edge (see the scatter below).
     clip rather than hidden, and on one axis only: hidden would make the
     section a scroll container and cut the card shadows off vertically. */
  overflow-x: clip;
  /* THE SAME BLOCK AS #celebrity, and every value below is copied from
     it on purpose rather than shared through a token: the point of both
     blocks is that they do NOT follow the day-to-night driver, so each
     states its own ground and ink outright. This band used to sit on a
     second copy of the spectrum above it, with its own warm ink solved
     for that ground; it is the white talent block now, so the ink, the
     tiers, the coral step and the padding all come from there. Keep the
     two lists in step. */
  /* Was a hard #FFFFFF, same as the talent block's. The literal is now a
     fallback behind the pick band's token so the two blocks change
     together; with no band around it, or in the mode the page opens in,
     it resolves to the same white it states. */
  background: var(--pick-ground, #FFFFFF);
  --ink: #0B0B0C;
  --muted: rgba(11, 11, 12, 0.66);
  /* 0.62, not the 0.58 this had against white. The ground under both of
     these blocks is a tint now, and at 0.58 the small-caps tier measured
     4.35:1 on the violet, which is a plain AA failure. 0.62 clears both:
     5.21 on the amber, 4.93 on the violet. */
  --faint: rgba(11, 11, 12, 0.62);
  --line: rgba(11, 11, 12, 0.13);
  /* Stepped down again, for the same reason as the tier above. The old
     value was solved against white and measures 4.06:1 on the amber and
     3.57:1 on the violet once the ground is tinted. This is the same hue
     taken to 82%, which clears both: 5.5 and 4.84. */
  --red: #A53039;
  --card: #FFFFFF;
  --bg2: #F4F4F5;
  color: var(--ink);
  padding-top: 104px;
  /* SHORT AT THE BOTTOM, unlike the top. This band and #celebrity below it
     are now the same white, so the join between them is invisible and the
     two paddings simply stacked into dead air: 104 here plus 104 there plus
     the row's own 58 put 266px of nothing between the two prize cards and
     the talent heading. #celebrity's own 104 is the break the reader sees;
     this only needs to clear the cards' drop shadows. */
  padding-bottom: 24px;
}
/* The talent block's grotesque, for the heading and the section around
   it, so the two blocks are set the same way and not only coloured the
   same way. Sarabun stays behind it for Thai. */
#channels-fit,
#channels-fit .sec-head h2 {
  font-family: 'Helvetica Neue', Helvetica, Arial, 'Sarabun', sans-serif;
}
#channels-fit .sec-head h2 {
  letter-spacing: -0.033em;
  font-weight: 700;
}
#channels-fit .sec-head .eyebrow { letter-spacing: 0.16em; }
#channels-fit .sec-head p { letter-spacing: -0.004em; }
/* White cards on white: the hairline is what separates them now, so it
   takes the block's own line tier rather than the old warm one. */
#channels-fit .fit-card {
  background: var(--card);
  border-color: var(--line);
}
/* No lift on hover any more: the scroll engine writes `transform` inline
   on these cards, which would win over anything set here. Colour and
   shadow carry the hover instead. */
#channels-fit .fit-card:hover {
  border-color: rgba(196, 78, 52, 0.4);
  box-shadow: 0 16px 36px rgba(120, 72, 26, 0.22);
}

/* The two halves of the merged section. It answers two questions that
   used to be two sections, so each half is numbered rather than given its
   own heading: one reading order, one background, one scroll. */
.offer-lead {
  display: flex; align-items: center; gap: 13px;
  margin: 0 0 20px;
  font-size: 0.82rem; font-weight: 600;
  letter-spacing: 0.09em; text-transform: uppercase;
  color: var(--muted);
}
.offer-lead span {
  flex: 0 0 auto;
  padding: 5px 11px;
  border-radius: 100px;
  background: var(--fire);
  color: #fff;
  font-size: 0.68rem; font-weight: 700; letter-spacing: 0.1em;
}
html[lang="th"] .offer-lead { letter-spacing: 0.02em; }
/* Widened out of its own 660px centre column to the full row, so the two
   prize cards and the three channel cards read as one grid rather than a
   narrow pair sitting above a wide trio. */
/* The layout here is the JOINED state: a tidy two-column grid with the
   wide card under it, which is where the cards END UP.

   The scatter is not in the layout at all, it is the animation's starting
   pose. Each card begins pushed out and away from its neighbours and a few
   degrees out of square, and closes on this grid as it scrolls in (see
   OFFER_SCATTER in lucky-draw-scenes.js). Putting the scatter in the
   layout instead would mean animating margins and grid spans, which is
   layout work on every frame; as transforms it is free, and the cards can
   converge rather than merely appear. */
#channels-fit .picks {
  max-width: none;
  gap: 22px;
  /* No tail. The 58 was the gap down to the channel cards that used to
     follow this row inside the same section; the row is the last thing in
     it now, so the section's own padding is the only spacing needed. */
  margin: 0;
  align-items: start;
}
#channels-fit .fit-grid { gap: 22px; }
/* One per row on a phone, and this is a sizing fix rather than a taste
   one. The two faces of a flip card share a single grid cell, so the row
   is as tall as the taller of them. A card has a definite aspect ratio, so
   it does not stretch to the row; two-up on a 390px screen the art came to
   107px while the copy on the back needed 240, and the difference showed
   as a 130px hole under the pair. Full width the art is 219px and the two
   faces very nearly agree. */
@media (max-width: 620px) {
  #channels-fit .picks { grid-template-columns: minmax(0, 1fr); }
}

.sec-head--split {
  display: grid;
  grid-template-columns: 150px minmax(0, 1fr);
  gap: 0 40px;
  max-width: 1040px;
}
/* The label is a gutter note, so it sits on the heading's first line
   rather than above it, and gives up the stacked margin. */
.sec-head--split .eyebrow { margin: 8px 0 0; }
.sec-head--split .sec-head__body { max-width: 680px; }

.fit-grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 20px;
  align-items: start;
}
.fit-card {
  display: grid;
  /* The recording is portrait and full height it is 500px of phone, which
     is taller than everything it is meant to support. Beside the copy at
     148px it stays legible and the card stays a card. */
  grid-template-columns: 148px minmax(0, 1fr);
  gap: 16px 20px;
  padding: 20px;
  border: 1px solid var(--line);
  border-radius: 22px;
  background: var(--card);
  /* Two transitions on one element: .rise animates opacity and transform
     to bring the card in, and this carries the hover lift afterwards. They
     share the transform, so the lift is only ever applied once the card
     has settled at translateY(0). */
  transition: opacity 0.55s ease, transform 0.4s ease,
              border-color 0.4s ease, box-shadow 0.4s ease;
}
.fit-card:hover {
  border-color: rgba(255, 107, 107, 0.34);
  box-shadow: 0 18px 40px rgba(8, 4, 18, 0.42);
}
/* Hybrid has no recording of its own, because hybrid is the two above it.
   It runs the full width under them and says so. */
.fit-card--wide { grid-column: 1 / -1; grid-template-columns: minmax(0, 1fr); }
/* Capped, because the wide card has no phone beside it to break the line:
   left to the full row its paragraph ran past 120 characters. */
.fit-card--wide .fit-card__body { max-width: 72ch; }
.fit-card__tag {
  grid-column: 1 / -1;
  justify-self: start;
  margin: 0;
  padding: 7px 13px;
  border-radius: 100px;
  background: var(--fire);
  color: #fff;
  font-size: 0.62rem;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
}
html[lang="th"] .fit-card__tag { letter-spacing: 0.06em; }
/* The WHOLE recording, at its own 572/1180. It used to be cropped to a
   fixed 248px window with object-fit: cover, back when the copy beside it
   was about 140px and a full-height clip left a hole down the middle of
   every card. The copy has since grown to ~250px, so the full clip is only
   about 60px taller than the words next to it and the card still reads as a
   card — and a screen recording with its bottom sliced off does not.

   align-self matters here: a grid item stretches to its row by default, and
   a definite height beats aspect-ratio, so stretched it would be pinned
   back to the height of the copy and cropped all over again. */
.fit-card .channel__phone {
  margin-top: 0;
  max-width: none;
  width: 100%;
  align-self: start;
}
.fit-card__body { display: flex; flex-direction: column; gap: 9px; }
/* The verdict, and it is the point of the whole section. The copy used to
   define each shape and never say the thing the reader came to find out. */
.fit-card__yes {
  display: flex;
  align-items: center;
  gap: 9px;
  margin: 0;
  font-size: 1.04rem;
  font-weight: 700;
  color: var(--ink);
}
/* The tick lands a beat after its card, which is the one moment in the
   section worth animating: it is the answer the reader came for. */
.fit-card__yes::before {
  content: '✓';
  transition: transform 0.5s cubic-bezier(.2, 1.4, .4, 1), opacity 0.35s ease;
  transition-delay: 0.3s;
  flex: 0 0 auto;
  display: grid;
  place-items: center;
  width: 23px; height: 23px;
  border-radius: 50%;
  background: var(--fire);
  color: #fff;
  font-size: 0.74rem;
  font-weight: 700;
}
/* Hidden only once the script is running: without it the tick is simply
   there, which is the right answer for a reader with no JavaScript. */
.js .fit-card__yes::before { opacity: 0; transform: scale(0.3); }
.fit-card.settled .fit-card__yes::before { opacity: 1; transform: scale(1); }
.fit-card__what {
  margin: 0;
  font-size: 0.92rem; line-height: 1.62;
  color: var(--muted); font-weight: 300;
}
.fit-card__proof {
  margin: auto 0 0;
  padding-top: 11px;
  border-top: 1px solid var(--line);
  font-size: 0.86rem; line-height: 1.55;
  color: var(--faint); font-weight: 300;
}
.fit-card__proof b {
  display: block;
  margin-bottom: 3px;
  color: var(--ink);
  font-size: 0.62rem; font-weight: 700;
  letter-spacing: 0.14em; text-transform: uppercase;
}
html[lang="th"] .fit-card__proof b { letter-spacing: 0.06em; }

@media (max-width: 860px) {
  .sec-head--split { grid-template-columns: 1fr; gap: 0; }
  .sec-head--split .eyebrow { margin: 0 0 12px; }
  .fit-grid { grid-template-columns: 1fr; }
  /* One column, and the engine narrows its horizontal throw to match:
     there is no room to come together from off to the side. */
  #channels-fit .picks { grid-template-columns: minmax(0, 1fr); }
  #channels-fit .fit-grid { grid-template-columns: minmax(0, 1fr); }
}
@media (max-width: 560px) {
  .fit-card { grid-template-columns: 120px minmax(0, 1fr); gap: 14px; padding: 16px; }
}
/* ── CONTACT CARD (beside the lead form) ────────────────── */
.contact-card {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 18px;
  padding: 22px 24px;
}
.contact-card__who { margin-bottom: 16px; }
.contact-card__who b { display: block; font-size: 1rem; font-weight: 600; letter-spacing: -0.01em; }
.contact-card__who span { font-size: 0.8rem; color: var(--faint); }
.contact-card__lines { list-style: none; display: flex; flex-direction: column; gap: 12px; }
.contact-card__lines li {
  display: flex;
  align-items: baseline;
  gap: 12px;
  padding-top: 12px;
  border-top: 1px solid var(--line);
}
.contact-card__label {
  flex: 0 0 46px;
  font-size: 0.66rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--faint);
}
.contact-card__lines a {
  color: var(--ink);
  text-decoration: none;
  font-size: 0.92rem;
  overflow-wrap: anywhere;
  border-bottom: 1px solid transparent;
  transition: border-color 0.2s, color 0.2s;
}
.contact-card__lines a:hover { border-bottom-color: var(--red); color: #c2453f; }

/* ══════════════════════════════════════════════════════════
   SQUIRCLE CORNERS
   Every rectangular container uses a superellipse rather than
   a circular-arc corner, so the curve eases into the straight
   edge instead of meeting it at a tangent.

   Pills (border-radius: 100px), the dot indicators and the
   circular avatar slot are left alone on purpose: a pill is
   already a stadium and a squircle would flatten its ends.

   corner-shape is simply ignored where it is unsupported, and
   those browsers keep the ordinary rounded corner, so this
   needs no @supports guard. Radii are nudged up slightly
   because a squircle reads tighter than an arc at the same
   value.
   ══════════════════════════════════════════════════════════ */
.card,
.case,
.quote,
.cta-strip,
.lead-form,
.contact-card,
.one-team,
.stats-row div,
.proof div,
.step,
.proc li,
.note,
.case__logo,
.case__metrics div,
.qr-box img,
.field input,
.field select,
.field textarea,
.img-slot,
.img-slot--phone,
.img-slot--icon,
.img-slot--step,
.img-slot--logo {
  corner-shape: squircle;
}

/* A superellipse at the same radius looks less round than an arc,
   so the larger panels get a little more to keep the same weight. */
.card, .step { border-radius: 20px; }
.case, .stats-row div { border-radius: 24px; }
.quote, .cta-strip, .lead-form, .contact-card { border-radius: 26px; }
.one-team { border-radius: 30px; }
.proof div, .proc li { border-radius: 18px; }
.img-slot { border-radius: 22px; }
.img-slot--phone { border-radius: 38px; }
.img-slot--icon { border-radius: 20px; }
.img-slot--step, .img-slot--logo, .case__logo, .case__metrics div { border-radius: 15px; }
.field input, .field select, .field textarea { border-radius: 15px; }

/* Two .img-slot variants must not inherit the generic squircle radius
   set above: the avatar is a circle, and the case visual sits flush to
   the top of its card, where any radius of its own double-rounds
   against the card's already-clipped corner. */
.img-slot--avatar { border-radius: 100px; corner-shape: round; }
.case__visual { border-radius: 0; }

/* ── WORK CARDS ─────────────────────────────────────────────
   Mirrors index.html's .product-card: no panel, no border, no
   shadow — a contained image over centred text, lifting on hover.
   ────────────────────────────────────────────────────────── */
.work-grid {
  display: grid;
  /* 260 rather than 230: at the 1120 shell auto-fit lays down exactly
     three tracks for three cards, instead of a fourth empty one that
     drags the row off centre. */
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 40px;
}
.work-card {
  padding: 24px;
  display: flex;
  flex-direction: column;
  align-items: center;
  text-align: center;
  transition: transform 0.3s ease;
}
.work-card:hover { transform: translateY(-8px); }
/* A wide banner, matching the rectangle the app's home feed uses as a
   campaign card background.

   The slot's ratio IS the artwork's ratio, 1131x595, not a generic 16/9.
   These three are purpose-made web banners delivered at one size, so a
   slot that disagreed with them would letterbox every card.

   cover, and no padding, for the same reason. The old rule used contain
   plus 8px because the art then ran from 2.06:1 to square and cover would
   have cropped faces out of the square ones. That is no longer true: all
   three are now the same shape, so cover fills the slot edge to edge and
   crops at most the 1px by which UECOF is wider than the other two. */
.work-card__img {
  width: 100%;
  aspect-ratio: 1131 / 595;
  margin-bottom: 22px;
  border-radius: 18px;
  overflow: hidden;
}
/* The plate — the wash, the border and, on the deck, the big soft
   shadow — belongs to a slot that HAS artwork, and is opt-in for the
   same reason the waiting state is: CSS cannot ask whether an <img>
   has decoded, so the script says so.
   Opt-in on the PLATE rather than opt-out, which is the correction.
   The waiting class alone was not enough, because it is written by
   script and there is a window between the stylesheet applying and
   the script executing where an empty slot painted a full plate: a
   rounded rectangle carrying a 112px drop shadow and nothing inside
   it, which reads as a modal that failed to open. Now nothing paints
   until there is something to paint it around.
   A page with no script keeps its artwork and loses only the frame,
   rather than hiding the artwork outright. */
.work-card__img.is-ready { background: linear-gradient(135deg, rgba(255,107,107,0.05), rgba(255,189,113,0.07)); }
.work-card__img.is-ready:has(> img) { border: 1px solid var(--line); }
.work-card__img > img { object-fit: cover; padding: 0; }
.work-card h3 {
  font-size: 1.35rem;
  letter-spacing: -0.02em;
  font-weight: 600;
  margin-bottom: 8px;
}
.work-card p {
  font-size: 0.92rem;
  color: var(--muted);
  line-height: 1.65;
  max-width: 280px;
}
.work-card .card-link { margin-top: 14px; }

@media (max-width: 760px) {
  .work-grid { gap: 26px; }
  .work-card { padding: 14px; }
}

/* ── WORK DECK ──────────────────────────────────────────────
   The three campaigns as a centre-focused deck rather than a row:
   one card square-on in the middle, its neighbours smaller, tilted,
   dropped a little and dimmed, so the section reads as a stack being
   flicked through instead of a grid.

   Vertical scroll drives it sideways. The reference this borrows from
   swallows the wheel outright and never scrolls its page, which is fine
   for a one-screen site and hostile on a long one, so the runway below
   is ordinary page scroll and the stage simply pins inside it.

   Desktop only, deliberately: a pinned stage with two cards parked
   off-screen has nothing to offer a phone, and the plain column the
   cards fall back to below is the better layout there anyway.
   ────────────────────────────────────────────────────────── */
/* The ovals belong to the pinned stage and nothing else. In the plain
   column below 901 there is no stage to sit behind, so they never build.
   This sits ABOVE the media query on purpose: same specificity, so placing
   it below would have won at every width and hidden the ovals outright. */
.workdeck__sky { display: none; }
/* Same placement, same reason. Below 901 the cards are a plain column and
   nothing turns them over, and with no rule at all the back panel rendered
   as a bare block beside the artwork, its copy clipped at the card's edge.
   The deck's own rule further down re-declares it as a flex panel. */
.work-card__back { display: none; }

@media (min-width: 901px) {
  /* Runway. The arithmetic, because this number is not free-standing:

       height - 100vh (the pinned stage)     = the scroll the deck gets
       that - 40vh held at EACH end          = the travel the cards spend
       that / (cards - 1)                    = one hop between campaigns

     The held stretches at each end are why the number carries the extra
     80vh: without them the deck hands over to the next section on the very
     notch that lands the last campaign, and a single flick anywhere on the
     page throws you past the whole thing. One more scroll to get in, one
     more to get out.

     At three cards this was 340, giving two hops of 80vh. Each card added
     since is another hop, and leaving the height alone would pay for it by
     making every hop shorter: the deck would step through five cards in the
     scroll that used to show three, which reads as a flick rather than a
     walk. 500 keeps the hop at 80vh across five.

       500 - 100 = 400 of scroll, - 80 held = 320 of travel, / 4 hops = 80vh

     A sixth card means 580vh, and so on in 80s. The script has
     to know the same 40vh, or the cards would spend it drifting instead of
     parked; deckProgress in lucky-draw-scenes.js is the other half of this,
     and lucky-draw-deck-gl.js repeats it a third time so the pill's arrows
     land where the runway parks. */
  .workdeck { position: relative; height: 500vh; }
  .workdeck__stage {
    position: sticky; top: 0; height: 100vh;
    /* The deck is a space, not a row. Without a perspective the cards can
       only spin in the picture plane, which is the difference between a
       tilted picture and one that is turned away from you. Everything the
       script does on the z axis resolves through this. */
    perspective: 1500px;
    perspective-origin: 50% 45%;
    /* Full bleed out of the shell. A card mid-hop sits past the shell's
       edge no matter how the step is tuned: pull the step in far enough to
       contain the FAR card and the neighbour ends up overlapping the
       centre one. So the stage gets the whole window to travel in, and the
       edges are handled by the mask below rather than by cramping.
       50% resolves against the shell, 50vw against the window, so this
       re-centres itself at any width without a breakpoint. */
    margin-inline: calc(50% - 50vw);
    /* No edge mask here, deliberately. It used to fade the outer 8% of each
       side, because a card leaving the deck crossed the cloud rim at
       partial opacity and read as floating on the weather. The opaque
       field now covers the whole stage, so there is no rim inside it to
       cross and nothing left for the mask to protect — and it would fade
       the field's own edges, which is exactly the corner-to-corner
       coverage this is meant to have. A card running off the side is
       simply clipped by the window, as it is in the reference.
       (Not "overflow: hidden" either: that would clip the sticky.) */
  }

  .workdeck__card {
    position: absolute;
    left: 50%; top: 50%;
    /* 1.5x the old 430/34vw. Both halves are scaled, not just the pixel
       cap, so the crossover width where the vw term takes over stays put
       at about 1265px and the deck keeps the same proportions all the way
       down to its 901px floor. */
    width: min(645px, 51vw);
    /* The base transform matters: it is what the card wears before the
       script runs and after it stops, so the deck is centred rather than
       hanging off its own top-left corner. */
    transform: translate(-50%, -50%);
    transform-origin: 50% 50%;
    /* The slot inside tilts to the pointer, and it has to do that in the
       SAME space the card is turned in, not in a flattened one of its
       own. Note the script clears `filter` on the active card for this
       reason: any filter would group the card and flatten this back. */
    transform-style: preserve-3d;
    will-change: transform, opacity;
    /* .work-card carries transition: transform 0.3s. Left on, every
       scroll frame would start a new 300ms tween and the deck would
       swim a third of a second behind the reader. */
    transition: none;
    /* No panel. The artwork is the object, and it floats on the ground
       rather than being mounted on a card.

       The panel used to be here because a tilted picture with nothing
       behind it "just looks like a crooked image". That is a real problem
       and it still is: what solves it now is elevation instead of an edge.
       The slot below carries a three-stop shadow, contact plus mid plus
       ambient, so the art reads as lifted off the field. A rectangle that
       is plainly casting a shadow is not a crooked image, it is a raised
       one, and it needs no border to say so.

       padding goes to 0 so the art spans the full card width. .work-card
       still supplies the gap under it. */
    padding: 0;
  }
  /* Elevation, the panel's replacement. Three stops, because one shadow
     reads as a sticker: a tight contact shadow, a mid one for the lift,
     and a wide soft one for the ambient drop. Warm-black rather than
     neutral so it sits in the ground's own colour instead of greying it. */
  /* Idle life. A deck that only moves while the reader is scrolling reads
     as placed; one that keeps breathing reads as suspended.

     This animates `translate`, the individual property, NOT `transform`.
     The pointer tilt below writes `transform` on this same element and the
     script writes `transform` on the card above it, so all three motions
     live on different properties and none of them clobbers another. The
     stagger is negative delays, so every card is already mid-cycle on the
     first frame instead of all three starting together. */
  @keyframes workdeck-float {
    0%, 100% { translate: 0 0; rotate: 0deg; }
    50%      { translate: 0 -15px; rotate: var(--float-tilt, 0.7deg); }
  }
  .workdeck__card .work-card__img {
    animation: workdeck-float 8s ease-in-out infinite;
    /* 0.2s, not 0.35: the tilt tracks a moving cursor, and at 0.35s it
       trailed far enough behind to read as drift rather than response. */
    transition: transform 0.2s cubic-bezier(0.2, 0.7, 0.3, 1);
    will-change: translate, transform;
    border: 0;
  }
  /* An empty slot on the deck. The rule above strips the border every card
     carries, which is right for artwork and wrong for a placeholder: what
     was left was a near-invisible wash on a coloured ground with a caption
     too small to read at this size. is-ready is added by the script only
     once an image has actually decoded, so :not(.is-ready) is exactly the
     unfilled state, and the moment the banner lands all of this drops away
     and the card looks like every other campaign. */
  .workdeck__card .work-card__img:not(.is-ready) {
    border: 2px dashed rgba(255, 255, 255, 0.55);
    background: rgba(255, 255, 255, 0.14);
    box-shadow: none;
  }
  /* The label carries its own backing rather than relying on the ground
     behind it. The slot is centred on the stage, which is exactly where the
     ring gradient turns over, so the darkest band of every campaign's
     ground passes straight under this text. A shadow alone was not enough
     there. */
  .workdeck__card .work-card__img:not(.is-ready) .img-spec {
    font-size: 1rem;
    line-height: 1.9;
    color: rgba(255, 255, 255, 0.96);
    padding: 12px 22px;
    border-radius: 14px;
    background: rgba(28, 8, 16, 0.34);
    -webkit-backdrop-filter: blur(3px);
    backdrop-filter: blur(3px);
  }
  .workdeck__card .work-card__img:not(.is-ready) .img-spec b {
    font-size: 1.25rem;
    color: #fff;
  }
  .workdeck__card .work-card__img.is-ready {
    box-shadow:
      0 2px 8px rgba(28, 8, 16, 0.28),
      0 18px 40px rgba(28, 8, 16, 0.36),
      0 62px 112px rgba(28, 8, 16, 0.32);
  }
  /* Durations, phases AND tilt directions all differ per card, so the
     three never bob in sync — synchronised drift reads as a mechanism,
     staggered drift reads as three objects each floating on its own. */
  /* One line per campaign, counted with nth-of-TYPE rather than nth-child.
     The cards are the only <article> elements in the stage, but they are
     not the only children: the sky div sits before them, and under the GL
     ground a canvas is inserted before them too. Counting children made
     the first rule address the sky and never match a card, and shifted
     every other rule onto its neighbour, so the last campaign was left
     with no stagger at all and bobbed against the shared default. Counting
     articles is immune to both, and to the pill the script appends after.

     A card past the last line here gets the shared 8s cycle with no delay
     and no tilt, so extend this list whenever the deck grows. */
  .workdeck__card:nth-of-type(1) .work-card__img { animation-duration: 7.5s; animation-delay: -0.4s; --float-tilt: 0.8deg; }
  .workdeck__card:nth-of-type(2) .work-card__img { animation-duration: 9s;   animation-delay: -3.1s; --float-tilt: -0.9deg; }
  .workdeck__card:nth-of-type(3) .work-card__img { animation-duration: 8.2s; animation-delay: -5.6s; --float-tilt: 0.6deg; }
  .workdeck__card:nth-of-type(4) .work-card__img { animation-duration: 8.7s; animation-delay: -2.2s; --float-tilt: -0.7deg; }
  .workdeck__card:nth-of-type(5) .work-card__img { animation-duration: 7.9s; animation-delay: -4.4s; --float-tilt: 0.5deg; }
  @media (prefers-reduced-motion: reduce) {
    .workdeck__card .work-card__img { animation: none; transition: none; }
  }
  /* The caption has to stop following the night driver.

     By the time the reader is this far down the page, clouds.js has swung
     --ink to near-white and --card to #12182d, and the panel was the only
     thing making that pale type readable. This section deliberately stops
     being night for its duration (see the ground note above) and paints an
     opaque, bright field, so its type is DAY type and is pinned here
     rather than inherited.

     Dark, because the oval a card sits on is the lighter of the two ground
     layers at all three stops: purple, then teal, then yellow. The faint
     light halo is for the moment a card drifts off the oval and out over
     the darker field mid-hop. */
  .workdeck__card h3,
  .workdeck__card p {
    color: #1b1430;
    text-shadow: 0 1px 2px rgba(255, 255, 255, 0.34);
  }
  /* Full opacity, not 0.82. The faded body was the one thing that fell
     under AA on the Golden Mountain stop, whose oval is the darkest of the
     three: 3.79 at 0.82 alpha against 4.82 opaque. The saving in softness
     was not worth a caption that fails on the first card the reader sees. */
  .workdeck__card p { color: #1b1430; }
  /* The coral link is a brand colour tuned for a dark panel and it dies on
     a yellow field. Deepened just for this section so it still reads as
     the same red without dropping to 2:1 on the UECOF stop. */
  .workdeck__card .card-link {
    color: #96202f;
    text-shadow: 0 1px 2px rgba(255, 255, 255, 0.34);
  }
  /* ── THE BANNER TURNS WHEN THE DISC IS PRESSED ────────────
     On the <img>, not on .work-card__img around it: the slot's transform is
     written every frame by lucky-draw-deck-gl.js for the cursor springs,
     and it carries the float animation as well, so anything put there is
     overwritten on the next frame. The image itself has no other owner.

     A FULL TURN, not a half. rotateY(180deg) would leave the banner facing
     away — which for an image means mirrored, with the brand's own logo and
     Thai copy running backwards. 360 reads as the same gesture and lands
     where it started.

     One-shot, driven by a class the script removes on animationend, so a
     second press turns it again instead of the animation being stuck at
     its end state. */
  /* ── THE BANNER FLIPS TO ITS BACK ─────────────────────────
     THE BANNER, NOT THE CARD. What turns is .work-card__img — the artwork
     and this panel as one pair — so the card's own box, its heading and its
     terms link underneath all stay where they are. Only the picture turns
     over.

     The rotation itself is written by lucky-draw-deck-gl.js, not here, and
     that is forced: the script rewrites this slot's transform every frame
     for the cursor springs, so a rotate declared in CSS survives exactly
     one frame. It adds half a turn to the rotateY it is already writing
     there, which is why the tilt and the flip never fight. The easing is
     in the loop for the same reason — a CSS transition cannot run on a
     property being reassigned every frame. */
  /* NO preserve-3d, AND THAT IS NOT A CHOICE. .img-slot carries
     overflow: hidden for its rounded corners, and any overflow other than
     visible forces transform-style to compute to flat — so the 3D context
     never existed and backface-visibility was ignored with it. What that
     looked like: the banner turned to 180 and showed its own back, the
     artwork mirrored, with the panel stranded behind it.
     Flat is fine. The parent's rotateY renders as a horizontal mirror, the
     back face carries its own rotateY(180deg), and the two cancel — so the
     panel's text comes out the right way round. What flat cannot do is hide
     the face that turned away, so the script swaps them at the halfway
     point instead: see the .past-half class it sets when the turn crosses
     90 degrees. */
  .workdeck__card { perspective: 1400px; }
  .workdeck__card .work-card__img > img { opacity: 1; }
  .workdeck__card.past-half .work-card__img > img { opacity: 0; }
  .workdeck__card.past-half .work-card__back { opacity: 1; }
  .work-card__back {
    position: absolute;
    inset: 0;
    /* Parked facing away, so the half turn brings it round. */
    transform: rotateY(180deg);
    /* Hidden until the turn is past 90 degrees. No transition on either
       face: the swap happens at the moment the card is edge-on, where there
       is nothing to see, so fading would only make the change visible. */
    opacity: 0;
    /* Nothing on it is interactive and the same words are already in the
       card's own heading and paragraph, so it never needs to take a click
       or be read a second time. */
    pointer-events: none;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    gap: 8px;
    padding: 7% 9%;
    box-sizing: border-box;
    border-radius: inherit;
    /* Blush, the colour the deck arrives on, so the back of a card belongs
       to the section rather than introducing a colour of its own. */
    background: #F9DAD4;
    color: #2A1418;
    text-align: center;
  }
  .work-card__back h4 {
    margin: 0;
    font-size: clamp(0.95rem, 1.5cqw, 1.3rem);
    font-weight: 800;
    letter-spacing: -0.01em;
  }
  .work-card__back p {
    margin: 0;
    font-size: clamp(0.72rem, 1.15cqw, 1rem);
    line-height: 1.5;
  }
  /* ── The text cards ───────────────────────────────────────
     Two of the five carry words rather than artwork: the heading that
     opens the deck and the quote that closes it. They take the deck's
     transform like any other card and share one panel: the page's paper
     white, the artwork's corner radius, and the artwork's own three-stop
     lift, so a card made of words is as much an object on the ground as a
     card made of picture. Wider than a campaign card, because a heading
     set to 645px wraps to four lines at the sizes below.

     Every colour in here is pinned rather than taken from a token. These
     panels are light and they sit inside a section the day-to-night driver
     has already swung dark, so --ink, --muted and --card all resolve to
     night values: inherited, this type would be near-white on near-white.

     No text-shadows either. They were holding the type up against a ring
     gradient passing under it, and against a solid panel they only soften
     the letters. The type scale is per card below. */
  .workdeck__card--panel {
    /* A SQUARE, because this is a cover in the same stack as the campaign
       banners rather than a page element that wandered into it. Three
       limits, whichever binds first: 620 is as large as it wants to be
       drawn, 48vw keeps it clear of the cards parked either side, and 66vh
       is the one that binds on a short window, where a square as wide as a
       banner would slide under the nav and off the bottom at once. */
    width: min(620px, 48vw, 66vh);
    aspect-ratio: 1 / 1;
    /* The copy no longer fills its box, so it has to be placed in it.
       Centred on both axes: in a square, anything else reads as a mistake
       rather than a decision. Percentage padding resolves against the
       width, which is the side that is capped, so it cannot run away. */
    display: flex;
    flex-direction: column;
    justify-content: center;
    box-sizing: border-box;
    text-align: center;
    /* NOT a percentage, and that is a trap worth naming. The card is
       position: absolute, so a percentage padding resolves against its
       CONTAINING BLOCK's width, which is the full-width stage, not the
       square: 8% came out as 115px of a 594px card and ate the middle of
       it. A clamp tracks the same three limits the width does closely
       enough and cannot be misread. */
    padding: clamp(26px, 3vw, 46px);
    /* A SQUIRCLE, the same superellipse every other rectangular container
       on this page uses, so the curve eases into the straight edge rather
       than meeting it at a tangent. The radius goes up from the 22 the
       campaign banners carry, for the two reasons the convention above
       already states: a squircle reads tighter than an arc at the same
       value, and this is a much larger panel than a banner, so it needs
       more to keep the same visual weight. On a square in particular the
       superellipse is what makes it read as an object rather than a box.
       corner-shape is ignored where it is unsupported and those browsers
       keep the ordinary rounded corner, so no @supports guard is needed. */
    border-radius: 34px;
    corner-shape: squircle;
    /* NAVY, not the paper white it carried. A white panel here read as a
       dialog laid over the page rather than as one more object in the deck:
       every campaign card is saturated art and this was the one pale thing
       among them. The ground under this card is the brand gradient at full
       strength, coral into amber, so a deep cool blue is about as far from
       it as the wheel allows and the square lifts off the field the way the
       reference's covers do.
       The same value as the footer, so the two dark surfaces on the page
       are the same dark rather than two nearly-equal ones. */
    background: #1C2436;
    box-shadow:
      0 2px 8px rgba(28, 8, 16, 0.28),
      0 18px 40px rgba(28, 8, 16, 0.36),
      0 62px 112px rgba(28, 8, 16, 0.32);
    /* No artwork to tilt, so nothing here answers the pointer. */
    cursor: default;
  }
  /* The ink flips with the panel. Same family as the footer's, and
     measured on the same ground: solid is 13.7:1 here and 0.70 is 7.5:1. */
  .workdeck__card--panel .eyebrow {
    color: rgba(238, 241, 248, 0.70);
    margin-bottom: 12px;
  }
  /* ── The title card as a COVER ────────────────────────────
     Flat navy with a little centred type in the middle of it was a panel,
     not a cover: the campaign cards either side are full-bleed artwork and
     this was a slide. Three things fix it, and none of them invent content.

     The COMPOSITION first. space-between anchors the label to the top, the
     paragraph to the bottom and floats the heading between them, which is
     how a sleeve is set; centred in a column, all three sat together in the
     middle of a square that was two thirds empty.

     Pseudo-elements carry the decoration, so the copy has to be lifted over
     them: a positioned pseudo paints above in-flow text. The card already
     establishes a stacking context through its own transform, so plain
     z-index is enough. isolation would be the wrong tool here, since it is
     one of the grouping properties that force transform-style back to
     flat. */
  /* ── G · the artwork is the card ──────────────────────────
     The title sits INTO a piece of art rather than floating in an empty
     square. Top-anchored and ranged left, which hands the whole bottom
     right corner to the coin.
     align-items is deliberately left alone at stretch: flex-start would
     make each child fit-content, and a heading sized to its own longest
     line does not wrap at all, it overflows. text-align does the job. */
  .workdeck__card--intro {
    justify-content: flex-start;
    /* .work-card sets align-items: center further up, which is why the
       label and the paragraph sat centred under a ranged-left heading:
       both are narrower than the card, so both got centred as flex items
       while the heading, whose max-content is wider than the box, was
       clamped to the full width and looked left-aligned. Ranging the items
       themselves is what actually lines the three up.
       fit-content is not a risk here: under align-items: center the
       heading was already sized that way and wrapped correctly. */
    align-items: flex-start;
    text-align: left;
    /* Clips the coin to the card's own rounded edge, which is the whole
       composition: the mark runs OFF the corner instead of sitting inside
       it like a badge. Safe against transform-style here, because a text
       card has no slot to tilt - deck-gl.js returns null for any card
       carrying data-text - so there is no 3D child to flatten. */
    overflow: hidden;
  }
  .workdeck__card--panel > * { position: relative; z-index: 1; }
  /* THE GRAIN, and only the grain. This layer used to carry two soft lights
     as well, coral from the top left and amber from the bottom right, on
     the reasoning that a flat fill reads as a hole cut in the field rather
     than an object lying on it. The coral one read as a smudge behind the
     label rather than as light and it is gone. The amber one went with it
     for a different reason: once the coin grew to fill the bottom right
     corner it sat entirely behind the artwork and lit nothing at all.
     The grain stays. It is two octaves of SVG turbulence rather than a
     texture file, and it earns its place because the field behind the deck
     is grained by its own shader, so a glass-smooth panel against it looks
     pasted on. */
  .workdeck__card--panel::before {
    content: '';
    position: absolute;
    inset: 0;
    z-index: 0;
    border-radius: inherit;
    background-image: url("data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIxNDAiIGhlaWdodD0iMTQwIj48ZmlsdGVyIGlkPSJnIiB4PSIwIiB5PSIwIiB3aWR0aD0iMTAwJSIgaGVpZ2h0PSIxMDAlIj48ZmVUdXJidWxlbmNlIHR5cGU9ImZyYWN0YWxOb2lzZSIgYmFzZUZyZXF1ZW5jeT0iMC45IiBudW1PY3RhdmVzPSIyIiBzdGl0Y2hUaWxlcz0ic3RpdGNoIi8+PGZlQ29sb3JNYXRyaXggdHlwZT0ic2F0dXJhdGUiIHZhbHVlcz0iMCIvPjxmZUNvbXBvbmVudFRyYW5zZmVyPjxmZUZ1bmNBIHR5cGU9ImxpbmVhciIgc2xvcGU9IjAuMTAiLz48L2ZlQ29tcG9uZW50VHJhbnNmZXI+PC9maWx0ZXI+PHJlY3Qgd2lkdGg9IjE0MCIgaGVpZ2h0PSIxNDAiIGZpbHRlcj0idXJsKCNnKSIvPjwvc3ZnPg==");
    background-size: 140px 140px;
    background-repeat: repeat;
  }
  /* THE STICKER. The reference puts a round label on its covers; this is
     the page's own. The BaiSed coin, the same file the hero throws around,
     tilted and dropped into the corner. Gold on navy is the strongest pair
     on the card, which is what makes it read as applied rather than
     printed. Decorative, so it stays a pseudo-element and never reaches the
     accessibility tree. */
  .workdeck__card--intro::after {
    content: '';
    position: absolute;
    z-index: 0;
    right: -15%;
    bottom: -19%;
    width: 72%;
    aspect-ratio: 1 / 1;
    background: url("/assets/icons/coin.png") center / contain no-repeat;
    transform: rotate(-9deg);
    /* NO drop-shadow. The coin is drawn with its own rim and shading, and a
       filter on something this large would be re-rasterised on every frame
       the deck moves the card, which is every frame of the section. */
  }
  .workdeck__card--intro h2 {
    /* COVER SCALE. The first pass at the square took the heading DOWN, on
       the reasoning that a narrower box needs smaller type. What that
       produced was a caption adrift in a large empty square. A cover's
       title is its artwork, so this goes back up and the composition above
       gives it the room. */
    font-size: clamp(1.7rem, 3.2vw, 2.7rem);
    line-height: 1.08;
    letter-spacing: -0.025em;
    margin: 0.34em 0 0;
    color: #EEF1F8;
  }
  .workdeck__card--intro > p:not(.eyebrow) {
    /* Held clear of the coin. The coin's box starts 43% across the card, so
       the copy stops short of it rather than running over the rim. */
    max-width: 58%;
    margin: 1.1em 0 0;
    font-size: 0.95rem;
    line-height: 1.6;
    font-weight: 300;
    color: rgba(238, 241, 248, 0.70);
  }

  /* NOT A SQUARE. The square belongs to the title card, which is a cover
     and has artwork to fill one. This card's content is a portrait beside a
     paragraph, which is a wide, short shape: squared, it sat as a band
     across the middle of a box that was two thirds empty. Back to the
     panel's own proportions, sized by its content rather than by a ratio.
     aspect-ratio has to be reset explicitly, not just left out: it is
     inherited from .workdeck__card--panel above, which is where the square
     is declared for both text cards. */
  .workdeck__card--quote {
    width: min(700px, 56vw);
    aspect-ratio: auto;
    padding: clamp(30px, 3.6vw, 52px);
  }
  /* The quote arrived carrying its own card: a background, a border, a
     radius and 34px of padding, from the days when it was a figure sitting
     on the band. Inside the panel that is a card in a card, so all of it
     comes off and the panel is the only surface. Left-aligned, because it
     is a portrait beside a paragraph rather than a centred statement. */
  .workdeck__card--quote .quote {
    max-width: none;
    margin: 0;
    padding: 0;
    background: none;
    border: 0;
    border-radius: 0;
    text-align: left;
    gap: 24px;
  }
  .workdeck__card--quote blockquote {
    font-size: 1.02rem;
    line-height: 1.65;
    color: #EEF1F8;
  }
  .workdeck__card--quote figcaption { color: rgba(238, 241, 248, 0.70); }
  .workdeck__card--quote figcaption b { color: #EEF1F8; }
  /* The avatar placeholder's own label reads --faint, which is a night
     value in this section. */
  .workdeck__card--quote .img-slot--avatar {
    border-color: rgba(238, 241, 248, 0.26);
    background: rgba(238, 241, 248, 0.06);
  }
  .workdeck__card--quote .img-slot--avatar .img-spec { color: rgba(238, 241, 248, 0.62); }

  /* The lift on hover writes the same property the deck owns. The script's
     inline transform outranks it anyway; this keeps the two from
     disagreeing about what the card's resting state is. */
  .workdeck__card:hover { transform: translate(-50%, -50%); }
  .workdeck__card p { max-width: none; }

  /* ── The ground ──────────────────────────────────────────
     Three layers, matching the reference: an opaque FIELD edge to edge, a
     big portrait OVAL over it, and a soft RING of a third hue bleeding out
     of the oval into the field.

     The field is fully opaque on purpose. An earlier version ran the whole
     thing at partial alpha over the night sky, which kept the section in
     the weather but composited the colours down to about a fifth of their
     intensity — deep plum instead of red. Opaque means this screen stops
     being night for its duration: the star layer sits at z-index -1 and is
     covered, and so is the cloud tint over this section's opening.

     Colours arrive as bare "r g b" triplets, the same convention the
     section tints already use, so alpha stays a property of the layer
     rather than something baked into the value the script writes. */
  .workdeck__sky {
    display: block;
    position: absolute;
    z-index: 0;
    pointer-events: none;
    /* Overhangs the stage by FEATHER top and bottom, and the mask below
       fades exactly that overhang away.

       The stage is pinned for all but the first and last moments of the
       runway, and while it is pinned it measures exactly one viewport, so
       everything the mask softens is off-screen and the reader sees opaque
       colour into all four corners. The overhang only shows itself on the
       way in and the way out — and that is the point. Without it the field
       began at a razor-straight horizontal line sweeping up the window,
       slicing across the cloud border mid-scallop. A soft 160px leading
       edge instead of a hard one costs nothing while pinned.

       This also replaces the earlier overflow: hidden. The oval is taller
       than the window and has to be cut off top and bottom, but the WINDOW
       does that cutting on its own; overflow was only ever stopping the
       spill onto the sections either side, and the mask now ends the layer
       well before it could reach them. */
    inset: calc(var(--deck-feather) * -1) 0;
    /* Matches .band's 88px padding, so the overhang stops on the section's
       own edge instead of 72px inside the section above, and no mask, for
       the same reason as .workdeck__gl: the page behind this is the night
       sky, so fading the outer band out drew a dark stripe rather than a
       soft join. */
    --deck-feather: 88px;
    /* One solid colour, and it stays that colour for as long as a campaign
       is parked. The rings above it are what move. The colour rotation that
       used to live here moved every pixel of the section at once, which is a
       lot of motion to put behind a card someone is trying to read. */
    /* Two layers now, bottom to top: the base colour, then the wipe.

       The base is the colour the LAST wipe left behind. It does not
       crossfade with scroll any more: a campaign's colour arrives by being
       swept across the window and then stays, which is the whole point of
       the transition, and a base that was already crossfading underneath it
       would arrive twice. */
    background-color: rgb(var(--deck-field));
    background-image: radial-gradient(circle var(--deck-wipe-r) at 50% 50%,
      rgb(var(--deck-wipe)) 0 100%, transparent 100%);
    background-repeat: no-repeat;
    background-size: 100% 100%;
    /* Golden Mountain's palette, character for character the same values
       DECK_SKY[0] carries in lucky-draw-scenes.js. These are what paints
       between the stylesheet applying and the script executing, and they
       used to be an unrelated 214 48 62 red under a 126 118 240 purple —
       a combination that appears nowhere in the section once it is
       running. Reload while parked on the deck and the whole viewport
       went scarlet with a violet oval in it. Defaulting to the first
       campaign's real ground means that window paints the same picture
       the first scrolled frame does. */
    /* The conveyor. A circle is born at the centre as a point, grows one
       way until it is past the corners of the window, and is then reborn as
       a point. Two of them run half a cycle apart, so there is always a
       large one and a smaller one inside it, which is what makes the ground
       read as two circles rather than one.

       Nothing here ever shrinks or comes back. That was the mistake in the
       version before this: a symmetric breathe out and back is the one
       motion the reference never makes.

       max is the size at the END of a cycle, and it has to clear the
       window's corner or the circle would vanish while the field is still
       showing behind it. A 16:9 window needs about 115vw; 130 leaves room
       for taller ones. time is one circle's whole life, so a new one is
       born every half of it. */
    /* The ripple.

       seed is the diameter a disc is born at, and it has to be a little
       LARGER than a campaign card, not smaller. Born behind the card a disc
       is invisible until it has grown past it, and by then its partner is
       most of the way across the screen: the pair spends half its time
       looking like a single circle because only one of them is ever out
       where it can be seen. Born just proud of the card, both are visible
       from the moment they exist and you always see two.

       grow is how far it gets before it has finished fading, and at 3.7 that
       is past the edges of the window rather than stopping around the card.
       A disc that gives up at twice the card's width reads as a halo stuck
       to the card; one that runs off the screen reads as a wave leaving it,
       which is the difference between the two. The wipe is still the only
       thing that reaches every CORNER, and it is opaque all the way there,
       so the two do not read as the same event. */
    /* Born SMALL and grown a long way, rather than born nearly a screen
       wide and grown a little. A ring has to be seen arriving or the pair
       never reads as two waves.
       22 x 8 is a final diameter of 176vw, so the band at 78% of the
       radius passes 68vw from the centre — clear of the 59vw corner of a
       16:9 window, which is what "expands until it is off screen" needs.
       The cycle is longer to match: the same speed over four times the
       distance. */
    --deck-ripple-seed: 22vw;
    --deck-ripple-grow: 8;
    --deck-ripple-time: 6.4s;
    /* How far ABOVE the stage's centre the ripple is anchored.
       A campaign card's box is its artwork plus the title, caption and terms
       link underneath, so the box's centre sits well below the middle of the
       ARTWORK. Anchored to the box, as this was, the ripple looked low: the
       card appeared to hang slightly above a shape that was supposed to be
       coming out from behind it.
       69px, and it is exactly half the text block: the artwork sits flush
       with the card's top, so the gap between the two centres is
       (cardHeight - artworkHeight) / 2 and nothing else. That is why this is
       a fixed length rather than a share of the card. The artwork scales
       with the card's width, the text underneath does not, so the difference
       between their centres barely moves at any width the deck runs. */
    --deck-ripple-lift: 69px;
    --deck-wipe-time: 700ms;
    /* How far the colour wipe travels. A circle of this RADIUS covers a
       window whose corners are half a diagonal away several times over,
       which is the point: it has to leave no corner behind, and it has to
       still be growing when it does, or the sweep reads as a circle that
       stopped rather than a colour that took over. 0.75 of the diagonal is
       a diameter of 1.5 diagonals. */
    --deck-wipe-max: calc(0.75 * hypot(100vw, 100vh));
    /* The colour being wiped IN. Held at the current field colour so that a
       wipe which somehow runs without being handed a new one is invisible
       rather than a flash of something unrelated. */
    --deck-wipe: var(--deck-field);
    --deck-wipe-r: 0px;
    --deck-field: 108 92 176;
    --deck-oval: 214 202 246;
  }
  /* ONE disc, expanding and dissipating, born behind the card and gone by
     the time it clears the window.

     There used to be two, a lighter one inside a darker one, and both were
     flat colour on a border-radius box so each had a hard edge. That gave
     the section three tones stacked on two visible circle borders. It now
     carries one: a single colour per campaign for the ground, and one disc
     over it that runs out to nothing at its own rim, so what a reader sees
     is one wash gathering toward the middle rather than plates stacking.

     A hard-edged disc did have one real advantage, and losing it is the
     trade: the browser antialiases an element's own border, so a flat fill
     on a border-radius box is crisp at every size the disc passes through,
     which no gradient matches. The gradient's falloff has to be tuned
     against the grow factor instead — see the note on ::before. */
  /* Added by the script for the length of one wipe and then taken off
     again, which is what restarts the animation. */
  .workdeck__sky.is-wiping {
    animation: deckWipe var(--deck-wipe-time) cubic-bezier(0.22, 0.61, 0.36, 1) forwards;
  }

  .workdeck__sky::before,
  .workdeck__sky::after {
    content: '';
    position: absolute;
    border-radius: 50%;
    /* Anchored to a fixed point, dead centre, which is directly behind the
       card. Centred by anchor plus `translate` rather than inset:0 with
       margin:auto, because margin:auto only centres a box that FITS its
       containing block and a ring at full stretch is wider than the window.

       `translate` is the standalone property, so it composes with the
       `scale` the ripple and the handover both drive instead of fighting
       it. */
    top: calc(50% - var(--deck-ripple-lift));
    left: 50%;
    translate: -50% -50%;
    /* Both axes off the same value, so it is a circle on any window. A
       diameter fed from a different basis per axis is not a circle on
       anything but a square screen. */
    width: var(--deck-ripple-seed);
    height: var(--deck-ripple-seed);
    /* `backwards` matters. After a campaign switch the second disc is put
       BEHIND its own start so it is born half a cycle later than the first
       rather than arriving half grown. During that wait it is in the
       animation's before-phase, and without a backwards fill the before-
       phase applies no keyframe at all: the disc would sit at its plain CSS
       size, solid and visible, which is the exact thing the wait is there to
       avoid. With it, the before-phase holds the 0% keyframe, which is
       opacity 0. */
    animation: deckRipple var(--deck-ripple-time) linear infinite backwards;
  }

  /* The OUTER disc: the campaign's accent pulled a third of the way back
     toward its field colour. The two discs used to take two different
     entries straight out of the palette, and on a campaign whose entries
     happen to sit close together, Golden Mountain's peach and gold being the
     worst case, there was no visible step between them at all: two discs
     were painting and a reader saw one circle.

     Deriving the outer from the SAME hue as the inner and darkening it a
     fixed amount guarantees the step exists on every campaign, and keeps the
     pair reading as one colour at two depths rather than as two unrelated
     colours that happen to be stacked. */
  .workdeck__sky::before,
  .workdeck__sky::after {
    /* AN ANNULUS. Transparent in the middle, a band of colour, transparent
       again outside it.

       It was a FILLED disc, and that is why there only ever appeared to be
       one ring no matter how many were running: two filled discs of the
       same colour, one inside the other, composite into a single larger
       filled disc. The inner one contributes nothing a reader can see, so
       all that ever read was the outer one's edge. Two rings need two
       visible bands, which means the middle has to be empty.

       Peak at 78% of the radius with the falloff either side of it, so the
       band is soft on both edges and never draws as a stroked circle. */
    background-image: radial-gradient(closest-side circle,
      rgb(var(--deck-oval) / 0) 0 44%,
      rgb(var(--deck-oval) / 0.18) 56%,
      rgb(var(--deck-oval) / 0.62) 68%,
      rgb(var(--deck-oval)) 78%,
      rgb(var(--deck-oval) / 0.48) 88%,
      rgb(var(--deck-oval) / 0) 100%);
  }
  /* TWO RINGS, ALWAYS. One disc alone leaves the ground empty for the
     stretch between a wave dissolving at the rim and the next being born,
     and the section reads as a pulse rather than as water.

     Half a cycle apart, so there is always a large one on its way out and
     a smaller one behind it. The delay is NEGATIVE: a positive one would
     leave this ring unborn for its first 2.6s, which is exactly the empty
     stretch the second ring exists to fill. Negative starts it already
     mid-cycle on the very first frame.

     resetDeckWaves() in lucky-draw-scenes.js reads this delay back and
     seeds the ring to 2x it after a campaign switch, which is what makes
     the pair restart staggered instead of both arriving together. */
  .workdeck__sky::after {
    animation-delay: calc(var(--deck-ripple-time) / -2);
  }
}

/* A length has to be REGISTERED to animate. An unregistered custom property
   animates discretely, so the wipe would jump from nothing to full size at
   the midpoint instead of sweeping. Top level because @property cannot be
   nested in a media query. */
@property --deck-wipe-r {
  syntax: '<length>';
  inherits: false;
  initial-value: 0px;
}

/* The colour wipe. A circle of the incoming campaign's colour, from a point
   hidden behind the card out to well past every corner. `forwards` holds it
   at full size until the script swaps the base colour underneath it and
   takes the class off, so there is never a frame where the old colour comes
   back. Decelerating: it should arrive, not accelerate away. */
@keyframes deckWipe {
  from { --deck-wipe-r: 0px; }
  to   { --deck-wipe-r: var(--deck-wipe-max); }
}

/* One ring's whole life. Born at seed size behind the card at full
   strength, expanding outward and thinning as it goes, gone by the time it
   clears the window. Then straight back to the centre to do it again.

   Full opacity from 4% to 88%, so a disc is solid for all but the ends of
   its life. Earlier versions started at half strength and were down to a
   quarter by the midpoint, which is why the discs never had a visible
   border: a disc that is translucent for its whole life has no edge at any
   point in it.

   Full opacity from the moment it has size until 94%, so a disc wears ONE
   colour for effectively its whole life. That is the point of holding it so
   late: opacity is what makes a colour change, because a disc at 0.5 is half
   its own colour and half whatever is behind it. Fading across the last
   quarter, as this used to, meant the outer disc visibly washed from its own
   hue toward the field's on every single cycle, and what a reader sees is
   the ripple changing colour rather than the ripple leaving.

   What is left is a 6% exit, 312ms at the current speed, which reads as the
   wave passing rather than as it discolouring. It cannot go to zero: an
   opaque disc this size has to stop being drawn somewhere, and a hard cut
   would be a full width flash.

   A disc is born at scale ZERO and reaches its resting size over the first
   nine percent. It used to be born at that size outright, which meant it did
   not emerge from anywhere: a ring simply existed, already proud of the card
   on all sides. Growing out of a point puts the birth BEHIND the card, where
   it belongs, and the disc comes out from under it.

   Nine percent is 288ms, and the disc clears the card's own height about
   half way through that, so it is only genuinely hidden for around 4% of its
   life. That is the whole cost of the change, and it buys a birth that reads
   as coming out of the campaign rather than appearing beside it.

   No fade in either, and none is needed now: at 0% the disc has no size at
   all, so there is nothing to pop.

   The z-index flip is what keeps a PAIR of discs legible. The younger,
   smaller one is always on top; each drops behind at its own midpoint so its
   partner can rise over it in turn. Without it the older and larger disc
   paints over the younger and there is only ever one circle to see.

   No fade IN, and none is needed. At 0% the disc is about one card wide and
   sits directly behind the card, so it is born out of sight. */
/* Scale runs 1 to grow across the WHOLE cycle, so the ring expands at one
   steady rate for its entire life. It used to snap 0 to 1 in the first 9%
   and then creep, which reads as a circle appearing and then being resized
   rather than as a wave travelling.

   Opacity only tidies the two ends: in over the first 7% so it does not
   pop into being at its birth size, out over the last 14% so whatever is
   still in the corners when it resets goes quietly. In between it is fully
   opaque and the ring leaves by travelling off the screen. */
@keyframes deckRipple {
  0%     { scale: 1; opacity: 0; }
  7%     { opacity: 1; }
  86%    { opacity: 1; }
  100%   { scale: var(--deck-ripple-grow); opacity: 0; }
}

@media (prefers-reduced-motion: reduce) {
  /* No ripple and no handover: one solid colour and the disc
     held part way out, so the section still has a shape in it. The script
     checks the same query before it animates the handover. */
  .workdeck__sky,
  .workdeck__sky::before,
  .workdeck__sky::after { animation: none; }
  /* Held at two different sizes rather than one, so the still frame keeps
     the two-ring reading the motion has. */
  .workdeck__sky::before { scale: 5.4; opacity: 0.42; }
  .workdeck__sky::after  { scale: 2.6; opacity: 0.5; }
}


/* ── An empty slot paints NOTHING ────────────────────────────
   Until the artwork has actually decoded, the slot shows no plate, no
   shadow, no border and no image: just the ground behind it. An empty
   rounded rectangle carrying a big soft drop shadow reads as a modal that
   failed to open, and that is what a reader sees for as long as the lazy
   images take to arrive — which on the deck is until they scroll to it.

   The state is opt-IN, and that is deliberate. CSS cannot ask whether an
   <img> has pixels; :has(> img) is true from the first paint, long before
   anything has loaded. So the class is written by the script, and its
   ABSENCE means "behave exactly as before" — any page that uses these
   slots without lucky-draw-scenes.js is untouched rather than silently
   hiding its own artwork.

   Placed after the deck block on purpose: the deck's own shadow rule has
   the same specificity, so this has to come later in source order to win.
   ─────────────────────────────────────────────────────────── */
.work-card__img.is-waiting {
  background: none;
  box-shadow: none;
  border-color: transparent;
}
.work-card__img.is-waiting > img { opacity: 0; }
/* Fades in rather than popping, once there is something to fade. */
.work-card__img > img { transition: opacity 0.45s ease; }
@media (prefers-reduced-motion: reduce) {
  .work-card__img > img { transition: none; }
}

/* Below 901 and under reduced motion the deck is never built, so the cards
   need a layout of their own: the plain stacked column they had before. */
@media (max-width: 900px) {
  .workdeck__stage { display: grid; gap: 32px; }
}
@media (prefers-reduced-motion: reduce) {
  .workdeck { height: auto; }
  .workdeck__stage { position: static; height: auto; display: grid; gap: 32px; }
  .workdeck__card { position: static; width: auto; transform: none; }
}


/* ══════════════════════════════════════════════════════════
   THE PICK BAND · one wave across two sections
   The cards at the top of it are a control, and #celebrity is what they
   control. Wrapping the pair is what lets one animation cross the seam
   between them: an overlay in either section alone would stop at that
   section's edge, which is exactly where the interesting half of the
   journey starts.
   ══════════════════════════════════════════════════════════ */
/* A STACKING CONTEXT ON PURPOSE. The three layers below only stay in
   their intended order if the band owns the order, and z-index on a
   positioned element is what buys that. Nothing inside is position:fixed,
   so nothing is trapped by it. */
.pickband { position: relative; z-index: 0; }

/* THE GROUND THAT ANSWERS THE PICK, and it is BOTH sections, not just
   the lower one. #channels-fit and #celebrity are deliberately the same
   white block, stated twice so neither follows the day-to-night driver,
   and the point of that is they read as one unbroken run. A mode that
   moved only the bottom half would split exactly the run those two
   blocks were matched to make, and the seam would be back the first time
   anyone clicked.

   Cream rather than a stronger amber: every token both blocks set is
   pitched at white, and this keeps all of them passing. The near-black
   ink measures 18:1 on it, the muted tier 7:1, and the stepped-down red
   4.83:1, so the second mode needs no second set of tokens. */
/* BOTH grounds are now the picked card's own hue rather than one cream and
   one plain white. Derived, not chosen: the dominant saturated colour in
   each card's artwork is (200,140,0) for the draw and (80,20,160) for the
   fan, and each is mixed into white until it is plainly that hue and no
   further than the type allows - 22% for the amber, 18% for the violet.
   The violet has to stop sooner because it is the darker of the two and
   the small-caps tier runs out of contrast first.
   Declared as a PAIR of named grounds rather than only as the live value,
   because lucky-draw-scenes.js has to paint the wave in the colour the
   band is about to settle on, and it now reads these instead of carrying a
   second copy of the hexes that could drift from these. */
.pickband {
  --ground-fan: #E0D5EE;
  --ground-draw: #F3E6C7;
  --pick-ground: var(--ground-fan);
}
.pickband[data-mode="draw"] { --pick-ground: var(--ground-draw); }

/* THE GROUND, LIFTED OFF THE SECTIONS. Both of them are transparent now
   and this one layer carries the colour for the pair, which is what lets
   the wave be slid in between the ground and the content instead of
   riding over the top of everything.

   It also makes the two blocks one surface in fact and not only by
   agreement: they were matched white-for-white and had to be kept in step
   by hand, and a single painted layer cannot drift. */
.pickband__ground {
  position: absolute;
  inset: 0;
  z-index: 0;
  pointer-events: none;
  background: var(--pick-ground);
  /* Short, because it no longer has to hide anything: the script commits
     the colour only once the fill is over every visible pixel, so the
     change happens under a layer already wearing it. What is left for
     this to do is soften the part of the band below the fold, and the
     no-script and reduced-motion cases where there is no fill at all. */
  transition: background-color 0.3s ease;
}
/* Transparent over that layer, and lifted above the wave.

   Scoped through .pickband rather than left as bare ids, and that is not
   tidiness: both sections state their own background further DOWN this
   file, an id against an id, so a tie on specificity would be settled by
   source order and they would win. The descendant class breaks the tie
   the right way round and says the true thing at the same time - inside a
   band these are transparent, and on their own they are still the white
   block each of them states. */
.pickband #channels-fit,
.pickband #celebrity { background: transparent; position: relative; z-index: 3; }

/* THE WAVE. Two layers of it, inert to the pointer so it can cross the
   cards and the roster without swallowing a click meant for either.

   Both are clipped to the same expanding rounded rectangle, struck on the
   card's own box, and the crest is simply given less time to cover the
   same distance so it stays in front. The strip between the two is the
   only part of the crest that is ever visible, and that strip is the
   ripple.

   Neither carries a gradient any more. There was a radial one centred on
   the card, which did nothing useful once the shape stopped being a
   circle: its rings were fixed in the element while the clip grew past
   them, so the soft edge it was meant to give sat still while the wave
   moved. The crest replaces it and actually travels. */
.pickband__wave {
  position: absolute;
  inset: 0;
  pointer-events: none;
  opacity: 0;
  /* Parked on nothing until a sweep writes real keyframes over it. The
     shape is built in lucky-draw-scenes.js, off the card's own box,
     because only the script knows where the card is. */
  clip-path: inset(50% 50% 50% 50% round 22px);
}
/* Under the fill, so the fill eats it from behind as it catches up. */
.pickband__wave--crest { z-index: 1; background: var(--crest-tint, #FFB765); }
/* Over the crest, under everything the sections draw. */
.pickband__wave--fill  { z-index: 2; background: var(--wave-tint, #FFF4E6); }

/* THE PICKED CARD. A ring rather than a fill: the art is the object and a
   tint over it would be reading the selection off the artwork instead of
   off the frame around it.

   On .pick, not on .pick__front. The card stopped being two faces when
   the turn was removed and is now one object, art joined to copy, so a
   ring around the art alone drew a line through the middle of the thing
   it was supposed to be selecting. Matching radius, so it traces the
   card's own outline rather than boxing it. */
.pick { border-radius: 22px; transition: box-shadow 0.3s ease; }
.pick.is-picked {
  box-shadow: 0 0 0 3px var(--pick-ring, #C44E34),
              0 18px 40px rgba(120, 72, 26, 0.24);
}
.pick[data-campaign="celebrity"].is-picked { --pick-ring: #7154C6; }

/* No wave under reduced motion. The mode still changes and the colour
   still lands; what goes is the thing that sweeps. */
@media (prefers-reduced-motion: reduce) {
  .pickband__wave { display: none; }
  .pickband__ground { transition: none; }
}

/* A select still resting on its placeholder reads as unanswered, not as
   an answer, so it is greyed until a real option is chosen. */
.field select:has(option[value=""]:checked) { color: var(--faint); }

/* ── The GL ground, wake and pill (lucky-draw-deck-gl.js) ─────
   On desktop, with WebGL, and not under reduced motion, the script puts a
   canvas behind the cards and sets .deck-gl on <html>. Under that class the
   CSS ovals above yield to the canvas, the captions fold into the pill
   under the centred card (they stay in the DOM for readers and search),
   and the artwork answers to the script's springs rather than the 0.2s
   transition. Everything in this block is scoped to that class or to
   elements the script creates, so a page without the script, or a device
   without WebGL, never sees any of it. */
@media (min-width: 901px) {
  .deck-gl .workdeck__sky { display: none; }
  .workdeck__gl {
    position: absolute;
    z-index: 0;
    pointer-events: none;
    /* The overhang, and it is not a taste call: .band pads this section by
       exactly 88px, so the pinned stage sits 88px inside it at every width
       the deck runs (the only override is below 760px, where the deck is a
       plain column anyway). Overhanging by that exact amount means the
       ground reaches the section's own top and bottom edges while pinned
       and stops dead on them, and the clip in lucky-draw-deck-gl.js is the
       guarantee if that padding ever moves.

       NO MASK. This used to fade its outer 88px away at each end, which is
       the obvious way to avoid a hard horizontal edge — but what sits
       behind this canvas is the page's night sky, not the section above,
       so the fade did not soften the join at all. It revealed a dark band
       across the top of the section for as long as the stage had not
       pinned yet, and read as a dark border drawn under the ripple. Opaque
       to both edges instead: what the reader sees at the join is one
       colour meeting another, which is what a section boundary is. */
    --deck-feather: 88px;
    inset: calc(var(--deck-feather) * -1) 0;
    width: 100%;
    height: calc(100% + 2 * var(--deck-feather));
    /* The first campaign's ground, so the frame before the script's first
       paint is the colour the first painted frame will be. */
    background: rgb(108 92 176);
  }
  /* :not(--panel), because a text card IS its text. Everything else on the
     deck hands its caption to the pill. Two cards are panels: the heading
     that opens the deck and the quote that closes it. */
  .deck-gl .workdeck__card:not(.workdeck__card--panel) > h3,
  .deck-gl .workdeck__card:not(.workdeck__card--panel) > p,
  .deck-gl .workdeck__card:not(.workdeck__card--panel) > .card-link {
    position: absolute;
    width: 1px; height: 1px;
    margin: -1px; padding: 0; border: 0;
    overflow: hidden;
    clip: rect(0 0 0 0);
    white-space: nowrap;
  }
  .deck-gl .workdeck__card .work-card__img {
    transition: none;
    cursor: pointer;
  }

  /* The pill. Sized and placed as the reference's chapter button: a 60px
     dark capsule at 16% from the bottom of the stage, under its own 500px
     perspective so the rotateX on the way in and out reads as a flip. */
  .deckpill-wrap {
    position: absolute;
    left: 0; right: 0; bottom: 16%;
    z-index: 30;
    display: flex;
    justify-content: center;
    perspective: 500px;
    pointer-events: none;
  }
  /* display:flex above would outrank the hidden attribute the script sets
     when it hands the section back after a lost context. */
  .deckpill-wrap[hidden] { display: none; }
  /* A DISC THAT OPENS. Collapsed it is 60px square with three stacked
     dashes in it and nothing else; open it is the 350px pill it used to be,
     in a pastel, carrying the campaign's lines. Width and colour are the
     only things that move, so the two states are the same object rather
     than two controls. The transition is on width and background alone —
     not `all`, which would fight the entrance transform the script writes
     from. */
  /* NOT THE PILL ANY MORE. The black capsule moved onto the button itself,
     which is a disc; this is just the row that stands the disc next to the
     two arrows. Transparent, no radius, no width of its own — so nothing
     here can read as a long rectangle, which is what it did when the disc
     and the arrows shared one background. */
  .deckpill {
    pointer-events: auto;
    position: relative;
    display: flex;
    align-items: center;
    height: 60px;
    color: #fdede2;
    box-shadow: 0 18px 40px -18px rgba(28, 8, 16, 0.6);
    transform-origin: 50% 50%;
    will-change: transform, opacity;
    /* The script animates from exactly this pose. */
    opacity: 0;
    transform: translateY(100px) rotateX(-100deg) scale(0);
  }
  /* A BUTTON NEEDS ITS RESET. This was an <a> and the styling assumed it;
     as a <button> the UA default put a white box with a border round the
     whole thing, which is what the pill looked like before this line. */
  /* A BUTTON NEEDS ITS RESET. This was an <a> and the styling assumed it;
     as a <button> the UA default put a white box with a border round it,
     which is what the control looked like before this reset. */
  .deckpill__link {
    appearance: none;
    border: 0;
    font: inherit;
    cursor: pointer;
    flex: none;
    width: 60px;
    height: 60px;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: 50%;
    background: #191819;
    color: #fdede2;
    box-shadow: 0 18px 40px -18px rgba(28, 8, 16, 0.6);
    text-decoration: none;
    transition: transform 0.3s cubic-bezier(0.2, 0.7, 0.3, 1);
  }
  .deckpill__link:hover { transform: scale(1.06); }
  /* ── the two states ──────────────────────────────────────
     Closed: the dashes, centred, and the texts are not merely hidden but
     zero-width, so they cannot hold the disc open from the inside. */
  /* The texts and the arrows are out: nothing in a 60px disc can hold three
     lines of copy, and the arrows would push it back into a capsule.
     ⚠ That loses the prev/next buttons. The deck is scroll-driven so it is
     still fully navigable, but they were a convenience and they are gone. */
  /* IT NEVER WIDENS. The disc is the whole control: it used to expand into
     the 350px pill and carry the campaign's lines, and the lines live on the
     card's own back face now, so there is nothing for it to widen for. */
  /* THREE STACKED DASHES, drawn rather than typed: three dashes as text
     would inherit the font's own spacing and sit unevenly, and this way the
     gap is stated. currentColor, so it flips with the pill. */
  .deckpill__dashes {
    display: flex;
    flex-direction: column;
    gap: 4px;
    align-items: center;
  }
  .deckpill__dashes i {
    display: block;
    width: 18px;
    height: 2px;
    border-radius: 2px;
    background: currentColor;
  }
  /* The play triangle went with the texts: a disc carries the dashes. */
  .deckpill__play { display: none; }
  .deckpill__link:focus-visible { outline: 2px solid #fdede2; outline-offset: 4px; border-radius: 30px; }
  .deckpill__play {
    flex: none;
    width: 0; height: 0;
    border-left: 18px solid #fdede2;
    border-top: 11px solid transparent;
    border-bottom: 11px solid transparent;
    transition: transform 0.3s;
  }
  .deckpill__link:hover .deckpill__play { transform: scale(1.15); }
  /* GONE, with the arrows below. Nothing in a 60px disc can hold three
     lines, and this rule is further down the file than the hide that used
     to sit up by .deckpill__link — equal specificity, later wins, so the
     texts stayed laid out inside the disc and were only being clipped by
     its overflow. Declared dead here instead, where it cannot be outranked.
     The ARROWS go too. Asked for explicitly: the disc carries the three
     dashes and nothing else. The deck stays navigable — it is scroll-driven
     and always was, the arrows were a convenience on top of that. They are
     display: none rather than out of the markup because fillPill() sets
     their disabled state and still expects to find them; putting them back
     is this one line. */
  .deckpill__texts { display: none; }
  .deckpill__line { display: block; overflow: hidden; line-height: 1.15; white-space: nowrap; text-overflow: ellipsis; }
  .deckpill__line i { display: block; font-style: normal; transform: translateY(100%); }
  .deckpill__title { font-size: 15px; font-weight: 800; letter-spacing: 0.01em; }
  .deckpill__sub { font-size: 12px; opacity: 0.85; }
  .deckpill__cta { font-size: 10.5px; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; opacity: 0.75; }
  .deckpill__nav { display: none; gap: 2px; margin-left: 0; flex: none; }
  .deckpill__arrow {
    appearance: none;
    background: none;
    border: 0;
    padding: 5px;
    margin: 0;
    color: #fdede2;
    cursor: pointer;
    display: flex;
    border-radius: 6px;
    transition: transform 0.2s, opacity 0.2s;
  }
  .deckpill__arrow svg { width: 13px; height: 12px; display: block; }
  .deckpill__arrow:hover { transform: scale(1.15); }
  .deckpill__arrow:focus-visible { outline: 2px solid #fdede2; outline-offset: 1px; }
  .deckpill__arrow[disabled] { opacity: 0.45; cursor: default; transform: none; }
}
@media (max-width: 900px) {
  .workdeck__gl, .deckpill-wrap { display: none; }
  /* Below the deck's breakpoint the cards are a plain column, and card one
     is simply the section's heading at the top of it. It carries the
     sec-head measurements the block above the deck used to have, since
     that block is gone. */
  .workdeck__card--intro {
    max-width: 720px;
    margin-bottom: 44px;
    padding: 0;
    background: none;
    /* No text-align here on purpose. The column is a centred flex stack and
       every campaign under this one centres its title and copy, so the
       heading centres with them. Forcing it left put the eyebrow, which is
       narrow enough to shrink to its text, in the middle of a left-aligned
       block. */
  }
  .workdeck__card--intro h2 {
    font-size: clamp(1.7rem, 3.4vw, 2.7rem);
    margin-bottom: 14px;
  }
  .workdeck__card--intro > p:not(.eyebrow) {
    color: var(--muted);
    font-weight: 300;
    font-size: 1rem;
    margin: 0;
  }
}


/* ══════════════════════════════════════════════════════════
   WARM GRADIENT GROUND
   The page ran on a flat near-white. It now sits on a pastel
   sweep through the brand's own three hues — red, orange,
   yellow — dropped to tint strength so type keeps its contrast
   and the white cards read as raised against it.

   Painted on a fixed pseudo-element rather than with
   background-attachment: fixed, which iOS Safari mishandles and
   which repaints badly behind sticky scenes. --bg stays a flat
   neutral because form fields still use it as a fill.
   ══════════════════════════════════════════════════════════ */
body::before {
  content: '';
  position: fixed;
  inset: 0;
  /* -3, the floor. Everything else in the negative layer moved down one step
     so that .whystack__wash could be left alone at -1 and finally clear the
     cloud canvas; this had to move with them to stay underneath. */
  z-index: -3;
  pointer-events: none;
  background: linear-gradient(165deg,
    #FFEDE9 0%,
    #FFE1D3 24%,
    #FFDCC0 48%,
    #FCEFC8 76%,
    #FAF6E6 100%);
}

/* Alternating bands were a solid grey, which would have hidden the
   gradient wholesale. A translucent white veil keeps the alternation
   legible while the colour still shows through. */
.band--alt { background: rgba(255, 255, 255, 0.42); }

/* The nav was a near-opaque near-white slab; over a warm ground it
   needs to be lighter and warmer or it reads as a grey bar. */
#nav { background: transparent; backdrop-filter: none; -webkit-backdrop-filter: none; }
/* The bar never paints, stuck or not, so the ground reads straight through
   it at every scroll position. It used to fill with --nav-bg (and frost what
   was behind it) the moment the reader moved off the top; that slab is what
   made the nav read as a separate strip laid over the page.

   --nav-bg is still written by the night script and still drives the lang
   pill, so the bar keeps making the same day-to-night journey. It just does
   it through the LINK colours (--nav-fg) now rather than a filled panel. */
#nav.stuck {
  background: transparent;
  backdrop-filter: none;
  -webkit-backdrop-filter: none;
  border-bottom-color: transparent;
  box-shadow: none;
}

/* OVER A DARK SECTION. The bar is transparent everywhere, so its links are
   only ever as readable as the ground behind them, and --nav-fg is written
   once on :root by the night driver for a page that used to be light the
   whole way down. #why turns near-black under the bar now, so a light set is
   swapped in for exactly as long as that ground is the thing underneath —
   declared here rather than by overwriting --nav-fg, so the night driver
   keeps ownership of its own variable. */
#nav.on-dark .nav-links a { color: rgba(244, 246, 251, 0.82); }
#nav.on-dark .nav-links a:hover {
  color: #fff;
  background: rgba(255, 255, 255, 0.14);
}

/* ── S3 · THE THREE REASONS ─────────────────────────────── */
.reasons-stage { width: 100%; max-width: 1000px; }
.reasons-eyebrow {
  font-size: .6rem; letter-spacing: .2em; text-transform: uppercase;
  color: var(--faint); margin-bottom: 34px;
}
.js .reasons-eyebrow { opacity: 0; }
.reasons {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: 22px;
  /* stretch, not start: the three bodies are different lengths, so with
     start the cards ended at three different heights and the row read as
     ragged rather than as one set. */
  align-items: stretch;
}
.reason {
  position: relative;
  /* A flat white panel sat cold on the warm gradient. The faint peach at
     the bottom lets the card settle into the background instead of being
     cut out of it. */
  background: linear-gradient(180deg, #fff 0%, #FFFBF8 100%);
  border: 1px solid var(--line);
  border-radius: 26px;
  corner-shape: squircle;
  /* Top clears the badge, which overhangs by 26px, and leaves a gap under
     it before the heading. Sides were 24px, which crowded the centred body
     copy against the card edge. */
  padding: 58px 28px 32px;
  text-align: center;
  /* Two warm-tinted layers: a tight contact shadow to seat the card and a
     wide soft one for depth. The old single navy shadow was the one cool
     note in a warm section. */
  box-shadow:
    0 2px 6px rgba(150, 82, 60, .05),
    0 18px 44px rgba(150, 82, 60, .10);
  transition: box-shadow .35s ease, border-color .35s ease;
  will-change: transform, opacity;
}
/* Hover deliberately moves no transform. updateReasons() writes
   element.style.transform on every scroll frame, so a transform here would
   be overridden by that inline value and the lift would never show. */
@media (hover: hover) {
  .reason:hover {
    border-color: rgba(255, 107, 107, .28);
    box-shadow:
      0 2px 6px rgba(150, 82, 60, .06),
      0 26px 60px rgba(150, 82, 60, .16);
  }
}
.js .reason { opacity: 0; }
.reason__tab {
  position: absolute;
  top: -26px; left: 50%;
  transform: translateX(-50%);
  width: 66px; height: 66px;
  border-radius: 22px;
  corner-shape: squircle;
  display: flex; align-items: center; justify-content: center;
  font-size: 1.72rem;
  /* The ring is the card colour, so the badge reads as seated on the card
     edge rather than as a sticker floating over it. */
  border: 4px solid #fff;
}
/* Each badge casts its own hue, which ties the icon to the card without
   needing a coloured border or header band. */
.reason__tab--red {
  background: linear-gradient(150deg, #FF6B6B, #FF8A6B);
  box-shadow: 0 10px 24px rgba(255, 107, 107, .34);
}
.reason__tab--orange {
  background: linear-gradient(150deg, #FF9F4A, #FFBD71);
  box-shadow: 0 10px 24px rgba(255, 159, 74, .32);
}
.reason__tab--yellow {
  background: linear-gradient(150deg, #F4C651, #FF6B6B);
  box-shadow: 0 10px 24px rgba(228, 178, 52, .30);
}
.reason h2 { font-size: 1.28rem; font-weight: 700; margin-bottom: 12px; letter-spacing: -.02em; }
.reason p { font-size: .88rem; color: var(--muted); line-height: 1.7; }

@media (max-width: 860px) {
  .reasons { grid-template-columns: 1fr; gap: 40px; }
  /* The badge is the same size here, so 46px left only a 6px gap between
     it and the heading. */
  .reason { padding: 54px 22px 28px; }
  .scene--s3 .sticky-frame { height: auto; position: relative; padding: 90px 6vw; }
  .scene--s3 { height: auto; }
  .reason, .reasons-eyebrow { opacity: 1 !important; transform: none !important; }
}
@media (prefers-reduced-motion: reduce) {
  .reason, .reasons-eyebrow { opacity: 1 !important; transform: none !important; }
}

/* ── S1b · THE CLAIM ────────────────────────────────────────
   States the promise before the map spells out what it covers.
   ────────────────────────────────────────────────────────── */
/* The runway is the whole pacing control: progress is normalised, so a
   stage that occupies a fifth of it takes a fifth of however many pixels
   this is. 180vh left 80vh of runway and the entire camera move was over
   in one flick of the wheel; 340vh was still brisk; 620vh got the approach
   right but left no room to sit in the arrival. 860vh gives 760vh, about
   seven and a half screens: the camera move finishes by two thirds and the
   last third is a lit hold you can keep scrolling through. */
.scene--claim { height: 860vh; }
/* The warp fills the frame behind the claim. The claim needs its own
   layer and a promotion hint: the driver writes a transform on it every
   frame while the pointer moves, and an unpromoted text block that size
   repaints the whole band each time. */
/* Stack, bottom to top: the dark interior (0), the box (1), the light
   field (2), the claim (3).
   The box never fades. It grows until the hole in its lid is wider than
   the screen, and that is the whole transition — an opaque lid with an
   opening in it, brought close enough that the opening is all there is.
   An earlier cut faded the box out over the dark sheet instead, which
   turned the cream lid to grey mud on the way through.
   The lights sit ABOVE the box because they only switch on once the hole
   has passed the corners, by which point there is no lid left on screen
   for them to draw over. */
.warp-inside {
  position: absolute; inset: 0;
  background: #0C0916;
  pointer-events: none;
  z-index: 0;
  opacity: 0;
  will-change: opacity;
}
/* PORTRAIT COVER. The lid is a landscape face, and a phone is not: at the
   scale where the lid spans a phone's width it is still a band a third of
   the window tall, with the page showing above and below it until it has
   grown another three times. This is the lid's own cream, full-bleed, and
   the driver brings it up as the lid crosses the window's width, so what
   shows around the band is the lid going on rather than the page behind
   it. Same layer as the dark sheet and earlier in the DOM, so the dark
   paints over it on the way in and uncovers it on the way out. On a
   landscape window the lid clears the height before the width, so this is
   never seen there. */
.warp-lid {
  position: absolute; inset: 0;
  background: linear-gradient(150deg, #fffdfb, #f0e7dd);
  pointer-events: none;
  z-index: 0;
  opacity: 0;
  will-change: opacity;
}
.warp {
  position: absolute; inset: 0;
  width: 100%; height: 100%;
  pointer-events: none;
  z-index: 2;
}
.claim-wrap {
  position: relative; z-index: 3;
  will-change: transform, opacity;
}
/* Ink on cream cannot survive the black interior, so the claim flips to
   cream while the scene is dark. The gradient phrase keeps itself. */
.warp-dark .claim-wrap { color: #FBF4EC; }
.claim-wrap { transition: color 0.3s ease; }
/* ── the draw box, ported from the UI lab ─────────────────
   Six faces on a preserve-3d parent. The perspective lives on the wrapper
   so the driver only ever writes scale and two rotations on .box3d.
   Square corners on every face: on a 3D box each face is its own plane, so
   a radius carves a notch out of the join. Thickness comes from the edge
   shading instead. */
.drawbox {
  position: absolute; inset: 0;
  display: flex; align-items: center; justify-content: center;
  perspective: 1200px;
  /* The camera sits above centre. At the default 50% 50% you look straight
     down the box's own axis, every depth face goes edge-on and the whole
     thing reads as a flat tilted card with no lid. */
  perspective-origin: 50% 34%;
  pointer-events: none;
  z-index: 1;
}
.box3d {
  --w: 250px; --h: 186px; --d: 140px;
  position: relative; width: var(--w); height: var(--h);
  transform-style: preserve-3d;
  transform: rotateX(13deg) rotateY(-19deg);
  /* transform only. opacity is a grouping property, and naming it here
     applies that grouping even at opacity 1, which flattens
     transform-style and collapses every depth face to a couple of pixels.
     The fade lives on .drawbox, which is not the preserve-3d element. */
  will-change: transform;
}
.db-face {
  position: absolute;
  background: var(--fire);
  box-shadow: inset 0 3px 0 rgba(255,255,255,.5),
              inset 0 0 0 2.5px rgba(255,255,255,.22),
              inset 0 -8px 14px rgba(150,45,40,.32);
}
.f-front, .f-back { width: var(--w); height: var(--h); left: 0; top: 0; }
.f-front {
  transform: translateZ(calc(var(--d) / 2));
  display: flex; flex-direction: column;
  align-items: center; justify-content: center; gap: 6px; overflow: hidden;
}
.f-front::before {
  content: ''; position: absolute; inset: 0;
  background: radial-gradient(120% 90% at 18% 8%, rgba(255,255,255,.30), transparent 62%);
}
/* Back to the fire ramp. This was darkened when it was the interior, but
   .rim-back is what shows through the opening now, so nothing about the
   box's own shell needs to be black — and the sliver of this face that
   perspective shows above the lid was reading as a black band on an
   otherwise warm object. Deeper than the front so it still sits back. */
.f-back {
  transform: translateZ(calc(var(--d) / -2)) rotateY(180deg);
  background: linear-gradient(100deg, #D8443F, #DE8F4C);
}
.f-left, .f-right {
  width: var(--d); height: var(--h);
  left: calc((var(--w) - var(--d)) / 2); top: 0;
}
.f-left {
  transform: rotateY(-90deg) translateZ(calc(var(--w) / 2));
  background: linear-gradient(100deg, #D8443F, #DE8F4C); filter: brightness(.9);
}
.f-right {
  transform: rotateY(90deg) translateZ(calc(var(--w) / 2));
  background: linear-gradient(100deg, #F4776B, #F5AF69); filter: brightness(1.04);
}
/* The underside. Warm like the rest of the shell, and for the same reason
   as .f-back: it is part of the box, not the interior, now that .rim-back
   is what shows through the opening. Darkest of the five, because it is the
   face the light reaches last. */
.f-bottom {
  width: var(--w); height: var(--d);
  left: 0; top: calc((var(--h) - var(--d)) / 2);
  transform: rotateX(-90deg) translateZ(calc(var(--h) / 2));
  background: linear-gradient(100deg, #C93F3B, #CE7F45);
}
.brandmark {
  position: relative; z-index: 2; color: #fff; font-weight: 800;
  font-size: 1.5rem; letter-spacing: -.02em;
  text-shadow: 0 2px 10px rgba(150,40,40,.35);
}
.brandsub {
  position: relative; z-index: 2; color: rgba(255,255,255,.9);
  font-size: .7rem; letter-spacing: .22em; text-transform: uppercase;
}
/* The cream lid. Inset shadows only: an outer shadow on a plane rotated
   flat gets rotated with it and smears sideways as a coloured halo. */
/* What you see through the hole. Co-planar with the lid and 3px behind it,
   which is the whole point: the box's own floor sits a full box-height away
   in a parallel plane, so once the lid is square the two no longer line up
   and the floor shows as a dark band past the lid's top edge. A backing
   plate at the lid's own position cannot misalign with it at any scale, and
   nothing can be seen through the opening but this. */
.rim-back {
  position: absolute; left: 0; top: calc((var(--h) - var(--d)) / 2);
  width: var(--w); height: var(--d);
  transform: rotateX(-90deg) translateZ(calc(var(--h) / -2 + 3px));
  background:
    radial-gradient(90% 90% at 50% 58%, rgba(255,140,96,.14), transparent 70%),
    linear-gradient(160deg, #17111F, #0B0814);
}
/* Water inside the box, in the lid's own plane and 1.5px in front of the
   dark backing — so the box's own 3D stack occludes it exactly the way it
   occludes anything else in there: visible through the hole, hidden by the
   lid. It cannot be part of the falling stream, because that stream is a
   flat SVG in the section below and hangs from gravity rather than from the
   box; this is the piece that has to live in the box's space.
   Filled low in the circle rather than across all of it: a tipped box holds
   its water against the near edge, and leaving dark above the waterline is
   what keeps the opening reading as an opening. */
.pour-src {
  position: absolute; left: 0; top: calc((var(--h) - var(--d)) / 2);
  width: var(--w); height: var(--d);
  transform: rotateX(-90deg) translateZ(calc(var(--h) / -2 + 1.5px));
  /* 32%, i.e. toward the lid's local TOP, because that is what points down
     the screen once the box has tipped past vertical. Measured: a marker at
     local-down lands 36px UP the screen. At 68% the water pooled against the
     high edge and the dark crescent sat underneath it, which is upside
     down for a tipped box. */
  background: radial-gradient(circle at 50% 32%,
    #E1543E 0 calc(var(--d) * 0.30), transparent calc(var(--d) * 0.30 + 1px));
  opacity: 0;                /* the driver raises it as the box tips */
  pointer-events: none;
}
.rim {
  position: absolute; left: 0; top: calc((var(--h) - var(--d)) / 2);
  width: var(--w); height: var(--d);
  transform: rotateX(-90deg) translateZ(calc(var(--h) / -2));
  background: linear-gradient(150deg, #fffdfb, #f0e7dd);
  box-shadow: inset 0 0 0 2px rgba(255,255,255,.92),
              inset 0 -4px 9px rgba(176,146,126,.24);
  /* A real hole, not a dark disc painted on the lid. The mask cuts the
     circle out of the cream so whatever sits behind the box shows through
     it, which is what makes the opening grow into a full black screen as
     the camera arrives. Painting it instead gave two circles at once: the
     painted one scaling with the box and the interior growing on its own
     schedule, one visible inside the other. */
  --hole: calc(var(--d) * 0.33);
  -webkit-mask: radial-gradient(circle at 50% 50%,
    transparent 0 var(--hole), #000 calc(var(--hole) + 1px) 100%);
  mask: radial-gradient(circle at 50% 50%,
    transparent 0 var(--hole), #000 calc(var(--hole) + 1px) 100%);
}
@media (max-width: 620px) { .box3d { --w: 186px; --h: 138px; --d: 104px; } }
@media (prefers-reduced-motion: reduce) {
  /* No scroll to drive the approach, so the box would sit there as an
     orange slab over the claim. The claim is the content; it stays. */
  .drawbox { display: none; }
  .warp-inside { display: none; }
  .warp-lid { display: none; }
}
.claim-wrap { max-width: 980px; }
.claim-line {
  display: block;
  font-size: clamp(2rem, 5.4vw, 4rem);
  line-height: 1.18;
  letter-spacing: -0.03em;
  font-weight: 700;
  margin-bottom: 0.12em;
}
/* No margin between words: every multi-word line carries a real space in the
   markup, so a margin here doubled the gap and the trailing one pushed each
   centred line off-centre. The margin dates from the Thai copy, which had no
   literal spaces to render. */
.claim-line .word {
  display: inline-block;
  opacity: 0.14;
  transition: opacity 0.45s ease;
}
.claim-line .word.lit { opacity: 1; }
.claim-line .word.fire.lit {
  background: var(--fire);
  -webkit-background-clip: text; background-clip: text;
  -webkit-text-fill-color: transparent; color: transparent;
  font-weight: 800;
}
@media (max-width: 760px) { .scene--claim { height: 640vh; } }
@media (prefers-reduced-motion: reduce) {
  .scene--claim { height: auto; }
  .claim-line .word { opacity: 1; }
}

/* ── S3b · EVERYTHING IN ONE PLACE ──────────────────────────
   A level map rather than a paragraph. The line draws itself along a
   serpentine as the scene scrolls, and each node lights as the line
   reaches it. Under 860px the map swaps for a stacked bar, because a
   winding path across a phone puts the labels at about 7px.
   ────────────────────────────────────────────────────────── */
.scene--cover { height: 340vh; }
/* The SVG scales by width at roughly 1.56:1, so on a short viewport a
   wide map would grow taller than the frame it is pinned in and get
   clipped. 125vh is that ratio against 80vh of height, so whichever runs
   out first wins. */
/* Map left, the passed step right. The cap keeps the map column short
   enough to fit a viewport: the panel takes a fixed 360, so the height the
   map needs is (max-width - 400) * 0.64, and 110vh holds that under about
   three quarters of the screen. */
/* ══ the journey ══════════════════════════════════════════
   Nine steps as five pages of two. Every positional decision lives in
   this grid and nowhere else: the road is measured off the rendered card
   boxes, so nothing here has to be mirrored in JS or in the markup.
   The section is deliberately close to the scroll depth the old pinned
   board already used (340vh). It shows far more at once, but it does not
   cost the reader more page to get through. */
.scene--journey { position: relative; padding: 96px 0 40px; }
/* --pace is the scroll cost of the whole walk. Every step's reveal is tied
   to the road reaching its node, and the road is measured from where the
   cards actually land, so the ONLY thing setting how long a step takes is
   how tall the pages are. One multiplier over the drops and the page
   padding therefore speeds the whole walk up without touching the zigzag:
   the drops keep their ratios to each other, so the road bends the same
   way, just over less page.

   At 1: 3853px of journey for nine steps, about 428px of scroll each.
   At 0.75: about 334px each, which is where this sat until it went the
   other way: at 0.75 the "short, quick zag" page's second card starts
   235px below the first while both run close to 360px tall, so for most
   of the scroll both cards of a page sit on screen at once, cramped
   together rather than read one at a time. Higher values buy the cards
   room over more scroll; lower ones speed the walk up at the cost of that
   room.

   The first page's entry lead is deliberately NOT scaled by this. */
.journey {
  --pace: 1.4;
  /* How big a step card is, as a multiple of the size it was drawn at. Every
     dimension inside the card is struck off this — the box, its padding and
     radius, the picture's radius and the gap under it, and the type — so the
     card scales as one thing rather than turning into a wide box with small
     copy in it. Phone layout resets it to 1: there the card is already the
     full column, so there is no width to grow into and only the type would
     change. */
  --card-scale: 1.3;
  position: relative; width: min(1240px, 92vw); margin: 0 auto;
}

/* Spans the whole container including the side gutters the cards leave
   empty, because that is exactly where the nodes sit. */
.jrn-road {
  position: absolute; inset: 0; width: 100%; height: 100%;
  overflow: visible; pointer-events: none;
  /* Above the sections it runs down into. Those come later in the document,
     so by default they paint over it and the pour vanished the moment it
     crossed a boundary. Their own copy is lifted above it in turn (see
     .whystack__shell), so the road passes between a section's background
     and its words rather than over the top of everything. */
  z-index: 1;
}

.jrn-page {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  column-gap: 0;
  /* No side gutter. The cards go to the outer edges and the road keeps
     the whole middle to itself. */
  /* Room under the last card of a page as well as over the first one. The
     dashed rule is the NEXT page's border-top, so with no bottom padding it
     was drawn exactly on the bottom edge of whichever card sat lowest and
     read as touching it. The floor is 56px rather than a multiple of --pace
     alone because what has to be cleared is the card's shadow, which is
     0 14px 38px and so reaches 52px below the box no matter what --pace is;
     scaled alone, a slower pace would put the rule back on the card. */
  padding: calc(40px * var(--pace)) 0 max(56px, calc(40px * var(--pace)));
  border-top: 1px dashed rgba(26,26,46,0.13);
}
/* The two ends of the road are the only places it is allowed to be fat,
   so they are the only places that need room. This lead is what the
   opening blob grows into before the first node arrives. */
/* The lead has to hold the whole entry: the blob's straight drop, then
   the turn in toward the first node, and all of it above the first card.
   Too little room and the turn gets forced into the wide part, where an
   offset ribbon cannot bend without folding over itself.

   So this one is NOT scaled by --pace. It is the road's entry runway, not
   a step: nothing is revealed while it passes, so shortening it would buy
   no speed where the reader feels it and would risk folding the ribbon. */
.jrn-page:first-of-type { border-top: 0; padding-top: 470px; }
.jrn-page--last { padding-bottom: calc(170px * var(--pace)); }

/* The road is a filled shape, not a stroke. A stroke cannot change width
   along its length, and it cannot end in a dome that travels. */
/* Gradients, not per-step fills: the road is one shape now and its colour
   runs down the page with it. Flat — the offset underside that made it read
   as a tube is gone. */
.jrn-ink { fill: url(#jrnFlow); }
/* The same road, blurred, for the stretch below the join. Masked in by the
   script exactly where the crisp copy is masked out. The blur is what makes
   the paint read as having left the road and soaked into open ground. */
/* Rows are pinned explicitly because the flipped pages place their first
   card in column 2 and their second in column 1. Grid's auto-placement is
   sparse: it never walks backwards, so it dropped the second card into a
   whole new row instead of seating it beside the first, and the pair ended
   up 524px apart on a page asking for 165. */
/* The phase header. Nine steps is a lot to hold, and these five labels are
   what make it legible as five acts instead — so they are set to be READ on
   the way past, not to be found if you look for them.

   The colour is the point. --c is the step's own colour, and it was already
   asked for here, but it is only ever DEFINED on the cards ([data-i] below),
   never on the page, so var(--c) had nothing to resolve and the number came
   out the same grey as the label. Each page now carries the colour of the
   step it opens, so the header, the road under it and the node it leads to
   are all the same hue. */
/* Lifted over the road, the same way the cards are. The road is an
   absolutely positioned sibling at z-index 1, and a positioned element
   paints above static content, so without this the ribbon ran straight
   over the phase label and cut the words in half. The road is meant to
   pass BEHIND everything the reader has to read. */
.jrn-page__tag {
  position: relative;
  z-index: 2;
  grid-row: 1;
  grid-column: 1 / -1;
  margin: 0 0 22px;
  display: flex; align-items: center; gap: 14px;
  font-size: 0.92rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--ink); font-weight: 700;
}
.jrn-page__tag b {
  font-size: 2.2rem; line-height: 1; letter-spacing: -0.02em;
  color: var(--c, var(--faint));
}
/* A short rule in the same colour, so the label reads as the top of a band
   rather than a caption floating above the cards. Capped rather than
   flex: 1 — the road crosses this row, and a full-width rule would run
   under it. */
.jrn-page__tag::after {
  content: '';
  flex: 0 1 180px;
  height: 2px; border-radius: 2px;
  background: linear-gradient(90deg, var(--c, var(--line)), transparent);
  opacity: 0.55;
}

/* The colour of the step each page OPENS with: page one starts at step 01,
   page two at 03, and so on. Same tokens the cards and the road nodes read,
   so nothing can drift out of sync. */
.jrn-page[data-flow="1"] { --c: var(--c1); }
.jrn-page[data-flow="2"] { --c: var(--c3); }
.jrn-page[data-flow="3"] { --c: var(--c5); }
.jrn-page[data-flow="4"] { --c: var(--c7); }
.jrn-page[data-flow="5"] { --c: var(--c9); }

.jrn-card {
  max-width: calc(380px * var(--card-scale, 1));
  padding: calc(18px * var(--card-scale, 1));
  border-radius: calc(22px * var(--card-scale, 1));
  /* Fully opaque now. At 22px the road passing behind a card was a hint;
     at 320px it washes the whole card with colour and the copy sits on a
     tinted ground. */
  background: #fff;
  border: 1px solid rgba(26,26,46,0.07);
  box-shadow: 0 14px 38px rgba(26,26,46,0.09);
  opacity: 0; transform: translateY(18px);
  transition: opacity .5s ease, transform .5s cubic-bezier(.22,1,.36,1);
}
.jrn-card.on { opacity: 1; transform: none; }
/* This stagger IS the zigzag. The second card of a page drops far enough
   that the road has to cross the page to reach it, and the drop is what
   sets the page's height — and therefore the scroll cost of a step, which
   is why every drop below is scaled by --pace. */
/* Cards outboard, nodes inboard, road down the middle. The first cut had
   this the other way round and the road had to cross both cards on every
   page to get from one node to the next, so the drawing animation spent
   half its length hidden behind them.
   align-self is not optional either. A grid item stretches to its row by
   default, so without it the first card of every page was inflated to
   whatever the staggered second card made the row, which collapsed the
   two card centres to 110px apart and killed the zigzag. */
/* Above the road, which is lifted so it can cross into the sections below.
   Grid items take z-index without needing to be positioned. */
.jrn-card { grid-row: 2; position: relative; z-index: 2; }
.jrn-card--a { grid-column: 1; justify-self: start; align-self: start; }
.jrn-card--b { grid-column: 2; justify-self: end; align-self: start; }

/* ── every page bends differently ─────────────────────────
   Two numbers per page shape its stretch of road, and nothing in JS knows
   about either: the road is measured off the cards, so moving a card is
   the whole edit.
     --drop  how far the second card falls below the first. Small drop
             against a wide reach gives a fast lateral zag; big drop gives
             a long lazy diagonal.
     --inset how far both cards are pulled toward the middle. The road's
             reach is the gap between their two nodes, so insetting the
             pair by n narrows the swing by 2n.
   Flipped pages start on the right instead of the left. That matters more
   than it sounds: it means the road sometimes arrives at a page already
   on the side it needs and runs straight down, instead of traversing on
   every single page the way it did when all five were identical. */
.jrn-card--b { margin-top: var(--drop, 260px); }
.jrn-card--a { margin-left: var(--inset, 0px); }
.jrn-card--b { margin-right: var(--inset, 0px); }
.jrn-page--flip .jrn-card--a { grid-column: 2; justify-self: end; margin-left: 0; margin-right: var(--inset, 0px); }
.jrn-page--flip .jrn-card--b { grid-column: 1; justify-self: start; margin-right: 0; margin-left: var(--inset, 0px); }

.jrn-page[data-flow="1"] { --drop: calc(300px * var(--pace)); --inset: 0px;   }  /* wide, lazy S      */
/* Was the tightest page on the journey by a distance: its two cards
   overlapped by 183px vertically while sitting only 153px apart across, so
   steps 3 and 4 arrived as one cluster instead of one after the other. Both
   numbers moved. The drop pushes the second card further down the page, and
   the inset — which pulls BOTH cards in toward the middle — was cut, which
   buys separation sideways without making the page any taller. That lands it
   on roughly the same footing as the "medium, upright" page below, which
   reads as two steps. */
.jrn-page[data-flow="2"] { --drop: calc(250px * var(--pace)); --inset: 35px;  }  /* quick zag, given room */
.jrn-page[data-flow="3"] { --drop: calc(390px * var(--pace)); --inset: 20px;  }  /* long diagonal     */
.jrn-page[data-flow="4"] { --drop: calc(210px * var(--pace)); --inset: 60px;  }  /* medium, upright   */
.jrn-page[data-flow="5"] { --drop: calc(250px * var(--pace)); --inset: 0px;   }  /* runs out wide     */
.img-slot--jrn {
  aspect-ratio: 16 / 10;
  border-radius: calc(14px * var(--card-scale, 1));
  margin-bottom: calc(14px * var(--card-scale, 1));
}
.jrn-card h3 {
  margin: 0 0 calc(8px * var(--card-scale, 1));
  font-size: calc(1.02rem * var(--card-scale, 1)); font-weight: 700;
  letter-spacing: -0.01em; line-height: 1.3;
}
.jrn-card h3 i {
  font-style: normal; color: var(--c-deep);
  font-variant-numeric: tabular-nums;
  /* The step number is the ONLY place the number appears now. It used to be
     repeated on a bead out on the road, which is the thing this replaces, so
     it is sized to be read as the step marker rather than as a prefix to the
     title. In em, so it tracks the title through --card, and line-height 1
     so a number nearly twice the type size does not open up the row. */
  flex: 0 0 auto;
  font-size: 1.9em; line-height: 1;
  font-weight: 800; letter-spacing: -0.04em;
}
.jrn-card p {
  margin: 0; font-size: calc(0.86rem * var(--card-scale, 1));
  line-height: 1.55; color: var(--muted); font-weight: 300;
}

/* A step shows its number and its title, and keeps the description behind a
   click. Nine steps is a lot to hand someone as nine paragraphs; as nine
   titles it is a list you can take in, and the detail is there for the ones
   you actually want.

   Native <details>, which is the whole reason this is so little code: the
   keyboard, the screen reader and the script-disabled case are the
   element's own doing, not something re-implemented here. The <h3> stays a
   real heading — a summary is allowed exactly one heading as its content —
   so the page outline and what a crawler reads are both unchanged. */
.jrn-card { cursor: pointer; }
.jrn-card__disc > summary {
  display: flex; align-items: center;
  gap: calc(10px * var(--card-scale, 1));
  cursor: pointer;
  /* Both are needed to drop the default triangle: the standard property and
     the WebKit pseudo-element it shipped with first. */
  list-style: none;
}
.jrn-card__disc > summary::-webkit-details-marker { display: none; }
.jrn-card__disc > summary > h3 {
  margin: 0; flex: 1 1 auto;
  /* Baseline row, so the big number and the title sit on one line together
     however long the title is and whatever it wraps to. */
  display: flex; align-items: baseline;
  gap: calc(10px * var(--card-scale, 1));
}
.jrn-card__disc > summary:focus-visible { outline: 3px solid var(--red); outline-offset: 4px; border-radius: 6px; }
/* The affordance. A chevron built from two borders rather than a glyph, so
   it takes the step's own colour and scales with --card like everything
   else in the card. */
.jrn-card__disc > summary::after {
  content: ''; flex: 0 0 auto;
  width: calc(8px * var(--card-scale, 1)); height: calc(8px * var(--card-scale, 1));
  border-right: 2px solid var(--c-deep); border-bottom: 2px solid var(--c-deep);
  transform: translateY(-2px) rotate(45deg);
  transition: transform .3s cubic-bezier(.22,1,.36,1);
}
.jrn-card__disc[open] > summary::after { transform: translateY(2px) rotate(-135deg); }
.jrn-card__disc > p { margin-top: calc(9px * var(--card-scale, 1)); }

/* Opening slides rather than jumps, where the browser can interpolate to
   auto. Guarded, because everywhere else it simply toggles — which is the
   behaviour <details> has always had and is perfectly fine. */
@supports (interpolate-size: allow-keywords) {
  :root { interpolate-size: allow-keywords; }
  .jrn-card__disc::details-content {
    block-size: 0; overflow: hidden;
    transition: block-size .38s cubic-bezier(.22,1,.36,1),
                content-visibility .38s allow-discrete;
  }
  .jrn-card__disc[open]::details-content { block-size: auto; }
}

/* A winding road needs width to wind in. Below the breakpoint there is
   none, so the road goes and the pages become a plain column. */
@media (max-width: 860px) {
  .scene--journey { padding: 56px 0 20px; }
  .journey { width: 88vw; --card-scale: 1; }
  .jrn-road { display: none; }
  /* The flood is held back here. A phone gives the gradient a ray a third
     the length of the desktop one, so the whole spectrum lands inside a
     screen and the deepest bands sit exactly where the heading is; on
     desktop the same bands are spent long before they reach the type.
     Opacity rather than nine re-mixed colours: it blends every ring back
     toward the cloud white behind the section, which is the same thing
     the mixes do, with one number instead of nine. */
  .whystack__wash { opacity: .55; }
  /* Same clearance under the rule as on desktop, tightened for the phone:
     one column stacks the cards, so the lowest one is simply the last. */
  .jrn-page { grid-template-columns: 1fr; column-gap: 0; row-gap: 22px; padding: 28px 0 36px; }
  /* The header keeps its new weight on a phone, just not its full size, and
     it drops the trailing rule: with one column the label has the whole
     width to itself and a 180px rule beside a long phase name ("The talent
     and the night") is the one thing that could push it to two lines. */
  .jrn-page__tag { font-size: 0.82rem; gap: 10px; margin-bottom: 16px; }
  .jrn-page__tag b { font-size: 1.7rem; }
  .jrn-page__tag::after { display: none; }
  .jrn-page:first-of-type { padding-top: 0; }
  .jrn-page--last { padding-bottom: 0; }
  .jrn-card { max-width: none; opacity: 1; transform: none; }
  /* Every flow variable is a desktop concern. On one column there is no
     reach to narrow and no side to start from. */
  .jrn-card--a, .jrn-card--b,
  .jrn-page--flip .jrn-card--a, .jrn-page--flip .jrn-card--b {
    grid-column: 1; grid-row: auto; justify-self: stretch;
    margin: 0;
  }
}

@media (prefers-reduced-motion: reduce) {
  .jrn-card { opacity: 1; transform: none; }
  .jrn-card__disc::details-content { transition: none; }
  .jrn-card__disc > summary::after { transition: none; }
}

.cover-wrap {
  width: 100%;
  max-width: min(1320px, calc(400px + 110vh));
  display: grid;
  grid-template-columns: minmax(0, 1fr) 360px;
  column-gap: 40px;
  align-items: center;
}
.cover-wrap > .lvl-map, .cover-wrap > .steps-bar { grid-column: 1; }
.cover-wrap > .glimpse { grid-column: 2; }
.cover-wrap > .cover-claim { grid-column: 1 / -1; }
/* The map is the widest thing on the page, so this scene gets a narrower
   gutter than the 6vw the other scenes use. Only above the breakpoint —
   below it the stacked bar shows instead and needs the normal margin. */
@media (min-width: 861px) {
  .scene--cover .sticky-frame { padding-left: 3vw; padding-right: 3vw; }
}

/* The board is tilted back a little and every element is built as a top
   surface over a darker base, so the track reads as a raised road and the
   nodes as buttons standing on it. The tilt stays mild — past about 15deg
   the labels start to distort. */
/* No 3D transform here, deliberately. rotateX put the SVG in a 3D
   rendering context, which the browser rasterises into a texture and then
   scales — so at browser zoom every edge inside it went soft, dashes
   worst of all. The tilt was cos(13deg), a 2.6% squash, and all the depth
   actually comes from the offset dark undersides below each path and
   node. Losing the tilt costs almost nothing and keeps the map vector
   sharp at any zoom. Anything added here later that forces
   rasterisation — a filter, a mask, a drop-shadow — brings the blur back. */
/* Nine steps, nine colours, warm at the permit and cool by the report so
   the road reads as one journey rather than nine unrelated badges.
   Each step carries three values because a hue has three jobs here: the
   cap it fills, the ring it tints, and the underside of the road it
   darkens. White numerals sit on --c*, so every main colour clears 3:1
   for large bold text (the lightest is 3.7:1). The *d values are the only
   ones that ever carry small text — the numeral on the glimpse card — so
   that numeral takes the darkened value, never the main one.
   Declared on the scene because the map, the phone bar and the card
   column are three separate subtrees that all need to read the same list. */
.scene--journey {
  --c1: #E1543E; --c1s: #FBE3DC; --c1d: #9E3B2B;   /* permit   */
  --c2: #D2661F; --c2s: #FAE5D9; --c2d: #934716;   /* mechanic */
  --c3: #AD7B0B; --c3s: #F5E7D6; --c3d: #795608;   /* system   */
  --c4: #588E2E; --c4s: #EBEADA; --c4d: #3E6320;   /* admin    */
  --c5: #1F927B; --c5s: #E4EAE4; --c5d: #166656;   /* artist   */
  --c6: #1F86AE; --c6s: #E4E9EA; --c6d: #165E7A;   /* event    */
  --c7: #4067C4; --c7s: #E8E5EC; --c7d: #2D4889;   /* draw     */
  --c8: #7154C6; --c8s: #EEE3ED; --c8d: #4F3B8B;   /* prizes   */
  --c9: #AC4293; --c9s: #F5E0E7; --c9d: #782E67;   /* report   */
}
/* Every part of a step — its stretch of road, its bead, its card — resolves
   --c from the same index, so a step is recoloured by editing one line
   above and nothing else.
   Scoped to the section rather than listing each class: the old list named
   .glimpse__card, which the journey replaced, and the card numerals went
   silently back to ink because nothing matched them any more. */
.scene--journey [data-i="0"] { --c: var(--c1); --c-soft: var(--c1s); --c-deep: var(--c1d); }
.scene--journey [data-i="1"] { --c: var(--c2); --c-soft: var(--c2s); --c-deep: var(--c2d); }
.scene--journey [data-i="2"] { --c: var(--c3); --c-soft: var(--c3s); --c-deep: var(--c3d); }
.scene--journey [data-i="3"] { --c: var(--c4); --c-soft: var(--c4s); --c-deep: var(--c4d); }
.scene--journey [data-i="4"] { --c: var(--c5); --c-soft: var(--c5s); --c-deep: var(--c5d); }
.scene--journey [data-i="5"] { --c: var(--c6); --c-soft: var(--c6s); --c-deep: var(--c6d); }
.scene--journey [data-i="6"] { --c: var(--c7); --c-soft: var(--c7s); --c-deep: var(--c7d); }
.scene--journey [data-i="7"] { --c: var(--c8); --c-soft: var(--c8s); --c-deep: var(--c8d); }
.scene--journey [data-i="8"] { --c: var(--c9); --c-soft: var(--c9s); --c-deep: var(--c9d); }

.lvl-map { width: 100%; }
.lvl-map svg { width: 100%; height: auto; display: block; overflow: visible; }

/* No underside on the un-run track. It used to carry the same 22/16 dash
   translated 7px down, so every dash drew twice with a vertical offset and
   the doubled edge read as blur at zoom. The road that has been drawn keeps
   its underside — a solid stroke over a solid stroke reads as depth; two
   translucent dashes do not. */
/* The road ahead is dashed and the road behind is solid, which is how a
   level map shows what you have not reached yet. */
.lvl-track {
  fill: none; stroke: rgba(26,26,46,0.10);
  stroke-width: 17; stroke-linecap: round;
  /* The dash scales with the stroke or the gaps close up and the road
     ahead stops reading as dashed. */
  stroke-dasharray: 30 22;
}

/* The road is one tube built from three stacked strokes, drawn in this
   order: the side, the body, then the light on top. Each is on the same
   path and all three are walked by the same dashoffset, so the tube draws
   itself as a single object.
   Widths matter more than offsets here: the side has to be WIDER than the
   body or it stops peeking out beneath it, and the highlight has to be
   much NARROWER or it flattens the body into a stripe. */
/* Nine arcs share one path definition and each is given a dash window
   covering only its own stretch of it, so the tube changes colour at every
   node without being nine separate shapes to keep in register. Hidden
   until JS opens the window: a zero-length dash under a round linecap
   renders as a dot, which would leave nine beads sitting on an undrawn
   road. */
.lvl-seg-base, .lvl-seg {
  fill: none; stroke-linecap: round;
  stroke-dasharray: 0 2; opacity: 0;
}
.lvl-seg-base {
  stroke: var(--c-deep); stroke-width: 24;
  /* Deeper than the old 7px: this gap IS the apparent thickness of the
     tube, and it has to grow with the stroke or a fatter road just looks
     flat. */
  transform: translateY(11px);
}
.lvl-seg { stroke: var(--c); stroke-width: 20; }
/* The specular. Sitting a little high on the body and well inside its
   width is what turns two flat ribbons into something round. Kept white
   at low alpha rather than a lighter red so it reads as light falling on
   the tube, not as a third colour in the palette. */
.lvl-fill-hi {
  fill: none; stroke: rgba(255,255,255,0.38); stroke-width: 4;
  stroke-linecap: round;
  stroke-dasharray: 1; stroke-dashoffset: 1;
  transform: translateY(-5px);
}

.lvl__base { fill: rgba(26,26,46,0.18); }
.lvl__dot {
  fill: #F2EFEC;
  stroke: rgba(26,26,46,0.10);
  stroke-width: 2;
  transform-box: fill-box;
  transform-origin: center;
  transition: fill .35s ease, stroke .35s ease, transform .4s cubic-bezier(.34,1.56,.64,1);
}
.lvl__n {
  font-size: 30px; font-weight: 800; fill: rgba(26,26,46,0.42);
  transition: fill .35s ease, transform .4s cubic-bezier(.34,1.56,.64,1);
}
.lvl__label {
  font-size: 21px; font-weight: 700; fill: var(--faint);
  transition: fill .35s ease;
}

/* A node is a control. The hit circle is wider than the cap so the target
   is comfortable, and fill:transparent rather than fill:none because none
   takes no pointer events. */
.lvl { cursor: pointer; }
.lvl__hit { fill: transparent; }
.lvl:focus { outline: none; }
.lvl:focus-visible .lvl__cap { stroke: var(--c); stroke-width: 4; }
/* The picked step reads as picked even when the road has not reached it. */
.lvl.sel .lvl__cap { stroke: var(--c); stroke-width: 3.5; }
.lvl.sel .lvl__label { fill: var(--ink); font-weight: 600; }
.lvl:hover .lvl__label { fill: var(--ink); }
.step-seg { cursor: pointer; }
.step-seg.sel .step-seg__label { color: var(--ink); font-weight: 600; }

/* A cleared node lifts off its base and takes the brand colour. */
.lvl__base.on { fill: var(--c-deep); fill-opacity: 0.34; }
.lvl__dot.on {
  fill: var(--c-soft); stroke: var(--c); stroke-width: 5;
  transform: scale(1.04);
}

/* The cap sits above the road and carries the numeral, so the number never
   has to compete with the line crossing underneath it. */
.lvl__cap {
  fill: #fff;
  stroke: rgba(26,26,46,0.10);
  stroke-width: 2;
  transform-box: fill-box; transform-origin: center;
  transition: fill .35s ease, stroke .35s ease, transform .4s cubic-bezier(.34,1.56,.64,1);
}
.lvl.on .lvl__cap { fill: var(--c); stroke: #fff; transform: scale(1.06); }
.lvl.on .lvl__n { fill: #fff; }
.lvl.on .lvl__label { fill: var(--ink); font-weight: 600; }

/* ── the glimpse column ───────────────────────────────────
   One card per node, carrying the same number, heading and copy as its
   row in the chain section below, so the map reads as a preview of that
   section. Cards are absolutely stacked inside a reserved column: only
   the active one is opaque, so a swap never reflows and the map beside it
   never moves. The min-height is the tallest card, measured, not guessed. */
.glimpse { position: relative; min-height: 400px; text-align: left; }
.glimpse__card {
  position: absolute; inset: 0 0 auto 0;
  padding: 18px;
  border-radius: 22px;
  background: rgba(255,255,255,0.74);
  border: 1px solid rgba(26,26,46,0.07);
  box-shadow: 0 12px 34px rgba(26,26,46,0.08);
  opacity: 0; pointer-events: none;
  transform: translateY(10px);
  transition: opacity 0.32s ease, transform 0.32s ease;
}
.glimpse__card.on { opacity: 1; transform: none; }
.img-slot--glimpse { aspect-ratio: 16 / 10; border-radius: 14px; margin-bottom: 14px; }
.glimpse__body h3 {
  margin: 0 0 8px; font-size: 1.02rem; font-weight: 700;
  letter-spacing: -0.01em; line-height: 1.3;
}
.glimpse__body h3 i {
  font-style: normal; color: var(--c-deep); margin-right: 9px;
  font-variant-numeric: tabular-nums;
}
.glimpse__body p {
  margin: 0; font-size: 0.86rem; line-height: 1.55; color: var(--muted);
  font-weight: 300;
}

/* the phone version: one row per step */
.steps-bar { display: none; }
.step-seg { display: flex; flex-direction: row-reverse; align-items: center; gap: 14px; }
.step-seg__track {
  flex: 1 1 auto; height: 8px; border-radius: 100px;
  background: rgba(26,26,46,0.09); overflow: hidden;
}
.step-seg__fill {
  display: block; height: 100%; border-radius: 100px;
  background: var(--c);
  transform: scaleX(0); transform-origin: left center; will-change: transform;
}
.step-seg__label { flex: 0 0 84px; font-size: 0.86rem; color: var(--faint); transition: color .35s ease; }
.step-seg.on .step-seg__label { color: var(--ink); font-weight: 500; }

/* Between the two breakpoints the panel gives up 80px rather than the
   layout dropping to one column: a full-width map plus a card below it is
   over a thousand pixels tall, and the sticky frame would clip it. */
@media (max-width: 1100px) {
  .cover-wrap { grid-template-columns: minmax(0, 1fr) 280px; column-gap: 28px; }
}

@media (max-width: 860px) {
  .scene--cover { height: 280vh; }
  .lvl-map { display: none; }
  .steps-bar { display: flex; flex-direction: column; gap: 14px; }
  /* The step list is a control here, so it has to clear the sticky nav.
     Padding the frame down by the nav height put the wrap over the
     viewport, so the two internal gaps give the room back. */
  .scene--cover .sticky-frame { padding-top: 60px; }
  .cover-wrap { grid-template-columns: 1fr; row-gap: 16px; }
  .cover-wrap > .glimpse { grid-column: 1; min-height: 360px; }
  .glimpse__card { padding: 14px; }
  .glimpse__body h3 { font-size: 0.96rem; }
  .glimpse__body p { font-size: 0.82rem; }
}

@media (prefers-reduced-motion: reduce) {
  .scene--cover { height: auto; }
  .glimpse { display: none; }
  .lvl__dot { fill: var(--c-soft); }
  .lvl__cap { fill: var(--c); }
  .lvl__n { fill: #fff; }
  .lvl__label { fill: var(--ink); }
  .step-seg__label { color: var(--ink); }
  .step-seg__fill { transform: scaleX(1); }
}

/* ══════════════════════════════════════════════════════════
   CLOUD BANDS
   Lifted from the app's lucky draw page, which stacks a
   coloured cloud behind a white one with a small vertical
   offset so the edge reads as depth rather than a sticker.
   Used as the top edge of a section, so the band below looks
   like it is sitting in cloud over the warm gradient.
   ══════════════════════════════════════════════════════════ */
.cloud-band {
  position: relative;
  line-height: 0;            /* no descender gap under the image */
  margin-bottom: -1px;       /* kill the hairline against the band below */
  pointer-events: none;
}
/* The road from the section above runs down into this band and has to pass
   BEHIND it. DOM order alone already does that — both are positioned with
   z-index auto and the band comes later — but the road is the only thing on
   the page that depends on it, so it is stated rather than inherited.
   Entry band only: the exit band has content after it that would end up
   underneath. */
.cloud-band:not(.cloud-band--exit) { z-index: 1; }
/* The back plate is in normal flow, so the band is exactly as tall as the
   cloud is — a fixed height clipped the scallops off and left only the
   flat fill showing, which is what made it look like a sliver. */
.cloud-band__back {
  display: block;
  width: 100%;
  height: auto;
  opacity: 0.95;
}
/* The front plate overlays it, bottom-aligned. It is the shorter of the
   two, so the warm scallops read above the white ones.

   It is NOT the artwork. Cloud1.webp is pure white, #FFFFFF, and the cloud
   the canvas paints below this band is --bg, #F6F7F9. Nine points apart
   across a flat field the width of the page, which is small enough to look
   like a rendering fault and large enough to see: the band read as a
   brighter white slab sitting on a greyer one. So the file is used as a
   MASK and filled with --cloud-now, the colour the script publishes for
   the canvas cloud, and the two are the same value by construction rather
   than by two people remembering to update the same hex twice.

   The BACK plate stays real artwork on purpose. It is cream, and that warm
   layer behind the white is the band's depth, not a mismatch to fix. */
.cloud-band__front {
  display: block;
  position: absolute;
  left: 0;
  bottom: 0;
  width: 100%;
  /* A span has no intrinsic size, so the ratio of the plate it replaces
     has to be stated or the box collapses to zero height. */
  aspect-ratio: 1572 / 424;
  background: var(--cloud-now, var(--bg));
  -webkit-mask-image: url("/assets/icons/Cloud1.webp");
  mask-image: url("/assets/icons/Cloud1.webp");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
}





/* ── CLOUDLAND ──────────────────────────────────────────── */
/* Above the single cloud band the page is open gradient sky. Everything
   from that cloud down sits on white, so the transition reads as climbing
   through cloud into it rather than as a divider you pass twice. The
   cloud's own fill is opaque #fff, sampled, so the join is invisible. */
/* No position: relative here. It made this the offsetParent for every
   scene inside, so their offsetTop became relative to the wrapper rather
   than the document and each one animated ~8,800px too early. Nothing in
   here is positioned against it, so it does not need to be a containing
   block. */
.cloudland { background: #fff; }

/* ── PER-SECTION SKY TINT ───────────────────────────────────
   Every island is a hole onto ONE fixed sky, so without this they all
   show the same colour. Each section names a tint here and the cloud
   script washes it into that section's opening only.

   Keyed by id, deliberately, NOT by :nth-child. The section order on this
   page has changed repeatedly, and an index-based palette silently
   reshuffles every colour the moment one section moves or is removed.

   Values are bare "r g b" triplets so the script can apply its own alpha.
   The wash is translucent, so it multiplies with whatever sky is behind
   it: warm by day, and automatically deep and muted once night falls. */
#whystack     { --tint: 255 176 148; }   /* coral, picks up the hero */
/* Every number this section is built from lives here. */
#whystack {
  /* A ceiling on how big a plate may get, not the size it will be: in a
     row of three the COLUMN is what binds, always, and this only stops the
     artwork running away on a very wide window. */
  /* THE SIZE OF THE ROW, and no longer a ceiling: with content-sized
     columns this is what every plate is measured from, so it is also what
     has to keep the three inside the window.
     262px holds the row at the width it had when the three were sized 20%
     up: the total across the three plates is 1294px either way, and
     evening them out only moved size between them. The rocket comes down
     from 482 to 429 and the other two go up, which is what "the same size"
     costs when one of the three was the biggest.
     The fallback solves for the fit: the three plates together are
     --card-h * --plate-span * (0.9463 + 1 + 0.9644), which is
     --card-h * 4.9394, and what they have to fit inside is the window less
     the two 40px gaps and a page margin either side. Capped, so a very wide
     screen holds this size rather than carrying on past it. */
  --card-h: min(262px, calc((100vw - 150px) / 4.9394));
  /* The three plates' widths, as fractions of the widest. Hoisted here
     because the grid tracks below and the cards themselves both need them,
     and two copies would drift.
     EQUAL INK EXTENT, not equal panel area. The previous set solved for
     equal panel — the part you read — and on paper it worked: measured off
     the rendered page the three pieces covered 65.8k, 66.1k and 70.0k
     square pixels of ink, within 6% of each other. They still did not look
     the same size, because extent is what the eye judges, not area, and the
     rocket's flame and arrow head are thin things that add a lot of the
     first and little of the second: its drawing measured 482x282 against
     the sign's 380x251.
     So these solve for equal DIAGONAL of the artwork's own ink box, taken
     from the alpha channel of each file rather than from the frame it sits
     in: 1062x701 of 1078x713 for the sign, the full 1161x578 for the
     bubble, 1222x714 of 1222x720 for the rocket. Normalised against the
     largest. All three now come out at a 497px diagonal. */
  --pw-fans: 0.9463;
  --pw-buzz: 1;
  --pw-sales: 0.9644;
  /* The widest plate's width, in units of --card-h. Every plate is a fixed
     fraction of it (--plate-w), so the three keep their size relationship
     whichever limit is binding. 1.7426 is the number that makes the tallest
     of them exactly one --card-h tall. */
  --plate-span: 1.6969;
  /* This section runs wider than the rest of the page. The deck at 1.5x
     needs about 606px of column, and taking that out of the standard 1120
     shell would have squeezed the question into a width where it wraps to
     three lines. */
  --wide: 1600px;
}
/* The section's own opening in the cloud is cut from this, like every other
   section's. It used to nominate a smaller box with [data-cloud-island],
   because the deck dealt itself over a runway nearly twice the viewport and
   an opening that size has no edges on screen for most of the section — it
   read as the background growing and shrinking rather than as a hole the
   content sits in. In a row the whole section is shorter than a screen, so
   that reason is gone, and the nominated box had brought a fault with it:
   pinned, it kept filling its hole while the content scrolled out from
   under it; unpinned, the hole stopped following the box and the bare sky
   showed through the cloud where the peach should have been. */
/* NOT .shell, and that is what takes this section out of the cloud system.
   lucky-draw-clouds.js cuts an opening for every '.cloudland .shell' it
   finds; with no shell to find, no opening is cut here, the cloud simply
   covers the section, and what shows is the cloud's own white. The colour
   is put back by .whystack__wash below, on the section's own terms. */
/* THE PAGE HOLDS WHILE THIS SECTION PAINTS. The band is given a screen of
   runway on top of its own screen of content, and both of its layers pin at
   the top of the window for the length of it. So the reader arrives, the
   paint lands, and the page stops going anywhere until the colour has
   reached every corner — #why cannot be scrolled to early, because there is
   nothing to scroll to until the flood is done.

   The wash and the shell pin SEPARATELY rather than inside one sticky
   wrapper, and that is load-bearing. `position: sticky` makes a stacking
   context, so a wrapper would carry both layers as one unit and the road —
   which lives in the section above and is painted at z-index 1 in the root
   context — would end up entirely behind or entirely in front of the pair.
   Pinned as siblings they keep their own z-indices in the root context, and
   the order that makes the pour visible inside the band survives: wash 0,
   road 1, words 2. */
#whystack {
  padding: 0;
  min-height: 200vh;
}
#whystack > .whystack__shell {
  max-width: var(--wide);
  margin: 0 auto;
  width: 100%;
  /* Over both the wash and the road, so the pour passes BEHIND the words. */
  position: sticky;
  top: 0;
  min-height: 100vh;
  box-sizing: border-box;
  padding: 88px 6vw;
  display: flex;
  flex-direction: column;
  justify-content: center;
  z-index: 2;
}

/* THE PAINT FLOODS THE SECTION. The road pours down the page and arrives at
   this section's top edge; from the point where it lands, the section's
   colour spreads out until it has filled the whole band. A radial gradient
   whose one solid stop is driven from script: --wash is 0 before the paint
   lands and 1 once the flood has reached the far corner, and --wash-x is
   where the road actually crosses, read off the ribbon's own path so the
   colour starts where the paint is rather than at a guessed point.

   The soft trailing stop is the wet edge — paint spreading into a dry
   surface does not stop on a hard line. */
/* NO stacking context here, deliberately. The wash below sits in the page's
   own negative layer, which is the only place it can be BELOW the road and
   above the cloud at the same time. Boxed into a stacking context on this
   section it can still be below the section's own text, but the road lives in
   the section above and would then be painted over — and a covered road is
   what forces a second stroke to be drawn in CSS to stand in for it. There is
   one pour on this page and it is a real shape; nothing here imitates it. */
#whystack { position: relative; }
.whystack__wash {
  /* Pinned, not stretched over the runway. Left absolute across the whole
     200vh the gradient's origin would keep climbing while the content stood
     still, and the colour would read as sliding up the page rather than
     spreading from where the paint hit. One screen tall, pinned, and the
     origin holds exactly where the pour landed.
     The negative bottom margin takes it back out of the flow, so the shell's
     own pin still starts at the section's top edge. */
  position: sticky;
  top: 0;
  height: 100vh;
  margin-bottom: -100vh;
  width: 100%;
  /* Three layers, in this order: the wash, then the road, then the copy.
     The road is one shape drawn once, up in the section above. Painted OVER
     it, as it was, the paint could not be seen inside this band at all, so a
     second stroke had to be drawn here to stand in for it — and two strokes
     never line up: one ended in a nose above the join, the other began below
     it, and the reader saw two marks rather than one pour passing through.
     Under it instead, and the road IS the paint here, the same element that
     poured out of the box at the top of the page, with nothing to keep in
     sync because there is nothing to sync with.
     0 and not -1: at -1 this drops behind the cloud canvas, which is opaque,
     and the rainbow disappears entirely. */
  z-index: 0;
  pointer-events: none;
  --wash: 0;
  /* PAINTS NOTHING NOW, and this is the one that costs something to say.
     This carried the flood: nine rings expanding out of the point the road
     crosses in, driven by --wash, so the paint visibly arrived and spread.
     It is gone because the ground below it is now shared with #why —
     .rainbowband__ground, pinned across both sections — and a flood on top
     of a ground that is ALREADY the same rainbow reads as a second one
     appearing over the first rather than as paint landing.
     What was traded away: the arrival. What was bought: no seam between
     the two sections, which could not be had any other way, because two
     viewport-anchored gradients in two separate boxes never agree at the
     boundary between them.
     The element stays: washOne() writes --wash and --wash-x to it every
     frame and also drives holdRoad() and the phrase rail on the way past. */
  background: transparent;
}
/* Without script, and for anyone who has asked for less motion, the section
   is simply already painted. */
@media (prefers-reduced-motion: reduce) { .whystack__wash { --wash: 1; } }

/* NO HOLD WITHOUT A POUR. The road is hidden below 860px and the flood is
   already finished for anyone who has asked for less motion, so in both
   cases the pin would be a screen that refuses to scroll while nothing
   happens on it. The section goes back to being an ordinary band. */
@media (max-width: 860px), (prefers-reduced-motion: reduce) {
  #whystack { min-height: 0; padding: 88px 6vw; }
  #whystack > .whystack__shell {
    position: static;
    min-height: 0;
    padding: 0;
    display: block;
  }
  .whystack__wash {
    position: absolute;
    inset: 0;
    height: auto;
    width: auto;
    margin-bottom: 0;
  }
}

#why          { --tint: 255 227 209; }   /* peach, the section's own ground */
/* AN ID, because #why carries .scene--feed as well as .scene--why and that
   class sets `background: var(--feed-ground)` later in this file — equal
   specificity, so source order decides and the section would paint over
   .rainbowband__ground. --feed-ground itself stays: the paper's own top
   wash is drawn in it. */
#why { background: transparent; }
/* THE SAME PEACH, THROUGH THE TOKEN .scene--feed READS. #why carries both
   classes now, and .scene--feed's `background: var(--feed-ground)` is later
   in this file than .scene--why's own background — equal specificity, so it
   wins. Setting the token rather than fighting it also keeps the paper's
   own top wash, which is drawn in --feed-ground, on the same colour. */
#why { --feed-ground: #FFE3D1; --feed-ground-0: rgba(255,227,209,0); }
#s4           { --tint: 246 214 170; }   /* warm sand */
#cars         { --tint: 250 214 150; }
#facts        { --tint: 246 214 170; }   /* the sand the hidden comparison
                                            table used to wear */   /* between the question's amber and
                                            the sand of the two sections that
                                            spell the same point out at length */
#race         { --tint: 252 226 196; }   /* the sand again, a shade lighter:
                                            the race restates s4's argument,
                                            so it stays in s4's colour rather
                                            than announcing a new subject */
#data         { --tint: 150 214 202; }   /* mint, a break from the warm run */
#channels-fit { --tint: 196 180 240; }   /* lilac */
#celebrity    { --tint: 255 168 190; }   /* rose */
#types        { --tint: 180 198 246; }   /* indigo, lightened: at 150 176 240
                                            it pulled day body copy to 4.43 */
#license      { --tint: 158 190 220; }   /* slate blue */
#campaigns    { --tint: 255 148 110; }   /* the fire gradient's own midpoint */

/* ══════════════════════════════════════════════════════════
   WHY · THE ANSWER STACK
   The question pins on the left while its three answers deal
   themselves over each other on the right. There is no script
   behind it: the section's own height is the runway and two
   uses of position: sticky do the rest.
   ══════════════════════════════════════════════════════════ */
/* The reference puts the HEADING on the wide side and the cards on the
   narrow one: 610 against 374, about 1.6 to 1. Ours had it inverted at
   475 against 581, which is what made the composition look lopsided —
   oversized cards crowding a cramped question. Flipping the ratio gives
   cards near enough the reference's near-square proportion. */
/* THREE ACROSS, not a pile. The cards used to pin one on top of another
   and straighten as they landed, which was a good deal to watch and a bad
   one to read: only the top card was ever legible, and the two behind it
   showed as fragments around its edges once they became cut-out artwork
   with daylight around the shape. Side by side, all three can be read at
   once and compared, which is what a list of three reasons is for.

   Nothing in this section pins any more, so the question sits above the
   row at full width rather than in a column beside it. */
.whystack {
  display: block;
}

/* align-self: start is load-bearing. A grid item stretches to the row
   height by default, and a stretched box has nothing left to travel
   inside, so the sticky never engages and the column just scrolls away. */
/* Centred over the row it introduces. It sat left while the deck was a
   column beside it; with the three cards spread across the full width, a
   question ranged left had nothing under it to line up with. */
.whystack__pin {
  position: static;
  text-align: center;
}
.whystack__q {
  font-size: clamp(1.9rem, 3.6vw, 3rem);
  line-height: 1.1;
  letter-spacing: -0.03em;
  font-weight: 700;
  margin: 10px 0 16px;
  /* BLACK, both lines. The first inherited the section's near-navy ink and
     the second was set in the --fire gradient, so the question read as two
     colours before it read as one sentence. The band behind it is a nine
     ring rainbow now and every one of those rings is mixed well down into
     white, so flat black is the one value that holds against all of them
     rather than against whichever ring the reader happens to have scrolled
     to. */
  color: #000;
}
.whystack__q .grad {
  /* The <em> stays for the line break and the emphasis it carries in the
     markup; only its colour treatment is gone. -webkit-text-fill-color has
     to be reset explicitly — it is what the gradient set, and colour alone
     does not override it. */
  font-style: normal;
  background: none;
  -webkit-text-fill-color: #000;
  color: #000;
}
.whystack__list {
  list-style: none;
  margin: 56px 0 0;
  padding: 0;
  display: grid;
  /* CONTENT-SIZED COLUMNS, not three equal 1fr, and that is what makes
     "bigger AND further apart" possible at all. On 1fr the cell width and
     the plate width are locked together: every pixel of gap comes back out
     of the columns, so opening the row up shrinks the artwork, and any
     plate wider than its cell overflows it SYMMETRICALLY — which put the
     rocket's arrow head 6px off the right edge of the window while the
     sign sat 54px in. Solved on paper for 20% bigger with any real
     clearance and the row needed 1374px inside a 1267px content box; there
     is no gap or padding that makes that fit.
     Sized to their content instead, a cell IS its plate, nothing overflows
     anything, and the gap below is exactly the daylight between two pieces
     of art. */
  /* EXPLICIT TRACKS, one per plate, not `auto`. Content-sized tracks were
     the obvious way to write this and they collapsed the row to nothing:
     .whystack__item carries container-type: inline-size, which makes its
     inline size independent of its own contents, so an auto track has
     nothing to measure and resolves to 0. Stating each track outright
     gives the container a definite width to resolve cqw against and keeps
     the cell exactly its plate. Order follows the DOM: sign, bubble,
     rocket. */
  grid-template-columns:
    calc(var(--card-h) * var(--plate-span) * var(--pw-fans))
    calc(var(--card-h) * var(--plate-span) * var(--pw-buzz))
    calc(var(--card-h) * var(--plate-span) * var(--pw-sales));
  justify-content: center;
  gap: 40px;
  /* And it takes the page margin back, because the three at this size need
     1374 of the 1440 a 1440px window has and the shell only offers 1267.
     The row is the one thing in this section wide enough to want it. */
  margin-inline: -6vw;
  /* The three pieces are different heights by design — they are sized for
     equal PANEL, not equal box — so they hang from their middles rather
     than from a shared top edge. */
  align-items: center;
}
/* Each cell is the container the copy is sized against. It goes here and
   not on the plate because all three cells are the same width while the
   plates are not: sized against the plate, the sign's text would come out a
   fifth smaller than the rocket's and the row would look mis-set. */
.whystack__item { container-type: inline-size; }

/* THE ARTWORK IS THE CARD. There is no panel here any more — no gradient,
   no 28px radius, no padding box. Each of the three pieces is already a
   container in its own right (a lit sign, a speech bubble, a banner on a
   rocket), so a rounded rectangle behind one was a second container around
   the first, and the drawn edge had to fight the CSS edge for the reader's
   idea of where the card stopped.

   The card has no height of its own any more. It had to be exactly
   --card-h while the three were a pile, because flush stacking depends on
   every card being one step tall; in a row nothing is stacking, so the
   artwork sets the height and the row centres what it is given. The tilt
   went with the pile too — it was there to read as loose cards being dealt,
   and on three cards standing side by side it just looks misaligned. */
.whystack__card {
  position: relative;
  color: #fff;
  display: flex;
  justify-content: center;
}
/* The copy sits INSIDE the drawn container, so it is sized against the
   plate rather than the column: a banner is narrower than the card box was,
   and the old 0.84rem wrapped to more lines than the drawn panel could
   hold. Centred in its box so a two-line and a four-line card both sit
   right in their own artwork. */
.whystack__card h3 {
  /* Sized against the CELL, not the window. Above about 1560px the row
     stops growing — it is capped at --wide — so type on a vw would have
     carried on growing past the panel it sits in and spilled. cqw tracks
     the thing the text actually has to fit. The clamp is the fallback for
     an engine without container queries, tuned to agree with the cqw at a
     normal desktop width. */
  /* Divided by --plate-w, and that division is what keeps the three
     agreeing. The container is the cell, and the cell is now the plate, so
     cqw resolves against a DIFFERENT width on each card — the sign's text
     would come out a fifth smaller than the rocket's and the row would look
     mis-set. Every plate is --plate-w of one common span, so dividing it
     back out leaves all three sized against that span. */
  font-size: clamp(1.05rem, 1.4vw, 1.5rem);
  font-size: calc(4.5cqw / var(--plate-w));
  letter-spacing: -0.02em;
  font-weight: 700;
  margin: 0 0 0.34em;
}
.whystack__card p {
  font-size: clamp(0.78rem, 0.95vw, 1.05rem);
  font-size: calc(3.0cqw / var(--plate-w));
  line-height: 1.45;
  margin: 0 auto;
  /* Stops the longest line reaching the drawn edge, which is what made the
     paragraph read as filling the panel rather than sitting in it. */
  max-width: 34ch;
  text-wrap: pretty;
  color: inherit;
  opacity: 0.92;
}
/* The plate is the artwork's own box, and it carries the artwork's aspect
   ratio so the two are the same rectangle. That is what lets the copy be
   placed as a PERCENTAGE below: the percentages resolve against the plate,
   and the plate is the image, so an inset lands on the same part of the
   drawing at every size.

   Width takes whichever limit binds first — the column on a narrow screen,
   the card's own height on a wide one — and the height follows from the
   ratio. Sized the other way round (height 100%, width from the ratio) the
   widest of the three, the speech bubble at 2:1, overflowed the column as
   soon as the window came in a little, and a max-width there would have
   broken the ratio and slid every inset off its target.

   --plate-w is what makes the three read as the SAME SIZE, and the size it
   equalises is the PANEL, not the box. Equalising the boxes was the first
   attempt and it did not look equal, because none of these three fills its
   own box the same way: measured off the artwork, the sign spends 62% of
   its box on the panel you read, the bubble 68%, and the rocket only 45% —
   the rest of that one is flame and arrow head. Three equal boxes therefore
   put a third less banner on the rocket card than sign on the sign card,
   which is exactly what it looked like.

   So a piece is sized by what it has to SHOW: panel area is w × (w / ratio)
   × frac, so for equal panels the widths go as sqrt(ratio / frac),
   normalised here against the largest. The rocket ends up with much the
   biggest box and the sign the smallest, and the three read as one set.
   Re-measure frac if any of the art is redrawn.

   It is a share of ONE span rather than a scale applied to each piece
   afterwards, and that is the point: the span takes whichever limit binds,
   and the three then divide it in fixed proportion. Scaled after the limit
   instead, the column cap flattened all three to the same width — which is
   equal width and therefore unequal area again, and the bubble came out a
   quarter smaller than the sign on any window under about 1240px.

   The three pieces are drawn to the three colours this deck used to paint
   itself with, so nothing is lost by dropping the gradients: the rocket is
   the red-orange one, the bubble the amber, the sign the navy. */
.whystack__plate {
  position: relative;
  aspect-ratio: var(--plate-ar);
  /* No min(100%) any more: with content-sized columns the cell IS this
     element, so a percentage here would be circular. --card-h is the real
     driver now rather than a ceiling that never bound, and it is what
     carries the row's fit — see its own note. */
  width: calc(var(--card-h) * var(--plate-span) * var(--plate-w));
  /* .whystack__card is a flex row, so the plate is a flex item and its
     default flex-shrink: 1 pulled every one of the three back to the cell
     width. Also the guard for the single-column layout below, where the
     column can be narrower than the plate wants to be. */
  flex: none;
  max-width: 100%;
  margin-inline: auto;
  filter: drop-shadow(0 14px 32px rgba(60, 30, 20, 0.22));
}
.whystack__art {
  display: block;
  width: 100%;
  height: 100%;
  pointer-events: none;
}
/* Inset into the drawn panel, per card, because each piece holds its panel
   somewhere different: the bubble almost fills its box, the banner sits
   inboard of the rocket's flame and the arrow head, and the sign has a post
   under it that is not writing space. */
.whystack__copy {
  position: absolute;
  display: flex;
  flex-direction: column;
  justify-content: center;
  /* Centred, because the copy is sitting on a drawn panel rather than in a
     text column: ranged left it hung off one edge of a shape that is
     symmetrical, and on the sign it disagreed with the title drawn into the
     artwork. The insets either side are not equal for the same reason —
     each one is set so the BOX is centred on its panel, and the panels are
     not centred in their boxes. */
  text-align: center;
  inset: var(--copy-top) var(--copy-right) var(--copy-bottom) var(--copy-left);
}

.whystack__card--sales {
  --plate-ar: 1222 / 720;
  --plate-w: var(--pw-sales);          /* widest drawing, so the smallest share */
  --copy-top: 13%; --copy-right: 13%; --copy-bottom: 33%; --copy-left: 20%;
}
.whystack__card--buzz {
  --plate-ar: 1161 / 578;
  --plate-w: var(--pw-buzz);          /* tightest drawing, so the largest share */
  --copy-top: 13%; --copy-right: 14%; --copy-bottom: 22%; --copy-left: 12%;
  /* White on the pale amber of this bubble is the one place in the deck
     where the copy stopped being readable once the dark panel behind it
     went away. */
  color: #6E3405;
}
.whystack__card--fans {
  --plate-ar: 1078 / 713;
  --plate-w: var(--pw-fans);
  /* Nudged left of the panel's own centre, and only on this card: the title
     drawn into this artwork sits left of centre, so copy centred on the
     panel did not line up under it. This splits the difference between the
     two centres. Another thing that goes away with a blank version of the
     art. */
  --copy-top: 34%; --copy-right: 12%; --copy-bottom: 27%; --copy-left: 5%;
}
/* The sign carries its own title, drawn into the artwork, so a second one
   underneath it said the same words twice. The h3 stays in the DOM for the
   outline and for anything reading the page rather than looking at it.
   ⚠ The drawn title is English, so the Thai page shows an English heading
   here: this card wants a version of the art with no text on it. */
.whystack__card--fans h3 {
  position: absolute;
  width: 1px; height: 1px;
  margin: -1px; padding: 0; border: 0;
  overflow: hidden; white-space: nowrap;
  clip-path: inset(50%);
}

@media (max-width: 900px) {
  /* --card-h above is solved for a ROW: it divides the window by the three
     plates' combined span, so on a 390px phone it came out at 48px and each
     plate drew 78px wide with three paragraphs stacked on top of it. One
     per row, the plate is the column: 88vw is the shell's own width, and
     dividing the span back out makes the widest plate exactly that. */
  #whystack { --card-h: calc(min(88vw, 520px) / var(--plate-span)); }
  /* One column. Three of these side by side on a phone would be a third of
     a phone wide each, and none of the copy would be readable. */
  .whystack__list {
    grid-template-columns: 1fr;
    gap: 44px;
    margin-top: 32px;
    /* The bleed above is for a row of three that needs more than the shell
       offers. One per row does not, and left on it would put the artwork
       hard against both edges of a phone. */
    margin-inline: 0;
  }
  /* The cell is 343px on a phone against ~420 in the desktop row, so the
     desktop cqw steps landed the body copy under 11px. A notch up keeps it
     readable and still inside each plate's drawn panel. */
  .whystack__card h3 { font-size: calc(5cqw / var(--plate-w)); }
  .whystack__card p { font-size: calc(3.4cqw / var(--plate-w)); }
}

/* No reduced-motion block here any more: this section has no motion left to
   switch off. The deck used to deal itself as you scrolled, and the cards
   had to be listed out flat for anyone who had asked for less of that. */



/* ── LEAVING THE CLOUDS ─────────────────────────────────────
   The entry band at the top of the page closes cloud over the reader.
   This is the same two plates flipped, so on the way out the cloud opens
   away instead. Flipping the whole band rather than swapping in mirrored
   art keeps the two ends built from one asset, so they cannot drift.
   margin-bottom: -1px on the base rule kills the hairline against the
   band BELOW, which is the wrong edge once flipped, so it moves to the
   top here. */
.cloud-band--exit {
  transform: scaleY(-1);
  margin-bottom: 0;
  margin-top: -1px;
}

/* The exit band is NOT the daylight art. Cloud1 is pure white and
   cloudYellow is cream, and the night tint lives inside the canvas
   composite, so an <img> plate could never follow it: at the bottom of the
   page that put a white slab directly beneath a night-dark section. These
   two plates use the same files as a MASK and fill them with --cloud-now,
   the colour the script publishes for the canvas cloud, so the band is the
   same colour as the clouds above it at any point in the day. Falls back
   to the page background if the script never runs. */
/* Only the BACK plate needs handling here now: the front plate is
   mask-filled for both bands by the rule above. The exit band cannot use
   the cream artwork the entry band does, because by the bottom of the page
   the cloud is night-dark and a cream plate would sit under it as a lit
   slab, so this one is mask-filled too. */
.cloud-band--exit .cloud-band__back {
  display: block;
  width: 100%;
  aspect-ratio: 1572 / 488;
  opacity: 0.95;
  background: var(--cloud-now, var(--bg));
  -webkit-mask-image: url("/assets/icons/cloudYellow.webp");
  mask-image: url("/assets/icons/cloudYellow.webp");
  -webkit-mask-repeat: no-repeat;
  mask-repeat: no-repeat;
  -webkit-mask-size: 100% 100%;
  mask-size: 100% 100%;
}

/* Past the exit band there is no cloud at all: the canvas is clipped to
   .cloudland, so it simply stops. The sections below therefore sit
   straight on the fixed sky, which by this point in the page is night.
   They must not paint their own light surfaces over it. */
.clouds-live .after-clouds,
.clouds-live .after-clouds .band,
.clouds-live .after-clouds .band--alt { background: transparent; }

/* ── CLOUD WIPE ─────────────────────────────────────────────
   The cloudland white can move onto a fixed canvas so a pointer can wipe
   it away and show the gradient body::before paints underneath. The
   handover happens only once the script is running and has set
   .clouds-live; with no JS, or under reduced motion, the rules below never
   apply and .cloudland keeps its own opaque white. */
.cloud-wipe {
  /* ABSOLUTE, not fixed, and that is the whole trick. Browsers scroll on
     the compositor thread, so the page visibly moves before any scroll
     event reaches the main thread; a fixed canvas whose openings are cut
     at viewport positions therefore slips against its sections during
     every real scroll, no matter how promptly the handler redraws. An
     absolute canvas is part of the scrolled document: the compositor
     moves cloud and content together, pixel for pixel. The script sizes
     it taller than the viewport (style.height) and re-centres it
     (style.top) as the reader moves, with the extra height hiding the
     re-centring latency. */
  position: absolute;
  top: 0;
  left: 0;
  /* width:100% is load-bearing, same lesson the ui-lab scratch card
     learned: a canvas's width/height ATTRIBUTES also act as its CSS size,
     and a replaced element ignores stretching when its width is auto.
     Without it the element rendered at backing-store size and every wipe
     landed at twice the cursor's distance from the corner. */
  width: 100%;
  /* Negative z-index puts it above body::before but below every block
     background and every line of text, so it can never cover content.
     pointer-events: none keeps it out of hit testing entirely. */
  /* -2 rather than -1: the wash is the one thing that must paint OVER this
     canvas, and it cannot, at equal z-index, because it comes earlier in the
     document than the canvas does. Everything else in the negative layer went
     down with it, so nothing changed order except the wash. */
  z-index: -2;
  pointer-events: none;
  display: none;
}
/* ── DUSK ───────────────────────────────────────────────────
   A night wash over the fixed sky, deepening as the page is descended so
   the reader travels from late afternoon into dusk. It must sit ABOVE
   body::before but BELOW the cloud canvas: same negative z-index, earlier
   in the DOM, which is exactly how the painting order resolves. Opacity is
   set by the script; with no JS it never leaves 0 and the sky is unchanged.
   Deliberately capped well short of true night — the content islands are
   holes onto this sky, so whatever darkens here darkens the ground behind
   body copy too. */
.night-sky {
  position: fixed;
  inset: 0;
  /* -2, moved down in step with the cloud canvas so the two keep the order
     they had: same z-index, this earlier in the DOM, so the cloud paints over
     the night. */
  z-index: -2;
  pointer-events: none;
  opacity: 0;
  /* Night, not dusk: near-black with only a little blue left in it. The
     script drives the opacity, so this is the colour at full night.
     The star field is the FIRST background layer, so it paints over the
     gradient. It lives on this element rather than its own, so it fades in
     with the night; and because the cloud canvas sits above, stars only
     ever show through an opening. Base64 rather than a URL-encoded SVG:
     the encoded form is one quote-escaping mistake away from silently
     failing to parse, which is exactly what happened first time. */
    /* The gradient stops are written by the script, because this is a sunset
     rather than a dimmer. Crossfading one dark plate over the warm page
     takes the ground through GREY: orange and blue sit opposite each other,
     so the midpoint of any straight blend between them is unsaturated. The
     sky instead goes opaque almost at once and it is the COLOUR that
     travels, through stops that stay saturated the whole way. */
  background-image: linear-gradient(170deg,
    var(--sky-1, #04060F) 0%,
    var(--sky-2, #070B1C) 46%,
    var(--sky-3, #12111F) 100%);
}
/* Stars on their own layer so they can arrive late. Carried on the sky
   itself they would be fading up through the orange and the blue, where
   there is no such thing as a visible star. */
.night-sky::after {
  content: "";
  position: absolute;
  inset: 0;
  pointer-events: none;
  background-image: var(--stars);
  background-repeat: repeat;
  background-size: 1400px 1000px;
  opacity: var(--star-a, 0);
}
.clouds-live .cloud-wipe { display: block; }
.clouds-live .cloudland { background: transparent; }
/* The alternating tint sits above the canvas, so opaque it would hide the
   wipe inside those bands. Translucent, it reads the same over white and
   still lets the revealed gradient through. */
/* Light enough that the gradient showing through a content cutout still
   reads as the pastel sky, not as an off-white wash — the content islands
   must stay visibly NOT part of the cloud. */
.clouds-live .cloudland .band--alt { background: rgba(253, 247, 241, 0.30); }

/* Alternating bands were a translucent white, which is now invisible
   against the white beneath. A faint warm tint keeps the alternation
   legible without breaking the cloud illusion. */
.cloudland .band--alt { background: #FDF7F1; }

/* ── HERO COINS ─────────────────────────────────────────── */
/* The opening screen was type on a gradient and nothing else, which reads
   as a still image. These drift continuously behind the headline: each
   coin has its own duration and delay so they never sync into a pulse,
   and they sit well outside the text column so nothing crosses the type. */
/* The warp sits under everything in the frame. All three layers get an
   explicit z-index, because .hero-coins was positioned at 0 while the type
   beside it was static, and a positioned sibling paints over a static one
   regardless of document order. */
.sc-hero { position: relative; z-index: 2; }

/* A PAGE layer, not a hero layer. It used to be absolute inside the hero's
   sticky frame, so it unpinned and scrolled away with the headline and the
   opening screen's coins never reached the box. Fixed to the viewport it
   carries on into the second scene, which is what makes the box read as
   somewhere the reader travelled TO rather than a new screen.

   z-index -1 is the same trick the ground uses: above body::before, below
   every block background and every line of text. That is what keeps it
   decorative. It also means .warp-inside, an opaque sheet at z-index 0,
   covers the coins by itself as the camera goes in, with no coordination
   between the two. The fade below only has to get them out before that. */
.hero-coins {
  position: fixed;
  inset: 0;
  overflow: hidden;
  pointer-events: none;
  /* -2, moved down in step with the cloud so the coins keep painting under it. */
  z-index: -2;
}
.hero-coin,
.hero-ticket {
  position: absolute;
  filter: drop-shadow(0 10px 22px rgba(26,26,46,0.12));
  /* Visible without help. An earlier version started at opacity 0 and
     relied on an animation to fade it in, which meant anything that
     stalled the animation clock left the hero with no coins at all. */
  opacity: 0.92;
  will-change: transform;
}

@keyframes coin-drift {
  0%, 100% { transform: translateY(0) rotate(var(--r, 0deg)); }
  50%      { transform: translateY(var(--d, -16px)) rotate(calc(var(--r, 0deg) + 4deg)); }
}
.js .hero-coin,
.js .hero-ticket {
  animation: coin-drift var(--t, 7s) ease-in-out var(--delay, 0s) infinite;
}

.hero-coin--1 { top: 16%; left: 7%;  width: 110px; --r: -14deg; --t: 7.5s;  --d: -18px; --delay: 0.05s; }
.hero-coin--2 { top: 63%; left: 12%; width: 72px;  --r: 22deg;  --t: 9s;    --d: -13px; --delay: 0.35s; }
.hero-coin--3 { top: 19%; right: 8%; width: 128px; --r: 13deg;  --t: 8.2s;  --d: -20px; --delay: 0.18s; }
.hero-coin--4 { top: 70%; right: 11%;width: 84px;  --r: -19deg; --t: 10s;   --d: -15px; --delay: 0.5s; }
.hero-coin--5 { top: 44%; left: 3%;  width: 58px;  --r: 40deg;  --t: 11s;   --d: -11px; --delay: 0.62s; }
.hero-coin--6 { top: 38%; right: 4%; width: 66px;  --r: -32deg; --t: 8.8s;  --d: -14px; --delay: 0.28s; }

/* Entry tickets from campaigns that have run. height: auto matters: the
   three sources are not all the same aspect, and a shared height would
   squash one shape or the other.
   Placed in the gaps the coins leave rather than on top of them: each slot
   below was checked against the coin above and beside it for overlap once
   the drift is at full travel, and against the text column, which starts
   at about 34% of the window. */
.hero-ticket {
  height: auto;
  /* Tickets carry their own shadow in the art, so this is lighter than the
     coins' and just lifts them off the sky. */
  filter: drop-shadow(0 8px 18px rgba(26,26,46,0.16));
  opacity: 0.96;
}
/* Slots measured against the coins and the text column at 1512x900, with
   the drift at full travel counted on both sides:
     1 Golden Mountain  x  212-362  y 343-412  (coin 1 ends y254, coin 5 x103)
     2 Mellolica        x   76-204  y 775-835  (coin 2 ends y639)
     3 UECOF            x 1150-1300 y 415-484  (coin 3 ends y299, coin 6 x1391)
     4 BaiSed           x 1308-1467 y 541-614  (coin 6 ends y481, coin 4 starts y703)
   The text column runs 451-1061, so all four stay out of it entirely, and
   nothing reaches the 900 window bottom, so no slot is clipped.
   BaiSed is the house draw and gets the largest slot of the four. It sits
   in the gap BETWEEN coins 6 and 4 rather than under coin 4: the obvious
   bottom-right slot leaves only 6px of daylight above that coin, and both
   objects drift on their own clocks, so they would have met. */
.hero-ticket--1 { top: 30%; left: 14%;  width: 150px; --r: -10deg; --t: 9.4s;  --d: -16px; --delay: 0.12s; }
.hero-ticket--2 { top: 78%; left: 5%;   width: 128px; --r: 15deg;  --t: 10.6s; --d: -12px; --delay: 0.44s; }
.hero-ticket--3 { top: 38%; right: 14%; width: 150px; --r: 12deg;  --t: 8.6s;  --d: -18px; --delay: 0.22s; }
.hero-ticket--4 { top: 52%; right: 3%;  width: 158px; --r: -14deg; --t: 9.9s;  --d: -15px; --delay: 0.70s; }

/* Narrow screens: the text column takes the middle, so only keep the
   coins that stay clear of it and shrink them. */
@media (max-width: 860px) {
  /* Tickets are the widest objects in the hero and the text column takes
     the middle of a narrow window, so there is no lane left for them.
     They go rather than shrink: at phone size a 90px ticket is unreadable
     as a ticket and reads as a coloured smudge. */
  .hero-ticket { display: none; }
  .hero-coin--5, .hero-coin--6 { display: none; }
  .hero-coin--1 { width: 68px; top: 12%; left: 4%; }
  .hero-coin--2 { width: 48px; left: 6%; }
  .hero-coin--3 { width: 76px; top: 14%; right: 4%; }
  .hero-coin--4 { width: 54px; right: 5%; }
}

@media (prefers-reduced-motion: reduce) {
  .js .hero-coin,
  .js .hero-ticket { animation: none; transform: rotate(var(--r, 0deg)); }
}

/* ── SALES CHANNELS ─────────────────────────────────────── */
/* Whether a brand sells in store, online or both is a qualifying question,
   so it gets stated rather than left as one bullet in a list. */
.channels {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(260px, 1fr));
  gap: 18px;
  margin-top: 28px;
}
.channel {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 22px;
  corner-shape: squircle;
  padding: 24px 26px;
}
.channel__tag {
  display: inline-block;
  font-size: 0.66rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: #c2453f;
  background: rgba(255, 107, 107, 0.12);
  border-radius: 100px;
  padding: 4px 12px;
  margin-bottom: 12px;
}
.channel h3 { font-size: 1.05rem; font-weight: 500; margin-bottom: 8px; letter-spacing: -0.01em; }
.channel p { font-size: 0.88rem; color: var(--muted); line-height: 1.65; }
.channels__note {
  margin-top: 16px;
  font-size: 0.82rem;
  color: var(--faint);
  line-height: 1.6;
}

/* ── CHANNEL DEMO VIDEOS ─────────────────────────────────
   Two screen recordings of the real app, one per entry channel.
   They are muted, looping, and only start once their card is on
   screen — 4.5MB should not be spent by a reader who never scrolls
   this far. Aspect is the source's own 572 × 1180.               */
.channel__phone {
  margin-top: 18px;
  border-radius: 20px;
  overflow: hidden;
  aspect-ratio: 572 / 1180;
  max-width: 250px;
}
/* No bezel and no backing panel: these are screen recordings that already
   contain the phone's own status bar and chrome, so a frame here reads as
   a frame around a frame. contain rather than cover, because the two
   sources differ by 2px in width and cover would shave one of them. */
.channel__video {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: contain;
}
@media (max-width: 760px) {
  .channel__phone { max-width: 210px; margin-inline: auto; }
}
@media (prefers-reduced-motion: reduce) {
  /* Never autoplay; the script checks this too, so the frame just sits there. */
  .channel__phone { outline: none; }
}

/* ── PERMIT DOCUMENT CHECKLIST ──────────────────────────────
   The two-column "what you send / what we file" split under the
   permit timeline, plus the obligations list under it.
   ────────────────────────────────────────────────────────── */
.docs {
  display: grid;
  /* One column now that the filing card is gone. It was 1fr 1fr, stated
     rather than auto-fit, because auto-fit laid down a third empty track at
     the shell width and nudged the pair off centre; with a single card left
     that same fixed pair would leave an empty half. */
  grid-template-columns: 1fr;
  gap: 22px;
  margin-top: 40px;
}
/* A single prose card runs the width of the shell, which is far too long a
   line to read, so its list splits into two. Scoped to :only-child, so
   nothing changes if a second card is put back beside it, and excluding
   the stats card below, which sets its own columns. */
@media (min-width: 901px) {
  .docs > .docs__col:only-child:not(.docs__col--load) .docs__list {
    columns: 2;
    column-gap: 40px;
  }
  .docs > .docs__col:only-child:not(.docs__col--load) .docs__list li { break-inside: avoid; }
}

/* ── WHAT IT TAKES ───────────────────────────────────────
   The same facts the prose card carried, as figures. A reader skims five
   numbers and understands the size of the job in a second, where five
   sentences asked them to read for it. The last two are deliberately
   awkward numbers to look at: 30 days that have to be planned around, and
   a zero that is a prohibition rather than a quantity.

   The closing line is the turn: the list is heavy on purpose, and then it
   says none of it is yours to carry. */
.loadstats {
  list-style: none;
  margin: 0;
  padding: 0;
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(168px, 1fr));
  gap: 20px 26px;
}
.loadstats li {
  margin: 0;
  padding: 0 0 0 16px;
  border-left: 3px solid rgba(255, 107, 107, 0.34);
  min-width: 0;
}
.loadstats li::before { content: none; }
.loadstats b {
  display: block;
  font-size: 2.1rem;
  font-weight: 800;
  line-height: 1.05;
  color: var(--ink);
  font-variant-numeric: tabular-nums;
  letter-spacing: -0.01em;
}
.loadstats span {
  display: block;
  margin-top: 5px;
  font-size: 0.86rem;
  font-weight: 600;
  color: var(--ink);
}
.loadstats em {
  display: block;
  margin-top: 5px;
  font-style: normal;
  font-size: 0.78rem;
  line-height: 1.45;
  color: var(--muted);
}
.loadstats__foot {
  margin: 26px 0 0;
  padding-top: 18px;
  border-top: 1px solid var(--line);
  font-size: 0.9rem;
  font-weight: 600;
  color: var(--ink);
}
@media (max-width: 700px) {
  .loadstats { gap: 16px 20px; }
  .loadstats b { font-size: 1.7rem; }
}
.docs__col {
  background: var(--card);
  border: 1px solid var(--line);
  border-radius: 20px;
  padding: 24px 26px;
}
.docs__col h3,
.docs__heading {
  font-size: 1.02rem;
  font-weight: 600;
  letter-spacing: -0.01em;
  margin-bottom: 14px;
}
.docs__heading { margin-top: 38px; }
/* The fee table sits in the same card language as .docs__col, but the
   table already carries its own cell padding, so the card's own padding
   would double it up on every edge. Zero it here and let table-wrap's
   overflow scroll right up to the card's rounded corner. */
.docs__col--table { padding: 0; overflow: hidden; }
.docs__col--table .table-wrap { padding: 4px 6px; }

.docs__list { list-style: none; }
.docs__list li {
  position: relative;
  padding-left: 26px;
  margin-bottom: 12px;
  font-size: 0.9rem;
  line-height: 1.65;
  color: var(--muted);
}
.docs__list li:last-child { margin-bottom: 0; }
.docs__list li::before {
  content: '✓';
  position: absolute;
  left: 0;
  top: 0;
  color: var(--red);
  font-weight: 700;
}
.docs__list li b { color: var(--ink); font-weight: 600; }

@media (max-width: 760px) {
  .docs { grid-template-columns: 1fr; gap: 14px; margin-top: 30px; }
  .docs__col { padding: 20px; }
}

/* ── THE PERMIT SECTION'S GROUND ────────────────────────────
   Every section past the sunset is a hole onto the fixed night
   sky, so this one had no colour of its own. It now carries the
   navy the reference page is built on, #1D1842, poured over the
   sky and masked back into it across 140px at each edge so the
   band joins the night rather than being cut into it. The middle
   of the section is that navy exactly.

   Two layers, not one. The navy alone would hide the star field
   for the length of the section, which is visible as a blank
   rectangle between two sections that still have stars, so the
   sheet is re-laid on top at whatever opacity the sky is running.
   background-attachment: fixed anchors it to the viewport exactly
   as .night-sky::after is anchored, so inside the fade the two
   copies sit on the same stars instead of scrolling apart.

   Gated on .clouds-live: with no JS, or under reduced motion, the
   sky never falls and the page stays daylight with dark ink — a
   navy slab there would be unreadable. */
.clouds-live #license { position: relative; isolation: isolate; }
/* This band paints its OWN ground, which is why setting `background` on the
   section itself does nothing: the ::before below covers it. It used to be
   #1D1842 with a starfield masked to fade into the sky at both seams, so the
   permit checklist sat on a night sky. That is the wrong backdrop for the
   one section on the page that is a working document rather than a pitch.

   Flat slate now, no stars, and no mask. The mask existed to blend this band
   into the sky above and below; a solid band wants a clean edge instead, and
   it already meets a white block above it. The section keeps its own
   `background` too, as the fallback for when .clouds-live is absent, which
   is the no-script and reduced-motion case. */
.clouds-live #license::before {
  content: "";
  position: absolute;
  inset: -1px 0;                /* -1px, or a hairline of sky shows at the seam */
  /* -2, moved down in step with the cloud so it keeps painting under it. */
  z-index: -2;
  pointer-events: none;
  /* The section's own token, so the lit band and the no-script fallback
     below can never drift apart. Inherited: this is a pseudo of #license. */
  background: var(--ground);
}
/* The starfield is gone from this band. */
.clouds-live #license::after { content: none; }

/* ── DYNAMIC PERMIT CHECKLIST ───────────────────────────────
   The document set changes with who files and where the draw is
   held, so the list is built from those two answers rather than
   printing every branch and making the reader filter it.
   ────────────────────────────────────────────────────────── */
/* A flat slate ground, and no purple in it. This band used to be the night
   sky, which put a permit checklist on a starfield: the one section on the
   page that is a working document rather than a pitch. Solid, and neutral,
   so the form reads as a form.

   It states its own tokens for the same reason the apricot band above does.
   Its ground no longer follows the day-to-night driver, and under reduced
   motion that driver never runs at all. --bg is set too, and that one is
   load-bearing: see the filter pills below. */
#license {
  position: relative;
  /* The ground, and the only value to change to re-pitch this band. It is
     read here and by the .clouds-live ::before above, which is what
     actually paints when the sky has fallen.

     Butter, the page's own. This band went from near-black to a mid slate
     to white to here, and the white step was the one that changed its
     nature rather than its shade: everything below that was written for
     light type on a dark ground is inverted with it, so the ink, the
     filter pills, the progress track, the completion green and the lift
     under every sheet all flipped in that move. Changing --ground alone
     would not have done it, and nothing below needs revisiting for this
     step, which only warms the same light ground.

     #FFFBEE is not a new colour on this page. It is the ground of the
     paint band and of #why, so the permit stops reading as an isolated
     white slab dropped between two sections that are already butter.

     One consequence worth knowing. The sheets are pale stocks, so on a
     ground this light they need an edge of their own or the palest of
     them dissolve into it; that is the hairline in the base shadow
     below, drawn as an inset so it costs no layout. It matters more here
     than it did on white: the form sheet is #FDFBF6, which is 4 points
     of blue away from this ground and nothing else. */
  --ground: #FFFBEE;
  background: var(--ground);
  /* Day ink, measured on the page rather than calculated: the heading
     lands at 17.8, the eyebrow and the ready-count at 7.0, and the two
     smallest pieces of type, the pill-group label and the foot note, at
     5.1. The faint alpha is up from the 0.58 the page uses elsewhere,
     because at that value those two measured 4.35 against white, under
     AA. It is the first token to fail whenever this ground moves. */
  --ink: #16181D;
  --muted: rgba(22, 24, 29, 0.72);
  --faint: rgba(22, 24, 29, 0.66);
  --line: rgba(22, 24, 29, 0.12);
  --card: #F7F8FA;
  --bg: #F6F7F9;
  --bg2: #EDEEF2;
  color: var(--ink);
}

/* ── THE LEAD FORM'S GROUND ─────────────────────────────────
   The same construction as the permit band above, for the same reason:
   this section had no ground of its own and simply sat on the night sky,
   stars and all. A form is the other thing on this page that is a working
   surface rather than a pitch, and the last thing a reader touches before
   writing to us, so it gets daylight.

   The FIRST SCREEN'S SKY, brought down to the last one. The top of this
   page is a three-stop gradient that lucky-draw-clouds.js writes onto
   .night-sky and then walks from dawn to midnight as the reader scrolls;
   the three values below are that gradient's stops at t = 0, which is what
   the hero actually paints. Frozen, not live: by the time a reader reaches
   this band the script has taken the real sky to near-black, so following
   it would give the opposite of what is wanted here.

   The effect is that the page ends where it began. The enquiry form sits
   on the same warm cream the reader first saw, after a long night in
   between, which is a better argument for this band than any colour picked
   on its own merits. Keep it in step with SKY[0] in lucky-draw-clouds.js if
   that dawn is ever retuned.

   Everything inside is token-driven, so the whole section inverts from the
   six values below: the form card and the contact cards take --card and
   become white plates on the blush, the inputs take --bg and stay a shade
   warmer so they read as fields inside those plates, and every label,
   border and caption follows --muted, --line and --faint. Nothing in the
   form itself needed touching. */
.clouds-live #lead { position: relative; isolation: isolate; }
.clouds-live #lead::before {
  content: "";
  position: absolute;
  inset: -1px 0;                /* -1px, or a hairline of sky shows at the seam */
  z-index: -2;
  pointer-events: none;
  background-color: var(--ground);
  background-image: var(--ground-sky);
}
/* No starfield over a daylight band. */
.clouds-live #lead::after { content: none; }
#lead {
  position: relative;
  /* The ground, and the only value to change to re-pitch this band. Read
     here and by the ::before above, which is what actually paints once the
     sky has fallen; this one is the no-script and reduced-motion case. */
  /* Two properties, and both are load-bearing. The gradient is what the
     reader sees. The flat colour under it is its midpoint, and it exists
     because the sticky nav finds the ground by walking up from a hit test
     looking for an opaque background-COLOUR: a section carrying only a
     gradient reads as transparent to that walk, the nav falls through to
     the night sky behind, and it goes white-on-cream. */
  --ground: #FBE9D4;
  --ground-sky: linear-gradient(170deg, #FCF0E2 0%, #FBE9D4 46%, #F9E2C8 100%);
  background-color: var(--ground);
  background-image: var(--ground-sky);
  /* Every value below is derived from the ground rather than picked beside
     it: the ink is the ground taken almost to black, so it keeps a trace of
     the same green and belongs to the colour instead of sitting on top of
     it; --bg is the ground lifted toward white, which is what keeps the
     inputs reading as fields inside a white card; --bg2 is the ground a
     step down.

     The faint alpha is above the 0.60 the page uses elsewhere, and that is
     load-bearing. The tightest pairing in this band is not anything on the
     ground: it is the job titles under the two names, which sit on a WHITE
     card. At 0.60 they measure 4.2, under AA. */
  --ink: #222020;
  --muted: rgba(34, 32, 32, 0.74);
  --faint: rgba(34, 32, 32, 0.66);
  --line: rgba(34, 32, 32, 0.14);
  --card: #FFFFFF;
  --bg: #FDF3E7;
  --bg2: #F1E0CB;
  color: var(--ink);
}

/* THE DOCUMENTS, DRAWN AS DOCUMENTS. Every line in this list is a piece of
   paper a brand has to physically produce, so the list is a scatter of
   sheets rather than a column of rows. The arrangement is the Beagle site's
   (curvy.dk/beagle), where proposals lie across the ground at slight
   angles; here the ground is the section's own slate and each sheet is one
   requirement, with a stamp in its corner that you tick.

   The panel that used to hold the rows is gone: a card inside a card inside
   a band was three surfaces deep, and the papers want to lie on the ground,
   not in a tray. */
.dchk {
  margin-top: 34px;
  background: none;
  border: none;
  border-radius: 0;
  padding: 0;
}
.dchk__controls { display: flex; flex-wrap: wrap; gap: 22px; margin-bottom: 18px; }
.dchk__group { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.dchk__label {
  font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
  color: var(--faint); margin-right: 2px;
}
/* A tint of the surface it sits on, NOT var(--bg). --bg is the one colour
   token the day-to-night driver never inverts, so on the night page these
   came out as a near-white pill carrying near-white text: measured 1.03:1,
   which is why two of the four filters looked like empty capsules.
   A translucent INK, now that the band is white: a translucent white here
   would be the same bug the other way up, an invisible capsule on an
   invisible ground. */
.dchk__group button {
  font-family: inherit;
  font-size: 0.82rem;
  padding: 7px 15px;
  border-radius: 100px;
  border: 1px solid var(--line);
  background: rgba(22, 24, 29, 0.05);
  color: var(--muted);
  cursor: pointer;
  transition: background .2s, color .2s, border-color .2s;
}
.dchk__group button:hover {
  background: rgba(22, 24, 29, 0.10);
  border-color: rgba(22, 24, 29, 0.28);
  color: var(--ink);
}
.dchk__group button[aria-pressed="true"] {
  background: var(--fire); color: #fff; border-color: transparent; font-weight: 500;
}

/* The count carries a bar now. A checklist that reports "2 of 5 ready" in
   text alone makes the reader do the arithmetic to feel any progress; the
   bar is the same fact in a form you can read without counting. It replaces
   the rule that used to sit under this line rather than adding to it. */
.dchk__count {
  font-size: 0.82rem;
  font-weight: 600;
  color: var(--muted);
  margin-bottom: 10px;
}
.dchk__count::after {
  content: '';
  display: block;
  margin-top: 11px;
  height: 4px;
  border-radius: 100px;
  /* --done is a plain number, 0 to 100, written by lucky-draw-checklist.js.
     A literal gradient, not var(--fire): --fire IS a gradient, so it cannot
     stand where a background-image needs its own stops. */
  background-image: linear-gradient(90deg, #FF6B6B, #FFBD71);
  background-repeat: no-repeat;
  background-size: calc(var(--done, 0) * 1%) 100%;
  background-color: rgba(22, 24, 29, 0.10);
  transition: background-size 0.35s cubic-bezier(.2, .8, .25, 1);
}
/* Back to the white-card green. #5FD39A was picked when this band was
   dark and measures 1.9:1 on white; this one lands at 4.8. */
.dchk__count.is-complete { color: #1F7A4D; }

/* ── THE BOARD ────────────────────────────────────────────
   The sheets are pinned to cork now, and that is a layout decision as much
   as a look. The block used to be a bento: every document was given an
   explicit grid-area so the ten tiled into a flawless rectangle, at four
   widths, with and without the venue letter. It cost three hand-fitted
   tilings and it bought a shape nobody was asking for, because the sizes
   were assigned to make the rectangle work rather than to fit the words.
   The venue letter had the least to say of the ten and the second tallest
   tile on the board, and 926px of the block was blank.

   A pinboard does not tessellate. Papers are whatever size their content
   is, they sit at whatever angle the pin left them, and the gaps between
   them are board rather than holes. So the grid is gone: the sheets flow
   down four columns at their natural heights, and the cork shows through
   wherever they do not reach.

   The cork is one layer of fine fractal noise blended soft-light over a
   warm base, plus a diagonal sheen. It started as four sparse speckle
   layers at sizes with no common factor; they still tiled into a visible
   grid of dots at any zoom, because a repeating dot is a repeating dot
   whatever its period. Noise has no period. The svg is inline, so
   nothing is fetched.

   A more literal cork was tried on top of this: fractal-noise granules
   and mottling, pinholes, a vignette, and a wood frame with per-rail
   grain and bevelled edges. It was rolled back at the owner's request in
   favour of this quieter surface. If it is ever wanted again, the whole
   thing was one background declaration on this rule. */
.dchk__list {
  list-style: none;
  margin-top: 6px;
  columns: 4;
  column-gap: 18px;
  padding: 26px 20px 8px;
  /* Measured on the outer edge. The cork inside rounds by this minus the
     frame width, so 18 on a 12px frame gives the cork a 6px corner rather
     than a square one, and the inset shadows follow it. */
  border-radius: 18px;
  background-color: #C0905C;
  background-image:
    url("data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' width='220' height='220'%3E%3Cfilter id='g'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.95' numOctaves='3' seed='7' stitchTiles='stitch'/%3E%3CfeColorMatrix values='1.4 0 0 0 -0.2  1.4 0 0 0 -0.2  1.4 0 0 0 -0.2  0 0 0 0 1'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23g)'/%3E%3C/svg%3E"),
    linear-gradient(148deg, rgba(255, 230, 194, 0.20), rgba(116, 70, 28, 0.18));
  background-size: 220px 220px, 100% 100%;
  background-blend-mode: soft-light, normal;
  border: 12px solid #77492A;
  box-shadow:
    inset 0 0 0 1px rgba(58, 32, 10, 0.45),
    inset 0 12px 28px rgba(58, 32, 10, 0.26),
    0 16px 38px rgba(22, 24, 29, 0.17);
}
.dchk__item {
  position: relative;
  display: block;
  /* A column box may not split a sheet across two columns. */
  break-inside: avoid;
  -webkit-column-break-inside: avoid;
  margin: 0 0 18px;
  /* Room at the top for the pin that holds it. */
  padding: 28px 20px 21px;
  transform: rotate(var(--tilt, 0deg));
  /* 3px, not a card radius. Paper has a corner, not a rounded edge. */
  border-radius: 3px;
  /* One property carries the whole sheet. Each document sets its own
     --paper below; the ticked state, and anything else that needs to
     follow the stock, derives from this rather than restating a colour. */
  --paper: #FAF9F6;
  background: var(--paper);
  /* Two jobs in one property. The hairline is an INSET, so a sheet whose
     stock is nearly the colour of the ground still has an edge without
     taking a border and shifting the grid by two pixels. The drop is much
     lighter than the one this carried on the dark band: a 0.32 black at
     30px was reading as a bruise once the ground went white. */
  box-shadow:
    inset 0 0 0 1px rgba(22, 24, 29, 0.07),
    0 1px 2px rgba(22, 24, 29, 0.06),
    0 10px 24px rgba(22, 24, 29, 0.10);
  transition: transform .3s cubic-bezier(.2,.8,.25,1), box-shadow .3s ease, background .3s ease;
}
/* The sheets used to be laid down at four angles on a cycle. A bento has
   to tessellate, and a tile rotated inside its own cell either leaves a
   wedge of gap at the corners or laps over its neighbour, so the tiles now
   sit flush and the hand-laid angle moves onto the tags instead, which is
   what the reference does with its stickers. */
/* Lifted off the board and pulled most of the way straight, the way a
   hand does before reading one. The tilt is kept at a fraction rather than
   zeroed, so the sheet still belongs to the board while it is up. */
.dchk__item:hover {
  transform: rotate(calc(var(--tilt, 0deg) * 0.25)) translateY(-5px);
  box-shadow:
    inset 0 0 0 1px rgba(22, 24, 29, 0.07),
    0 2px 4px rgba(22, 24, 29, 0.07),
    0 18px 38px rgba(22, 24, 29, 0.16);
}
/* The band the stamp is pressed into. It draws nothing.

   Every sheet used to carry an illustration here: ruled lines under the
   copy, and per document a seal, a photo box, a pad, a receipt, the boxes
   of a form. All of it is gone. At the size these tiles are it read as a
   sheet still loading rather than as paper, and with a stamp now on every
   document the sheet has something to look at without it.

   The empty box stays because it is what keeps the stamp off the words.
   The stamp is absolutely positioned near the bottom edge, and nothing
   else stops a long paragraph from growing down into the same place. How
   long the paragraph is depends on the language: the letter of intent
   cleared its stamp in English and ran 9px into it in Thai. Reserving the
   band here fixes that for any copy in any language, because this box
   always sits between the last line of text and the bottom of the sheet.
   Tiles with room absorb it and do not move; a tile without room grows a
   little, which is the right outcome. */
.dchk__item::after {
  content: '';
  display: block;
  margin-top: 16px;
  min-height: 46px;
}
/* The stamp. A real checkbox still, just wearing the seal: appearance:none
   keeps the control, the focus ring and the label association intact. */
.dchk__item input {
  appearance: none;
  -webkit-appearance: none;
  position: absolute;
  top: 15px; right: 15px;
  margin: 0;
  width: 25px; height: 25px;
  border-radius: 50%;
  border: 2px solid rgba(22, 24, 29, 0.22);
  background: transparent;
  cursor: pointer;
  transition: background-color .2s, border-color .2s;
}
.dchk__item input:checked {
  border-color: #D4452F;
  background-color: #D4452F;
  background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='white' stroke-width='3.4' stroke-linecap='round' stroke-linejoin='round'><path d='M4 12.5l5.5 5.5L20 7'/></svg>");
  background-size: 14px;
  background-position: center;
  background-repeat: no-repeat;
}
.dchk__item input:focus-visible { outline: 2px solid #D4452F; outline-offset: 3px; }
.dchk__item label { cursor: pointer; display: block; padding-right: 30px; }
/* Everything on a sheet is stated in ink, not in the band's tokens: the
   band is dark and the paper is not, so --ink and --muted are the wrong
   colours the moment they land here. The who-filed-it badge that used to
   sit here is gone — every document is "You" now that filing through an
   agency asks for nothing extra, so the badge said the same word on every
   single card and stopped carrying information. Its top margin moves onto
   the title, so the sheet keeps the same clearance under the stamp. */
.dchk__item b {
  display: block;
  font-size: 1rem;
  font-weight: 700;
  letter-spacing: -0.01em;
  line-height: 1.28;
  color: #16181D;
  margin: 4px 0 8px;
}
.dchk__d {
  display: block;
  font-size: 0.81rem;
  line-height: 1.5;
  color: rgba(22, 24, 29, 0.66);
  margin-top: 0;
}
/* Filed. The sheet goes flat and grey and the stamp is the only colour
   left on it. */
/* Ticked: the same stock, taken down toward the ink so it reads as filed
   rather than as a different document. Derived, so a new colour above
   needs no second value here. The flat fallback is for browsers without
   color-mix, where every done sheet lands on the old shared grey. */
.dchk__item.is-done {
  background: #EDEBE4;
  background: color-mix(in srgb, var(--paper) 86%, #16181D);
  /* Filed: it settles onto the desk, so the drop shortens rather than
     disappearing, and it keeps its edge. */
  box-shadow:
    inset 0 0 0 1px rgba(22, 24, 29, 0.07),
    0 1px 2px rgba(22, 24, 29, 0.05),
    0 4px 10px rgba(22, 24, 29, 0.07);
}
.dchk__item.is-done b { text-decoration: line-through; color: rgba(22, 24, 29, 0.45); }
.dchk__item.is-done .dchk__d { color: rgba(22, 24, 29, 0.42); }

/* ── One tile per document ──────────────────────────────────
   Everything above is the default sheet. What follows gives each document
   its own size and its own paper, so the grid reads as ten different
   things to gather rather than ten copies of one thing. Keyed off
   data-doc, which lucky-draw-checklist.js writes from the document id, so
   the set can be reordered or a document dropped without any of this
   pointing at the wrong tile the way nth-child would.

   Anything without a rule here is a plain small sheet, which is the right
   default: a new document is a normal one until somebody decides it is
   not. ────────────────────────────────────────────────────── */

/* Paper stock, one per document. Muted on purpose: these are sheets of
   paper in a pile, not a set of category chips, so the difference between
   any two is the difference between two reams rather than a hue wheel.
   Every one is light enough to carry the same dark ink, so nothing else in
   the tile changes with the colour.

   The artwork and the venue letter are not here. They are the two sheets
   that mean something beyond being paper, the one that must not be
   published early and the one that only exists if you draw off-site, and
   they keep the solid orange and the dark stock that say so. Colouring the
   other eight is what makes those two read as deliberate rather than as
   the only two anyone bothered to style. */
.dchk__item[data-doc="form"]   { --paper: #FDFBF6; }  /* the permit itself, the cleanest sheet */
.dchk__item[data-doc="intent"] { --paper: #E4EDF7; }  /* a letter */
.dchk__item[data-doc="cert"]   { --paper: #E3F0E6; }  /* a certificate */
.dchk__item[data-doc="tm"]     { --paper: #ECE6F7; }
.dchk__item[data-doc="poa"]    { --paper: #F9E6E0; }  /* warm: this is the one that gets refused */
.dchk__item[data-doc="id"]     { --paper: #E8EBEE; }  /* a photocopy */
.dchk__item[data-doc="pc14"]   { --paper: #F6EBCF; }  /* manila, an official form */
.dchk__item[data-doc="prize"]  { --paper: #DFEFEA; }

/* How each sheet was pinned. Fixed per document rather than random, so a
   sheet does not jump to a new angle on every render, and small: past
   about two degrees a paragraph starts to read as tilted rather than the
   paper it sits on. The form is nearly square because it is the biggest
   sheet and the eye measures big things against the frame. */
.dchk__item[data-doc="form"]   { --tilt: -0.5deg; }
.dchk__item[data-doc="intent"] { --tilt:  1.3deg; }
.dchk__item[data-doc="cert"]   { --tilt: -1.1deg; }
.dchk__item[data-doc="tm"]     { --tilt:  0.8deg; }
.dchk__item[data-doc="poa"]    { --tilt: -1.4deg; }
.dchk__item[data-doc="id"]     { --tilt:  1.6deg; }
.dchk__item[data-doc="pc14"]   { --tilt: -0.9deg; }
.dchk__item[data-doc="prize"]  { --tilt:  1.2deg; }
.dchk__item[data-doc="ads"]    { --tilt: -1.2deg; }
.dchk__item[data-doc="venue"]  { --tilt:  1.0deg; }

/* The pin. It reuses ::before, which the dog-ear used to hold, and takes
   its head colour from --stamp so the pin and the stamp on a sheet are the
   same ink. Two gradients: the highlight sits off centre, which is what
   makes a flat circle read as a dome. */
.dchk__item::before {
  content: '';
  position: absolute;
  z-index: 4;
  top: 8px;
  left: 50%;
  width: 15px;
  height: 15px;
  margin-left: -7.5px;
  border-radius: 50%;
  background:
    radial-gradient(circle at 34% 28%, rgba(255, 255, 255, 0.92) 0 2.5px, transparent 3.2px),
    radial-gradient(circle at 52% 46%, rgb(var(--stamp, 22 24 29)) 0 58%, rgba(20, 10, 4, 0.55) 100%);
  box-shadow: 0 2px 5px rgba(30, 16, 6, 0.5);
}

/* The application form. The one sheet that IS the permit, so it gets the
   type to match. */
.dchk__item[data-doc="form"] { padding: 26px 28px 28px; }
.dchk__item[data-doc="form"] b { font-size: 1.6rem; line-height: 1.15; margin-bottom: 12px; }
.dchk__item[data-doc="form"] .dchk__d { font-size: 0.92rem; max-width: 30ch; }




/* Proof of the prizes is a receipt: a narrow column of amounts, torn off
   at the bottom. */
.dchk__item[data-doc="prize"] {
  -webkit-mask-image: linear-gradient(#000 0 0), conic-gradient(from -45deg at 50% 0, #000 90deg, transparent 0);
  mask-image: linear-gradient(#000 0 0), conic-gradient(from -45deg at 50% 0, #000 90deg, transparent 0);
  -webkit-mask-size: 100% calc(100% - 9px), 16px 9px;
  mask-size: 100% calc(100% - 9px), 16px 9px;
  -webkit-mask-position: 0 0, 0 100%;
  mask-position: 0 0, 0 100%;
  -webkit-mask-repeat: no-repeat, repeat-x;
  mask-repeat: no-repeat, repeat-x;
  padding-bottom: 30px;
}


/* On letterhead, says the copy, so the sheet gets one. */
.dchk__item[data-doc="intent"] {
  background-image: linear-gradient(#D4452F 0 6px, transparent 6px);
}

/* The power of attorney is the one that carries a duty stamp, and it is
   the sheet most filings are refused over. */
.dchk__item[data-doc="poa"] { padding-right: 26px; }
.dchk__item[data-doc="poa"] .dchk__d { max-width: 46ch; }

/* The campaign artwork is the only thing in the set that is not paperwork,
   so it is the only tile that is not paper. This is the grid's colour, and
   one tile is the right amount of it: a second would start to argue with
   the point that these are all documents. */
.dchk__item[data-doc="ads"] {
  background: linear-gradient(122deg, #D4452F, #E8743A);
  box-shadow: 0 14px 30px rgba(212, 69, 47, 0.34);
}
.dchk__item[data-doc="ads"] b { color: #fff; }
.dchk__item[data-doc="ads"] .dchk__d { color: rgba(255, 255, 255, 0.9); max-width: 42ch; }
.dchk__item[data-doc="ads"] input { border-color: rgba(255, 255, 255, 0.6); }
.dchk__item[data-doc="ads"] input:checked { border-color: #16181D; background-color: #16181D; }
.dchk__item[data-doc="ads"].is-done { background: #6E4034; }

/* The venue letter is the only conditional document in the set. A cut edge
   says so before the copy does.

   It used to be a translucent WHITE over the dark band, which is exactly
   the wrong construction once the band turns white: a 9%-white wash on a
   white ground carrying near-white type is invisible. It is a solid dark
   stock now, so it stays the odd sheet out on any ground this section is
   ever given, and its dashed edge reads against the paper around it. */
.dchk__item[data-doc="venue"] {
  --paper: #2A2F3A;
  border: 1.5px dashed rgba(250, 249, 246, 0.42);
}
.dchk__item[data-doc="venue"] b { color: #FAF9F6; }
.dchk__item[data-doc="venue"] .dchk__d { color: rgba(250, 249, 246, 0.78); }
.dchk__item[data-doc="venue"] input { border-color: rgba(250, 249, 246, 0.5); }
/* The only sheet whose ticked state goes UP rather than down: the derived
   mix takes every other stock toward the ink, and this stock is already
   there, so filing it would turn it black. */
.dchk__item[data-doc="venue"].is-done { background: #3B4250; }

/* ── The stamp ─────────────────────────────────────────────
   What used to be a small rotated sticker on six of the sheets. It is a
   rubber stamp now, and on all ten, because the tiles are taller than
   their words and a sticker pinned to the bottom edge of a 400px sheet
   floats 200px away from the sentence it is warning about.

   It is PRESSED ON THE PAPER, low in the tile and clear of the copy. It
   is absolutely positioned rather than laid out in flow, which is what
   lets one construction work on a 171px artwork tile and a 478px one
   without either reserving space of its own or overflowing. The ink is
   translucent because that is how a stamp sits on a page, not because
   there is anything left underneath to show through.

   ONE COLOUR PER DOCUMENT, set below. Each is chosen to sit on a different
   hue from its own stock, which is the whole point of a stamp being
   visible: crimson on the green certificate, burnt orange on the blue
   letterhead, purple on the manila pad, rose on the mint receipt. The two
   dark tiles invert it and stamp in light ink instead.

   The alphas below are not a look, they are a floor. Ink at 0.88 over a
   0.05 wash of itself puts every one of these ten, composited on its own
   stock, at 5:1 or better against that stock. An earlier 0.74 was prettier
   and left six of them between 3.2 and 4.1, which on the lavender
   trademark sheet and the pale blue letterhead read as a watermark rather
   than a stamp. The one that cannot reach 5:1 is the artwork tile: light
   ink on that orange tops out near 4, which is also where its own white
   title sits, so it is the card's ceiling and not the stamp's.

   pointer-events: none so it never takes a click off the sheet, which is
   now one big target. ──────────────────────────────────────── */
.dchk__stamp {
  position: absolute;
  left: 20px;
  bottom: 17px;
  z-index: 2;
  /* off the horizontal, hand-pressed. transform-origin at the corner it is
     anchored by, so the rotation cannot walk it off the left edge. */
  transform: rotate(-8.5deg);
  transform-origin: left bottom;
  padding: 7px 13px 6px;
  border: 2.5px solid rgb(var(--stamp, 22 24 29) / 0.58);
  border-radius: 5px;
  background: rgb(var(--stamp, 22 24 29) / 0.05);
  color: rgb(var(--stamp, 22 24 29) / 0.88);
  font-size: var(--stamp-fs, 0.78rem);
  font-weight: 800;
  line-height: 1.05;
  letter-spacing: 0.07em;
  text-transform: uppercase;
  text-align: center;
  white-space: nowrap;
  pointer-events: none;
}
.dchk__stamp small {
  display: block;
  margin-top: 3px;
  font-size: 0.62em;
  font-weight: 700;
  letter-spacing: 0.13em;
  color: rgb(var(--stamp, 22 24 29) / 0.72);
}
/* Thai has no case, so uppercase is a no-op on it, and the wide tracking
   that makes a Latin stamp read pushes the tone marks off their letters.
   Both come back down on the Thai page. */
html[lang="th"] .dchk__stamp { letter-spacing: 0.02em; }
html[lang="th"] .dchk__stamp small { letter-spacing: 0.05em; }

/* The ink, one per document. Written as bare channels so the alphas above
   can vary without restating the colour four times. */
.dchk__item[data-doc="form"]   { --stamp:  31  62 110; }  /* navy on the near-white permit */
.dchk__item[data-doc="intent"] { --stamp: 140  60  14; }  /* burnt orange on pale blue */
.dchk__item[data-doc="cert"]   { --stamp: 146  36  36; }  /* crimson on pale green */
.dchk__item[data-doc="tm"]     { --stamp: 102  76   8; }  /* olive on lavender */
.dchk__item[data-doc="poa"]    { --stamp: 132  24  24; }  /* deep maroon on peach */
.dchk__item[data-doc="id"]     { --stamp: 144  48  14; }  /* rust on cool grey */
.dchk__item[data-doc="pc14"]   { --stamp:  88  48 128; }  /* purple on manila */
.dchk__item[data-doc="prize"]  { --stamp: 130  28  82; }  /* rose on mint */
.dchk__item[data-doc="ads"]    { --stamp: 255 240 228; }  /* light ink on the orange card */
.dchk__item[data-doc="venue"]  { --stamp: 240 169  59; }  /* amber on the dark stock */

/* The two tiles with a dark ground take the same construction inverted.
   A 0.42 border and a 0.74 ink are the right weights for dark ink biting
   into pale paper; light ink on a dark ground at those alphas turns to
   grey mush, so both go up. */
.dchk__item[data-doc="ads"] .dchk__stamp,
.dchk__item[data-doc="venue"] .dchk__stamp {
  border-color: rgb(var(--stamp) / 0.62);
  background: rgb(var(--stamp) / 0.09);
  color: rgb(var(--stamp) / 0.98);
}
.dchk__item[data-doc="ads"] .dchk__stamp small,
.dchk__item[data-doc="venue"] .dchk__stamp small { color: rgb(var(--stamp) / 0.84); }

/* Size follows the sheet. The five tiles the grid gives two rows to have
   the room for a stamp pressed at full size; the short ones would have it
   running off their own edge. A bigger stamp needs a deeper band under the
   copy, so those five raise the floor set on the band above. */
.dchk__item[data-doc="form"],
.dchk__item[data-doc="tm"],
.dchk__item[data-doc="poa"],
.dchk__item[data-doc="pc14"],
.dchk__item[data-doc="venue"] { --stamp-fs: 0.95rem; }
.dchk__item[data-doc="form"]::after,
.dchk__item[data-doc="tm"]::after,
.dchk__item[data-doc="poa"]::after,
.dchk__item[data-doc="pc14"]::after,
.dchk__item[data-doc="venue"]::after { min-height: 60px; }

/* Three tiles move it. The application form is padded wider than the rest;
   the receipt's bottom edge is torn off by a mask that would bite the stamp
   in half; and the artwork tile is the one sheet short enough that a stamp
   in the bottom-left corner lands on the last line of its own paragraph.

   The artwork tile is also two columns wide with its copy capped at 42ch,
   so the right-hand third of it is empty. The stamp goes there and clears
   the words entirely. It swings the other way too, because a stamp pressed
   near the right edge tilts the other way in the hand. */
.dchk__item[data-doc="form"] .dchk__stamp { left: 28px; }
.dchk__item[data-doc="prize"] .dchk__stamp { bottom: 40px; }
/* and because that stamp starts 23px higher than every other one, the band
   under the receipt's copy has to be that much deeper to stay clear of it */
.dchk__item[data-doc="prize"]::after { min-height: 70px; }
.dchk__item[data-doc="ads"] .dchk__stamp {
  left: auto;
  right: 22px;
  transform-origin: right bottom;
  transform: rotate(6deg);
}

/* Filed. The stamp stays legible rather than fading to nothing, because on
   a done sheet it is the only thing left saying which document this was. */
.dchk__item.is-done .dchk__stamp { opacity: 0.5; }

/* The whole sheet is the target. The label used to wrap the title and the
   description only, which on these tiles left the entire lower half of
   every one of the ten looking clickable and doing nothing. The overlay
   sits above the paper and below the stamp; the stamp takes no clicks, and
   the checkbox is lifted over the overlay so it keeps its own hit area. */
.dchk__item label::after {
  content: '';
  position: absolute;
  inset: 0;
  border-radius: 3px;
  z-index: 1;
}
.dchk__item input { z-index: 3; }

.dchk__foot { margin-top: 14px; font-size: 0.74rem; color: var(--faint); }

/* Below the full width the row count is fixed rather than following
   --cols: five sheets across a tablet would be 130px each, which is
   narrower than the words on them. */
@media (max-width: 1100px) {
  /* Three columns of sheets. Nothing else to say: the board reflows and
     the cork takes up whatever the sheets do not. */
  .dchk__list { columns: 3; }
}
@media (max-width: 860px) {
  .dchk__list { columns: 2; }
}
@media (max-width: 640px) {
  .dchk__controls { gap: 14px; }
  .dchk__group { width: 100%; }
  /* One sheet per row, and square on: there is no room to lay them askew
     and a tilted full-width card just looks like a mistake. The pin stays,
     because a pin is what says this is still a board. */
  .dchk__list { columns: 1; padding: 22px 14px 6px; border-width: 9px; }
  .dchk__item { transform: none; }
  .dchk__item:hover { transform: translateY(-4px); }
  .dchk__item[data-doc="form"] b { font-size: 1.25rem; }
}
@media (prefers-reduced-motion: reduce) {
  .dchk__item, .dchk__item:hover { transition: none; }
}

/* ── TALENT ROSTER ────────────────────────────────────────
   A catalogue, not a carousel: a staggered grid of cards, each with a
   fixed index number and its name set against it at the far edge.

   #celebrity sits below the night anchor, so all of this is read against
   a near-black sky. Nothing here may carry a literal colour — the type
   and the rules go through --ink/--muted/--line so the night driver
   reaches them. The one exception is commented where it appears, and it
   is an exception because its own background does not invert. */
.roster { margin: 44px 0 0; }

.roster__bar { display: flex; align-items: baseline; gap: 16px; margin-bottom: 18px; }
.roster__lbl {
  font-size: 0.78rem; letter-spacing: 0.14em; text-transform: uppercase;
  color: var(--muted); margin: 0;
}
.roster__shown {
  margin: 0 0 0 auto; font-size: 0.78rem; letter-spacing: 0.08em;
  text-transform: uppercase; color: var(--muted); white-space: nowrap;
}
.roster__shown b { color: var(--ink); font-weight: 700; font-variant-numeric: tabular-nums; }

/* Filters. The page's own .chip pill, squared off and set in the same
   letter-spaced caps as the catalogue rows so the control belongs to the
   grid rather than sitting on top of it. Two overrides carry over from
   the pill's defaults: the resting border goes to --muted because .chip's
   --line edge measured 1.03:1 against the cream, and the focus ring goes
   to --ink because it is the only value that clears 3:1 at BOTH ends of
   the sunset. */
.roster__filters { margin-bottom: 34px; }
.roster__filters .chip {
  border-radius: 2px; border-color: var(--muted);
  text-transform: uppercase; letter-spacing: 0.1em;
  font-size: 0.68rem; padding: 8px 13px;
}
.roster__filters .chip:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
.roster__filters .chip small { font-weight: 400; opacity: 0.6; margin-left: 6px; letter-spacing: 0; }
/* The selected pill keeps the brand gradient but takes DARK type, and the
   value is deliberately literal rather than var(--ink). Everything else in
   this section must invert with the sky; this must not, because its own
   background does not invert — the gradient is the same coral-to-amber at
   noon and at midnight, so ink that flipped to near-white at night would
   land at 2.78:1 on one end of it and 1.64:1 on the other. Dark type on
   the same gradient measures 6.15:1 and 9.80:1. */
.roster__filters .chip[aria-pressed="true"] {
  background: var(--fire); color: #1a1a2e; border-color: transparent; font-weight: 700;
}
.roster__filters .chip[aria-pressed="true"] small { opacity: 0.62; }

.roster__grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 14px 26px;
  list-style: none; margin: 0; padding: 0;
  align-items: start;
}
/* A filtered or capped card must leave the flow entirely, or it holds its
   grid cell and the catalogue fills with gaps. */
.roster__item[hidden] { display: none; }

/* Every source card is the same 936x598, so the reference's rhythm of
   varied crops is not available to borrow. The per-column offset supplies
   that rhythm instead. The column comes from the script, not from
   nth-child, which would count the cards a filter has hidden and scatter
   the offsets at random. */
.roster__item[data-col="1"] { margin-top: 5.5rem; }
.roster__item[data-col="2"] { margin-top: 1.75rem; }
.roster__item[data-col="3"] { margin-top: 7.5rem; }

/* Size varies by column as well as position. In the reference the rhythm
   comes from the crops themselves — one plate is a tall portrait, the next
   a small square — and with 55 identically framed cards that variation has
   to be introduced rather than inherited. Insetting two of the four columns
   gives it without touching grid flow, so the caption, which lives inside
   the card, stays locked to the image edge it belongs to. */
.roster__item[data-col="1"] .roster__card { width: 88%; }
.roster__item[data-col="2"] .roster__card { width: 78%; margin-left: auto; }

.roster__card { margin: 0; }
.roster__card img { display: block; width: 100%; height: auto; }

/* Number hard left, name hard right, on one baseline — the catalogue row
   the whole treatment hangs on. No rule between it and the card: the
   reference sets this row on open space and a hairline closes it up. */
.roster__cap {
  display: flex; align-items: baseline; justify-content: space-between;
  gap: 12px; margin-top: 11px;
  font-size: 0.68rem; letter-spacing: 0.1em; text-transform: uppercase;
}
.roster__num { color: var(--muted); font-variant-numeric: tabular-nums; flex: 0 0 auto; }
.roster__name { color: var(--ink); font-weight: 600; text-align: right; }

.roster__more { margin: 56px 0 0; text-align: center; }
.roster__viewall {
  font-family: inherit; font-size: 0.72rem; font-weight: 600;
  letter-spacing: 0.16em; text-transform: uppercase; cursor: pointer;
  padding: 15px 44px; border-radius: 2px;
  background: transparent; color: var(--ink);
  border: 1px solid var(--muted);
  transition: border-color 0.2s, background 0.2s;
}
.roster__viewall:hover { border-color: var(--ink); }
.roster__viewall:focus-visible { outline: 3px solid var(--ink); outline-offset: 3px; }
.roster__more[hidden] { display: none; }

/* The section heading restated as a graphic rule, the way the reference
   lays its own name across the page between runs of work. */
.roster__band {
  display: flex; justify-content: space-between; align-items: baseline;
  margin: 72px 0 0; padding: 0;
  font-weight: 800; text-transform: uppercase;
  font-size: clamp(1.75rem, 7vw, 5.5rem);
  line-height: 1; letter-spacing: -0.03em;
  color: var(--ink);
}

/* Announcement for screen readers only; the visible readout carries the
   same information for everyone else. */
.roster__status {
  position: absolute; width: 1px; height: 1px; margin: -1px;
  padding: 0; border: 0; overflow: hidden; white-space: nowrap;
  clip: rect(0 0 0 0); clip-path: inset(50%);
}

@media (max-width: 1180px) {
  .roster__grid { grid-template-columns: repeat(3, minmax(0, 1fr)); }
}
@media (max-width: 860px) {
  .roster__grid { grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px 18px; }
  .roster__item[data-col="1"] { margin-top: 3rem; }
  .roster__item[data-col="2"] { margin-top: 1rem; }
  .roster__item[data-col="3"] { margin-top: 4rem; }
  .roster__lbl { font-size: 0.7rem; }
  .roster__shown { font-size: 0.7rem; }
  /* Eight pills wrap to four lines and 158px of chrome on a phone, which
     pushes the catalogue itself off the screen. One scrolling line costs
     38px. */
  .roster__filters {
    flex-wrap: nowrap; overflow-x: auto;
    scrollbar-width: none; -webkit-overflow-scrolling: touch;
    padding-bottom: 2px;
    /* With the scrollbar hidden nothing said the row went on past the
       edge; the last visible pill was simply cut. A fade at the right
       edge is the tell that there is more to drag into view. */
    -webkit-mask-image: linear-gradient(to right, #000 calc(100% - 36px), transparent);
    mask-image: linear-gradient(to right, #000 calc(100% - 36px), transparent);
  }
  .roster__filters::-webkit-scrollbar { display: none; }
  .roster__filters .chip { flex: 0 0 auto; }
}
@media (max-width: 520px) {
  .roster__grid { grid-template-columns: minmax(0, 1fr); gap: 26px; }
  .roster__cap { font-size: 0.64rem; }
}

/* The checklists run full width now that the placeholder column is gone. */
.split--solo { grid-template-columns: minmax(0, 1fr); }

/* ── THE TALENT SECTION TURNS THE PAGE WHITE ──────────────
   The catalogue is printed matter, so the page becomes paper for the
   length of it and then goes back to the night sky.

   This is done by REDEFINING the ink tokens on the section rather than by
   writing colours onto its parts. The night driver sets --ink and the rest
   on documentElement; a definition here wins for this subtree because a
   custom property resolves at the nearest ancestor that declares it. So
   every rule in the section keeps reading var(--ink) exactly as before and
   simply gets a different answer, and nothing inside needs to know that
   the ground changed. It also means the section stays correct with the
   script disabled: no class is toggled and no observer has to fire.

   The values are fixed rather than token-derived, because the whole point
   is that this block does NOT follow the sunset. */
#celebrity {
  position: relative;
  /* Was a hard #FFFFFF. The token is what the pick band switches, and it
     still resolves to white in the mode the page opens in, so the
     untouched state is byte-for-byte the section that shipped before. */
  background: var(--pick-ground, #FFFFFF);
  --ink: #0B0B0C;
  /* The tiers sit darker here than they do on the sunset, for two reasons.
     They have to: against white, 0.58/0.46 measure 4.84:1 and 3.24:1, and
     .faq-group is 13px so the second one is a plain AA failure. And they
     should: the reference sets its captions in near-black, not in grey. */
  --muted: rgba(11, 11, 12, 0.66);
  /* 0.62, not the 0.58 this had against white. The ground under both of
     these blocks is a tint now, and at 0.58 the small-caps tier measured
     4.35:1 on the violet, which is a plain AA failure. 0.62 clears both:
     5.21 on the amber, 4.93 on the violet. */
  --faint: rgba(11, 11, 12, 0.62);
  --line: rgba(11, 11, 12, 0.13);
  /* The brand coral is 2.78:1 on white. This is the nearest step down its
     own hue that clears AA at the 14.7px the checklist tick is set in.
     Scoped to this section, so the coral is untouched everywhere else. */
  /* Stepped down again, for the same reason as the tier above. The old
     value was solved against white and measures 4.06:1 on the amber and
     3.57:1 on the violet once the ground is tinted. This is the same hue
     taken to 82%, which clears both: 5.5 and 4.84. */
  --red: #A53039;
  --card: #FFFFFF;
  --bg2: #F4F4F5;
  color: var(--ink);
  padding-top: 104px;
  padding-bottom: 104px;
}

/* A neutral grotesque for the length of the section, tracked tighter than
   the page's own face. Helvetica carries no Thai, so Sarabun stays in the
   chain behind it — Thai glyphs fall through to it and the Latin around
   them still picks up the grotesque. */
#celebrity,
#celebrity .sec-head h2,
#celebrity .roster {
  font-family: 'Helvetica Neue', Helvetica, Arial, 'Sarabun', sans-serif;
}
#celebrity .sec-head h2 {
  letter-spacing: -0.033em;
  font-weight: 700;
}
#celebrity .sec-head .eyebrow {
  letter-spacing: 0.16em;
}
#celebrity .sec-head p {
  letter-spacing: -0.004em;
}

/* The star field is painted by .night-sky::after at z-index -1 and the
   white ground already covers it. The nav is the one thing that does not
   follow automatically: it samples the backdrop itself, and that sampling
   is corrected in lucky-draw-clouds.js so an opaque block outranks the sky. */
