/* ═══════════════════════════════════════════════════════════════════════
   DreamPlay Concise — live presentation stage.
   Target device: MacBook 14 inch, PRESENTED FULLSCREEN: the stage is
   1512x982. In a browser window with chrome it is ~1512x860; everything
   here is derived from the live viewport, so both are correct.

   ONE HEADING SIZE
   The deck used to run two competing title sizes: .stage-title at 40px and
   .stage-sub at 26px. Adjacent scenes jumped between them, which reads as
   inconsistency rather than as hierarchy. There is now exactly one heading
   size — 40px, weight 600 — and it belongs to the scene that OPENS a
   chapter. A scene that continues a chapter carries the 12px kicker and,
   ONE heading size. Every scene titles itself at 40px, the chapter-title
   size, whether the line is a statement or just a name. The 18px
   .stage-label role is retired: two heading sizes read as two hierarchies
   from across a room, and there is only one here.

   THE CENTRING MODEL
   NOT a grid, despite what this comment used to claim. Every .scene computes
   display:block, and its content is placed into ONE absolutely positioned
   well running from --well-top to --well-bottom, 1384px wide.

   A three row template and a --band ladder (92 kicker only, 169 kicker plus
   title, 172 cover) were documented here and declared on 16 scenes. Both
   grid-template-rows rules that consumed --band targeted block elements, so
   neither ever applied: every band edit changed nothing, and the deck's real
   vertical rhythm came from three --well-top overrides instead. The dead
   mechanism has been removed rather than left to mislead the next edit.

   Vertical position is therefore set by --well-top per archetype, and by
   justify-content on the well's own container. It is NOT ridden on transform,
   which .is-in .rv would reset.

   --gutter is the minimum clearance to the bottom edge: 40px everywhere.

   CONTENT IMAGES vs BACKGROUND IMAGES
   A content image (.plate board, .frame-phone, showcase strip) carries its
   own information and often its own type. No deck text may cross one.
   A background image (.bleed cover art, .film__bg blurred backdrop) is a
   ground, and text over it is expected. Only those two carry a scrim.

   MEDIA IS HEIGHT-BOUND, NOT WIDTH-BOUND
   A board's --pw is derived from its own aspect ratio against the height
   left under its own band, so the picture plus its caption always clears
   the floor. Nothing in the deck is sized from width alone any more.
   ═══════════════════════════════════════════════════════════════════ */

:root{
  --ground-1:#000000;
  --ink-1:rgba(245,243,247,.92); --ink-2:rgba(245,243,247,.72); --ink-3:rgba(245,243,247,.58);
  /* Captions under artwork sit quieter than body ink and read regular. */
  --ink-cap:rgba(245,243,247,.60);
  /* Retained: the gate and the closing slide reference these by name. The
     gate is the same ground the deck opens onto, so it tracks --ground-1. */
  --dark-ground:#000000;
  --dark-ink-1:#F5F3F7; --dark-ink-2:rgba(245,243,247,.72); --dark-ink-3:rgba(245,243,247,.58);
  --indigo:#6A00F4; --chartreuse:#ADFF2F; --payne:#34455A;
  --indigo-on-dark:#9B5CFF;    /* 5.37:1 here, where --indigo is 2.92:1 */
  --pumpkin:#C2410C;           /* a GROUND: white on it is 5.18:1 */
  --signal:var(--chartreuse);  /* an INK: 17.12:1 on black */

  --font-display:'Schibsted Grotesk',system-ui,sans-serif;

  /* ── Type. Every floor is >= 12px, so no viewport can push a line under
     the deck's own minimum. Ceilings hold the ratios the deck was drawn at. */
  --stage-title:clamp(26px,2.6svh + 0.9vw,40px);   /* the deck's ONLY heading size */
  --stage-kicker:clamp(12px,0.95vw,14px);
  --mediacap-title:18px;
  --mediacap:clamp(12px,1.05vw,15px);
  --statement:clamp(15px,1.32vw,19px);

  /* ── The chrome band ────────────────────────────────────────────────
     Composed, not guessed. #counter and #mute are pinned to a definite
     30px box, so the band they need is arithmetic rather than a constant
     that happened to be right at one window size. Content can never enter
     it, because it is the scene's own padding-bottom. */
  --chrome-box:30px;                   /* 12px/1 type plus 8px padding, twice */
  --chrome-inset:var(--s5);            /* the chip's own offset from the edge */
  --chrome-air:18px;                   /* clearance content must never eat;
                                          30 + 24 + 18 lands the floor on 72 */
  --foot-safe:calc(var(--chrome-box) + var(--chrome-inset)
                 + var(--chrome-air) + env(safe-area-inset-bottom,0px));

  --safe-x:clamp(32px,4.2vw,64px);
  /* The floor is NOT free. #deckNav is 56px tall and overlays the stage, so
     anything starting above 56px is under the bar whenever the presenter
     moves the pointer. The old fixed 56px sat exactly flush with its bottom
     edge, touching; my clamp made it worse by letting the header ride up to
     28px on a short screen, where the nav covered the eyebrow outright.
     56 plus 8 measured as clearance but READ as touching: the bar has a
     bottom rule, and an eyebrow sitting 8px under a rule looks attached to
     it. This is not only nav clearance though: it is the deck's top
     margin, and at 88px on a 900px slide the title still sat high enough
     to read as pinned to the edge rather than set into the page. 112px is
     about an eighth of the stage, which is where an editorial top margin
     sits, and it leaves 56px clear of the nav bar. */
  --title-top:calc(clamp(112px,12svh,144px) - 32px);
  --band-air:clamp(28px,6.4svh,72px);  /* header to content: was baked into 208 */
  --foot-clear:var(--foot-safe);       /* .ch-card and the gate read this      */
  --gutter:40px;               /* minimum clearance at the floor */
  --plate-x:64px;
  /* A title that fits on one line must BE on one line. The longest title in
     the deck, "A low-cost way into the Dream Sports ecosystem.", measures
     915px at 40/600, so the measure is set just above it. */
  --title-measure:min(944px,100%);


  /* Rhythm ladder. Proximity has to encode grouping, so each step out is at
     least 1.4x the step inside it and the group step is more than 2x. The
     ratios are unchanged; only the ceiling is, so a short viewport tightens
     the spacing instead of clipping the slide. */
  --gap-in:clamp(10px,1.4svh,16px);      /* a label to its own body            */
  --gap-set:clamp(16px,2.6svh,28px);     /* sibling to sibling inside one set  */
  --gap-group:clamp(32px,6svh,64px);     /* one group to a different group     */
  --gap-col:clamp(28px,3.4vw,52px);      /* column gutter in a .split          */
  --gap-media:16px;                      /* a picture to its own caption       */

  --display:clamp(36px,4.4vw,64px);   /* statement scenes only              */
  --lead:clamp(22px,2.1vw,30px);
  --pair:clamp(19px,1.8vw,26px);
  --lead-body:clamp(16px,1.5vw,22px);

  --s2:8px; --s3:12px; --s4:16px; --s5:24px; --s6:32px; --s7:48px; --s8:64px;
  --r-capsule:11px; --r-tile:18px; --r-phone:20px;
  --r-card:22px;               /* the reference card radius; --r-tile stays 18 for .plate--edge */
  --card-fill:linear-gradient(180deg,#2E2240 0%,#261D35 100%);
  --border-1:rgba(245,243,247,.14);
  /* The one edge treatment for every non-phone media frame. Phone bezels are
     a separate, deliberate object and do not read these. */
  --frame-edge:var(--border-1);
  --frame-radius:var(--r-tile);

  --ease-standard:cubic-bezier(.4,0,.2,1);
  --ease-in-out:cubic-bezier(.42,0,.58,1);
  --ease-decel:cubic-bezier(0,0,.2,1);
  --ease-land:cubic-bezier(.16,1,.3,1);   /* position settles, no overshoot */
}

*,*::before,*::after{box-sizing:border-box}
html,body{margin:0;height:100%;background:var(--ground-1);overscroll-behavior:none;
  scrollbar-width:none;-ms-overflow-style:none}
html::-webkit-scrollbar,body::-webkit-scrollbar{width:0;height:0;display:none}
body{font-family:var(--font-display);color:var(--ink-1);-webkit-font-smoothing:antialiased}
#stage{-webkit-user-select:none;user-select:none}

/* ── The scene shell ───────────────────────────────────────────────────
   Two rows: the header at its own measured height, and the content taking
   everything that is left. The header is IN FLOW, which is the whole point:
   the old model put content at a constant --well-top and then hand-patched
   three scenes whose headers were shorter. A row that starts under whatever
   the header actually measures needs no patches.

   padding-bottom is --foot-safe, so the chrome band is structural. A scene
   physically cannot lay content under #counter and #mute, and no future
   author has to remember that it must not. */
.scene{position:relative;width:100vw;height:100dvh;overflow:hidden;
  display:grid;grid-template-columns:minmax(0,1fr);
  grid-template-rows:auto minmax(0,1fr);row-gap:var(--band-air);
  padding:var(--title-top) var(--safe-x) var(--foot-safe);
  background:var(--ground-1);--fit:1}

.scene > .anchor--tl{position:static;grid-row:1;grid-column:1;
  max-width:var(--title-measure);text-align:left}

/* Every well child occupies row 2 and is bound by it. min-height:0 is the
   load-bearing declaration: without it a grid item's automatic minimum size
   is its content, and the row grows straight past the viewport again. */
.scene > .plate,.scene > .ranked,.scene > .board,.scene > .spec,
.scene > .split,.scene > .frame-row,.scene > .cover-brief,.scene > .flow,
.scene > .stack,.scene > .statement,.scene > .cards{
  position:static;grid-row:2;grid-column:1;
  width:100%;height:100%;min-height:0;min-width:0;
  margin:0;padding-left:0;padding-right:0}

/* Decoration is absolutely positioned, so it is out of flow and needs no
   grid placement. It must NOT get one: giving an absolutely positioned
   child a definite grid area re-bases its containing block onto that area,
   which sits INSIDE the scene's padding. That is what stopped the cover
   art and the blurred film backdrop reaching the edges of the screen. The
   file's own header warned about exactly this trap and I walked into it. */

/* Spacing tightens under the fit pass; type never does. See fitScene() in
   app.js: it may trim the gaps and the media, and it may not touch a
   clamp()-floored font size, so a slide can be made to fit but can never be
   made less legible. */
.scene > *{
  --gap-in:calc(clamp(10px,1.4svh,16px) * var(--fit));
  --gap-set:calc(clamp(16px,2.6svh,28px) * var(--fit));
  --gap-group:calc(clamp(32px,6svh,64px) * var(--fit))}

/* Vertical centring, declared per container so it never rides on transform.
   .frame-row stays top-aligned on purpose: its cells carry captions of
   different depths, and centring each cell would step the phone tops out of
   line for the sake of ~30px. */
.scene > .plate,.scene > .stack,.scene > .ranked,.scene > .flow,
.scene > .spec,.scene > .statement{justify-content:flex-start}
.scene > .cover-brief{justify-content:center}
.scene > .split{align-content:start}
/* Two deliberate exceptions to the top line, both asked for by name.
   The opportunity's picture drops to the foot of the text beside it, and the
   fairplay scene centres its pair of columns. */
.plate--bottom{align-self:end}
.scene--vcentre > .split{align-content:center}
/* Text wells distribute: lead at the top, the demoted rank on the floor, so
   the space sits BETWEEN the ranks where it reads as hierarchy. */
.scene > .frame-row{align-items:stretch}
.scene > .split{align-items:start}
/* Kept dark on purpose: the cover, whose artwork is dark, and slide 2. */
/* Pinned top. These scenes carry frames and rails that should all begin at
   the same height down the page, so row one is a fixed track rather than a
   drifting with its own height. 212px is where slide 17 sat, and that is the
   reference the deck now holds. */

/* .scene--dark used to remap the ink tokens for the two scenes that stayed
   dark while the rest of the deck was light. The base is dark now, so every
   one of those rules restated what it already inherits. The class stays on
   the markup, and on the cover it still pairs with .scene--cover, but it no
   longer needs to declare anything. */
/* The closing statement has no title band at all, so it centres on the whole
   screen. The cover is the opposite case: its lede and figures are the
   continuation of the title stack and must hang off the title, not float. */
/* The close used to be a flat #6A00F4 field. After thirty four near-black
   slides a fully saturated purple arrives as a jolt, which is the opposite
   of what a last beat wants: the deck should dim, not shout. It keeps the
   deck's own ground and takes a single indigo bloom behind the line, so the
   colour is still the brand's but it reads as light in the room rather than
   a change of world. The bloom sits low and slightly above centre, under
   the text, and falls off to pure black well before the edges. */
.scene--payne{
  background:
    radial-gradient(112% 78% at 50% 44%,
      rgba(106,0,244,.40) 0%,
      rgba(106,0,244,.18) 34%,
      rgba(76,0,175,.06) 58%,
      rgba(0,0,0,0) 78%),
    var(--ground-1);
  display:flex;align-items:center;justify-content:center;
  padding:var(--title-top) var(--safe-x) var(--foot-safe)}
.scene--payne .closer{color:#FFFFFF}

.scene--cover > .cover-brief{grid-row:2;align-self:start}

/* ── The one title position ─────────────────────────────────────────── */
.anchor--tl{position:absolute;z-index:5;top:var(--title-top);left:var(--safe-x);
  max-width:var(--title-measure);text-align:left}
.anchor--center{position:relative;text-align:center;max-width:min(1400px,100%);margin:0 auto;
  padding:0 var(--safe-x)}

.kicker{font-size:var(--stage-kicker);font-weight:600;letter-spacing:.12em;line-height:1.2;
  text-transform:uppercase;color:var(--signal,var(--ink-3));margin:0 0 var(--s3)}
.stage-title{font-size:var(--stage-title);font-weight:600;letter-spacing:-.02em;
  line-height:1.15;margin:0}
/* The closing statement is a statement, not a scene title, but it is still
   the deck's one heading size. */
.closer{font-size:var(--stage-title);font-weight:600;letter-spacing:-.02em;
  line-height:1.15;margin:0}
.stage-cap{font-size:var(--statement);line-height:1.5;color:var(--ink-2);margin:0}
/* A caption sitting under the title needs the label-to-body step; both
   elements zero their own margins, so they were touching. */
.anchor--tl .stage-cap{margin-top:var(--gap-in)}
.stage-cap b{font-weight:600;color:var(--ink-1)}
/* Chartreuse measures 14.66:1 on the card fill and indigo only 2.50:1, so on
   this ground the accent is chartreuse. .board dt sets a colour of its own and
   at (0,1,1) outranks a bare .tint-c at (0,1,0), so the accent is matched on
   the element to win where it is applied. */
.tint-c{color:var(--chartreuse)}
.board dt.tint-c{color:var(--chartreuse)}

/* ── Full-bleed still. Cover art only; every other board is a .plate. ── */
.bleed{position:absolute;inset:0;width:100%;height:100%;object-fit:cover}

/* ── Board plate: picture and caption as one block, so the caption wraps
      inside the picture's own left and right edges. --pw is set per scene
      from the asset's real aspect ratio against the height available under
      that scene's own band, so no board can push its caption into the
      chrome. --mh is the same number expressed as a hard ceiling. ────── */
/* The canonical media fit. aspect-ratio plus height:100% plus width:auto
   plus max-width:100% is an exact two-axis fit: when the width cap bites,
   the ratio pulls the height down with it, so the picture is always as
   large as the box allows and is never cropped or letterboxed.
   The caption is a flex sibling, so it takes its own height out of the
   picture's automatically at every viewport. --cap, --mh, --pw and --wellh
   no longer have to price it in advance. */
.plate{position:relative;z-index:2;margin:0;display:flex;flex-direction:column;
  align-items:flex-start;justify-content:flex-start;
  width:fit-content;max-width:100%;height:100%;min-height:0}
.plate img{display:block;flex:1 1 auto;min-height:0;
  height:100%;width:auto;aspect-ratio:var(--ar,auto);
  max-width:100%;max-height:100%;object-fit:contain;object-position:left center}

/* A board exported from Figma is itself white. On a white slide it therefore
   has no visible edge, and the caption underneath reads as floating rather
   than as belonging to the picture. The hairline is the portfolio's own
   .cs-flat treatment (border-1 at the tile radius), which gives the picture
   an edge for the caption to line up against. The cover-adjacent opportunity
   board is excluded: its artwork already ends on a dark ground. */
/* NO treatment. A non-phone board is shown as it was exported: no border,
   no ring, no mount, no background of ours behind it. The boards already
   carry their own edge and their own ground, and anything added here is a
   second frame drawn around a picture that already has one. Phone bezels
   are a separate, deliberate object and keep theirs. */
.plate img,.flow-plate,.stack .plate img{
  border:0;border-radius:0;background:none;box-shadow:none;filter:none}

/* ── A board exported on white ─────────────────────────────────────────
   These four used to carry an 8px Payne's Gray ring plus a further 12px
   near-black ring: a literal 20px double border, and a box-shadow inside an
   overflow:hidden scene. That mount was solving a problem the ground does
   not have. A white board on black already has all the edge definition it
   will ever need, so these take the same single hairline as every other
   board and nothing else. */
.plate--light img{box-shadow:none;filter:none}

/* ── The statement scene ───────────────────────────────────────────────
   The deck ran ten type sizes inside a 3.3x range, all in one of three
   greys, so nothing could be a moment. This is the top end. Four scenes
   carry it and nothing else does: at 72px against a deck that otherwise
   stops at 40, the room knows without being told which four are the
   argument and which are the evidence. The 64x4 chartreuse rule is not new
   furniture, it is the mark .cover-shipped-head::before already draws,
   at presentation scale. */
.statement{position:relative;z-index:2;display:flex;flex-direction:column;
  align-items:flex-start;gap:var(--gap-group)}
.statement-lead{font-size:var(--display);font-weight:700;letter-spacing:-.03em;
  line-height:1.03;color:var(--ink-1);margin:0 0 var(--s5);max-width:22ch}
.split .statement-lead{font-size:clamp(24px,2.5vw,40px);max-width:none}
.split .statement-sub{margin:0 0 var(--gap-group)}
.statement-lead::before{content:"";display:block;width:64px;height:4px;
  border-radius:2px;background:var(--chartreuse);margin:0 0 var(--s5)}
.statement-lead em{font-style:normal;color:inherit}
.statement-sub{font-size:var(--lead-body);line-height:1.5;color:var(--ink-2);
  margin:0;max-width:56ch}
.statement-q{font-size:24px;font-weight:600;line-height:1.35;
  color:var(--signal,var(--chartreuse));margin:0 0 var(--s6);max-width:46ch}
.split .rank-rest{border-top:0;padding-top:0;max-width:none}
.scene--statement{--signal:var(--chartreuse)}
.scene--statement > .statement{justify-content:flex-start}

/* The closer is the last thing the room sees, so it is the largest thing in
   the deck. White on it is 7.19:1; chartreuse on it is 5.87:1. */
.scene--payne .closer{font-size:clamp(32px,3.6vw,56px);line-height:1.18;letter-spacing:-.025em;
  /* The scene is overflow:hidden and the last line's descender rounds past
     the line box, so a couple of pixels of the final 'y' were being shaved.
     A hair of padding absorbs it without moving the optical centre. */
  padding-bottom:6px}
.scene--payne .closer em{font-style:normal;color:var(--chartreuse)}

/* ── The chapter card ──────────────────────────────────────────────────
   Nine of these, one at each chapter turn. A deck of 27 dark scenes gives a
   room no way to feel the narrative move; a full-bleed chartreuse ground at
   128px does it in the two seconds it is on screen, without a word of
   explanation. Black on #ADFF2F is 17.12:1, so the boldest thing in the deck
   is also the most legible. It is the same chartreuse already used for the
   statement rule and the closer's number, so the deck gains a chapter signal
   rather than a new colour. */
.scene--chapter{background:var(--pumpkin);color:#fff}
.ch-card{position:absolute;inset:var(--title-top) var(--safe-x) var(--foot-safe);
  display:flex;flex-direction:column;justify-content:center;align-items:flex-start}
.ch-next{color:#fff;margin:0}
/* The chrome is near-white by default and would vanish on chartreuse. */
/* .86 white on pumpkin measured 4.21:1 at 12px. Solid white is 5.18:1. */
.on-chapter #counter,.on-chapter #mute{color:#FFFFFF;text-shadow:none}

/* The chapter progress hairline is GONE, and deliberately so. It was 3px of
   chartreuse pinned to the literal bottom edge, which is the first row a
   projector crops and which reads to a room as a loading bar. It also spent
   the deck's one accent on furniture while answering, vaguely, a question
   #counter already answers precisely: 07 / 35. Same call this file already
   made when it deleted #rail. */

/* The four flow boards size against the LIVE viewport rather than a number
   baked in at a 860px window. The deck is presented fullscreen, where a
   MacBook 14 gives 982px of stage, and a fixed max-height left roughly 120px
   of that unused under every one of them. --ar is the asset's own aspect, so
   the width still follows the picture and the caption still wraps inside the
   picture's edges. --cap is the caption's own cost: 14px of margin above a
   15/1.55 line. */
/* --cap, --mh and --pw are retired. They existed to price a caption's height
   into a picture's ceiling in advance, against a well whose height was a
   constant. The caption is a flex sibling of the picture now, so it takes its
   own height out of the picture's at every viewport without being predicted.
   The declarations are neutralised rather than hunted through the markup. */
.plate--fill{--cap:initial;--mh:initial;--pw:initial}

/* A full-width board is centred by the scene grid, which leaves its left edge
   a few px inboard of the title above it. This pins it to the same 64px the
   title, the cards and every rail already start from. */
.plate--left{justify-self:start;margin-left:0}

/* ── A board that sits BESIDE its rail ─────────────────────────────────
   .plate--fill sizes a board from the height left under its own band. A
   board in the right hand track of a .split is the other case: the rail
   and the gap have already taken their width, so the TRACK binds and the
   height follows the asset's own aspect. The first term is 100% of the
   track itself, never a number derived from 100vw, so it stays correct
   whatever --col and --split-gap become. The second term only bites if an
   aspect is ever tall enough to push the caption past the floor, in which
   case this degrades into .plate--fill's behaviour rather than
   overflowing. */
.plate--rail{--cap:initial;--mh:initial;max-width:100%}

/* The identity spec as the right hand column. It carries its own 1160 cap
   and its own 64px padding for the full width case; inside a track both
   jobs are already done. */
.split > .spec{max-width:none;padding:0;justify-self:stretch}

/* ── The happy flow ───────────────────────────────────────────────────
   The board is Figma node 346:132871 exported at 2.5x and cropped to its
   content group, with two things painted out in the frame's own #000: the
   three rects that carry live video, and the nine connector labels. The
   arrows, their dots and the five static screens are therefore the design
   itself, pixel for pixel. Everything sits at the rects Figma reports,
   expressed as percentages of the 1611.33 x 812.76 board, so the whole
   thing scales as one piece.

   The clips run muted. A flow board is read, not listened to, and each of
   these recordings also appears full size with its sound elsewhere. */
.flow{position:relative;z-index:2;margin:0;display:flex;flex-direction:column;
  align-items:flex-start;height:100%;min-height:0}
/* The board is the Figma export, used whole. The screens, the dashed
   connectors and every label are already in it, so nothing is redrawn here
   and nothing can drift out of register. The only thing layered on top is
   three clips, each pinned to the rect Figma reports for the screen it
   replaces, as a percentage of the 1655 x 860 crop. No device chrome: the
   screens in the export carry their own, and adding rings on top of three of
   eight would have made those three read as a different kind of object.

   Re-exported from frame 349:124921. Each clip is pinned to the box Figma
   reports for the screen it replaces, taken against the export's own render
   bounds rather than measured off the pixels. The bezel is stroked OUTSIDE
   in the source, so that box is already the inner rect and the export's
   stroke survives; the radius is the frame's own 24px, which is 1.338% of
   the 1794px board. Re-derive all four numbers if it is exported again. */
.flow-board{position:relative;flex:1 1 auto;min-height:0;
  height:100%;width:auto;max-width:100%;aspect-ratio:5383 / 2743;
  container-type:inline-size}
.flow-plate{display:block;width:100%;height:100%;object-fit:fill}
.flow-vid{position:absolute;display:block;object-fit:fill;border-radius:1.338cqi;
  background:#0a0a0c;pointer-events:none}

/* The connector labels are markup, not pixels. They sit outside the exported
   group in the source, so the export has none of them; as live text they also
   stay sharp at any size. Each --l/--r/--t is that label's box in the source,
   as a share of the board, so they track the arrows when it scales.

   The size is proportional and deliberately has no floor. A floor was tried
   and it collides: the board is 1794 wide in the source, so 16px type is
   .8918cqi, and pinning the type at 14px while the board keeps shrinking
   oversizes it by 1.2x at a 1308px board and 1.46x at 1075px. The labels are
   anchored left or right to follow their arrow, so that excess grows straight
   into the frames, which is what the overlap was. Proportional means the
   geometry matches the source at every width and cannot collide. The board is
   1596 wide on a 1920 screen, which puts these at 14.2px where it is presented;
   on a smaller laptop preview they scale down with everything around them. */
.flow-lab{position:absolute;left:var(--l);top:var(--t);
  font-size:.8918cqi;line-height:1.25;color:#BABABA;
  white-space:nowrap;pointer-events:none}
.flow-lab--r{left:auto;right:var(--r);text-align:right}


/* ── Phone frame: the portfolio's device treatment, unchanged. ──────── */
.frame-phone{border:3px solid #221D2E;border-radius:var(--r-phone);background:#221D2E;
  overflow:hidden;display:block;
  box-shadow:0 0 0 1px #5a5862, 0 18px 44px rgba(122,74,255,.14)}
/* A phone's height is the row it sits in, not a constant. --row-h:600px and
   --hero-h:700px were the other half of the clipping: at 800px of viewport a
   600px frame plus a title plus a caption does not fit, and at 1080px it was
   100px smaller than it could have been. box-sizing is border-box globally,
   so the 7px bezel is inside the flex height and the media at height:100%
   fills the content box exactly. */
.frame-phone--row,.frame-phone--hero{height:100%;width:auto;min-height:0}
.frame-phone--hero{position:relative;z-index:2}
/* One hero phone on an otherwise empty slide: the caption moves beside the
   device instead of under it, using width the slide is not otherwise using. */
/* The second column exists to hold a caption beside the device. The Motion
   scene's caption moved up under the title, so that column stayed reserved
   and empty and pushed the phone 166px left of centre. Reserve it only when
   there is something to put in it. */
.scene--hero .frame-cell{display:grid;grid-template-columns:auto;
  grid-template-rows:1fr auto auto;column-gap:clamp(24px,3vw,44px);align-items:end}
.scene--hero .frame-cell:has(.frame-cap-title){
  grid-template-columns:auto clamp(200px,20vw,292px)}
.scene--hero > .frame-row{justify-content:center}
.scene--hero .frame-cell > .frame-phone{grid-column:1;grid-row:1/-1}
.scene--hero .frame-cap-title{grid-column:2;grid-row:2;width:auto;min-width:0;margin:0}
.frame-phone > img,.frame-phone > video{height:100%;width:auto;object-fit:cover;display:block}
.frame-phone video::-webkit-media-controls-overlay-play-button,
.frame-phone video::-webkit-media-controls-start-playback-button{display:none!important}

/* Phone rows keep ONE device height across the whole deck. That is the one
   place a shared value is right: a handset that changes size from scene to
   scene reads as drift. Only the gap flexes. 540px is the largest height at
   which the tallest cell in the deck — frame + title + a three line caption
   — still clears the floor by 40px under the deepest band, at 860. */
/* .scene--pin is GONE, class and rule. It existed to reserve a second
   title line so devices would not step between neighbouring slides. No
   title in the deck wraps: measured all 23 at 1728, 1440, 1280 and 1100,
   every one sets on a single line. So the reserve bought nothing and cost
   a 42px step between pinned and ordinary slides, which is the same jump
   it was written to prevent, pointing the other way. Every titled scene
   now starts its content band at one height. If a future title does wrap,
   shorten the title: this deck has one heading size for a reason. */

.frame-row{position:relative;z-index:2;display:flex;align-items:stretch;
  justify-content:center;gap:var(--frame-gap,clamp(20px,2.4vw,44px));
  height:100%;min-height:0}
/* A row of two devices has width to spare that a row of three does not, and
   at the three-up gap the pair reads as one object. Only rows with no third
   cell widen. */
/* Two frames get a wider gap than three. This counts .frame-cell siblings
   rather than child position: the focus markers are .frame-row's first
   children, so a :nth-child(3) test saw the first cell of a two-frame row as
   the third child and the logic inverted, taking the two-frame rows down to
   44px and pushing the three-frame rows up to 104px. */
.frame-row:not(:has(.frame-cell ~ .frame-cell ~ .frame-cell)){
  gap:var(--frame-gap-2,clamp(44px,5.4vw,104px))}
/* The cell is exactly as wide as the frame. --cap-reserve holds the caption
   band at one depth across the row, so the phone bottoms stay in line even
   when one caption wraps to two lines and its neighbour does not. */
.frame-cell{display:grid;
  grid-template-rows:minmax(0,1fr) minmax(var(--capt-reserve,26px),auto);
  height:100%;min-height:0;width:fit-content;justify-items:start}
.frame-cell > .frame-phone{grid-row:1;align-self:end;height:100%;min-height:0;flex:none}
.frame-cap-title{grid-row:2;align-self:start;color:var(--ink-cap);text-align:left;
  font-size:var(--mediacap-title);font-weight:400;line-height:1.3;
  margin:var(--gap-media) 0 0;width:0;min-width:100%}

.film__bg{position:absolute;inset:-60px;width:calc(100% + 120px);height:calc(100% + 120px);
  object-fit:cover;filter:blur(64px) saturate(140%) brightness(.45);transform:scale(1.35)}
.reduced-transparency .film__bg{display:none}

/* ── Left rail of high level points beside the media ──────────────────
   Both columns start at the top of row two, so the points head and the top
   edge of the media share one baseline. --col and --split-gap are per scene:
   a 560px product shot and a seven tile grid do not want the same rail. */
.split{position:relative;z-index:2;display:grid;
  grid-template-columns:var(--col,340px) 1fr;gap:var(--split-gap,var(--gap-col));
  align-items:start;justify-items:center;
  width:100%;padding:0 var(--safe-x)}
.split > div{justify-self:stretch}

/* ── A board that leads, with its points beneath ───────────────────────
   Three of the identity scenes ran a 320px rail beside a landscape board,
   which capped the picture at 1008px and left it unable to reach even half
   the well. On scenes whose whole job is to show the artwork, the board
   takes the full 1384 and the points read as a row under it. --stack-foot
   is what the row and its gap cost, so the board can still never push the
   points past the well floor. Nothing is cropped: the image stays width and
   height bound on its own aspect. */
.stack{position:relative;z-index:2;display:flex;flex-direction:column;
  align-items:stretch;height:100%;min-height:0;gap:var(--gap-group)}
/* --stack-foot is retired with the rest of the reserved-height arithmetic.
   The points row is flex:none and takes its own height; the board takes what
   is left and can no longer push anything past the floor. */
.stack > .plate{flex:1 1 auto;min-height:0;width:100%;max-width:100%;
  align-items:flex-start}
.stack .points{flex:none}
.stack .points{flex-direction:row;gap:var(--gap-group);align-items:stretch}
.stack .points li{flex:1 1 0;font-size:18px;line-height:1.5}
.stack .points li::before{top:calc(.75em - 5.5px)}
.points{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:var(--gap-set)}
.points li{font-size:var(--statement);line-height:1.55;color:var(--ink-2);
  position:relative;padding-left:30px}
.points li::before{content:"";position:absolute;left:0;top:calc(.775em - 5.5px);
  width:9px;height:9px;border-radius:2px;background:var(--signal,var(--chartreuse))}
.points b{font-weight:600;color:var(--ink-1)}
.points-head{font-size:24px;font-weight:600;letter-spacing:-.012em;
  line-height:1.3;color:var(--ink-1);margin:0 0 var(--gap-in)}
.split-lede{font-size:19px;line-height:1.55;color:var(--ink-2);margin:0}

/* ── Ranked content ─────────────────────────────────────────────────────
   This replaced a card grid. Cards gave every item an identical container, so
   seven problems read as seven problems of equal weight, which is false and is
   the opposite of what the frame has to argue. The ranking is not invented:
   the two leads on the challenges scene are exactly what the next two scenes
   are built to solve. Type size and position carry it now, and the fill, the
   radius and the decorative rule are gone with the container. */
/* The remainder. Demoted, not hidden: still readable at the back, but clearly
   the second tier. A hairline separates the ranks without drawing a box. */
.rank-rest{list-style:none;margin:0;padding:var(--s6) 0 0;
  border-top:1px solid rgba(245,243,247,.10);
  display:flex;flex-direction:column;gap:var(--gap-set);max-width:96ch}
.rank-rest li{font-size:18px;line-height:1.55;color:var(--ink-2);
  position:relative;padding-left:30px}
.rank-rest li::before{content:"";position:absolute;left:0;top:calc(.775em - 5.5px);
  width:9px;height:9px;border-radius:2px;background:var(--signal,var(--chartreuse))}
.rank-rest b{font-weight:600;color:var(--ink-1)}


/* ── Stat boards. Bold: these are the numbers the room is meant to keep. ─
   Left-aligned to the title, not centred in the middle of the screen. */
.board{position:relative;z-index:2;display:grid;gap:64px;width:100%;
  padding:0 var(--safe-x)}
.board dt{font-size:56px;font-weight:700;letter-spacing:-.03em;line-height:1;
  color:var(--ink-1)}
.board dd{margin:var(--s3) 0 0;font-size:16px;font-weight:600;
  line-height:1.45;color:var(--ink-2)}

/* Cover: the 0 to 1 line and the team figures are the continuation of the
   title stack, so they hang off the band rather than centring. */
.scene--cover .bleed{z-index:0}
.scene--cover::after{content:"";position:absolute;inset:0;z-index:1;
  pointer-events:none;
  background:linear-gradient(102deg,rgba(0,0,0,.92) 0%,rgba(0,0,0,.78) 32%,
    rgba(0,0,0,.30) 54%,rgba(0,0,0,0) 72%)}
.cover-brief{position:relative;z-index:3;justify-self:start;
  padding-left:var(--safe-x);max-width:616px}
.board--cover{grid-template-columns:repeat(2,1fr);gap:clamp(14px,2.9svh,26px) 40px;
  padding:0;margin:clamp(18px,3.6svh,32px) 0 0;max-width:none}
.board--cover dt{font-size:clamp(34px,5.3svh,48px)}
/* The Head of Design figure carries a "(Me)" beside its numeral. Baseline
   alignment, not centre: the note sits on the same line the 48px digit sits
   on, so their bottoms agree even though the sizes do not. */
.board dt{display:flex;align-items:baseline;gap:.28em}
/* The second set on the cover: same grid, stepped down, so the two read as
   two groups rather than one long list. 34px against the team set's 48px is
   the same ratio the deck uses between a stage title and a card title. */
/* The shipped figures are a different claim from the team figures, so they sit
   in the deck's own card: same fill, same 22px radius, same --s6 padding and
   the same chartreuse mark the cards on the other scenes carry. Numbers step
   down to 34 against the team set's 48 so the card reads as the second beat. */
.cover-shipped{background:var(--card-fill);border-radius:var(--r-card);
  padding:clamp(18px,3.6svh,32px);margin:clamp(24px,4.8svh,44px) 0 0;
  width:552px;max-width:100%}
.cover-shipped-head{font-size:20px;font-weight:600;letter-spacing:-.012em;
  line-height:1.3;color:var(--ink-1);margin:0}
.board--shipped{gap:calc(clamp(16px,3.1svh,28px) * var(--fit)) 40px;
  margin:clamp(14px,2.7svh,24px) 0 0}
.board--shipped dt{font-size:clamp(25px,3.8svh,34px)}
.board--shipped dd{font-size:14px;margin-top:6px}

/* ── Identity spec. Mirrors dreamplay.html's own swatch and type grid. ── */
.spec{position:relative;z-index:2;display:flex;flex-direction:column;gap:var(--gap-group);
  max-width:1160px;width:100%;padding:0 var(--safe-x);justify-self:start}
.spec-type{display:grid;grid-template-columns:repeat(2,minmax(0,420px));gap:56px;
  justify-content:start;padding-top:24px;border-top:1px solid rgba(245,243,247,.10)}
.spec-aa{font-size:82px;line-height:.9;margin:0 0 14px;display:block}
.spec-aa--tomorrow{font-family:'Tomorrow',var(--font-display);font-weight:700}
.spec-aa--inter{font-family:'Inter',var(--font-display);font-weight:700}
.spec-name{font-size:20px;font-weight:600;margin:0 0 6px}
.spec-role{font-size:16px;line-height:1.5;color:var(--ink-2);margin:0}
.spec-row{display:flex;gap:14px}
.spec-row--accent{display:grid;grid-template-columns:repeat(4,1fr);gap:14px}
.swatch{flex:1;height:76px;border-radius:var(--r-capsule);display:flex;
  flex-direction:column;justify-content:center;gap:2px;padding:0 20px;
  box-shadow:inset 0 0 0 1px rgba(245,243,247,.15)}
.swatch b{font-size:15px;font-weight:600}
/* The role label sat at opacity .72, which blended it into its own swatch and
   dropped four of the seven below AA at 13px: Success 2.29:1, Neutral 3.92,
   Brand 4.14, Error 4.43. That is a live counter-example sitting next to a
   bullet claiming the palette is AA readable. Full strength ink instead, and
   the two mid-luminance grounds take near-black rather than white. */
.swatch span{font-size:13px}

/* ── The portfolio's navigation, inside the deck ─────────────────────────
   A talk should not be framed by chrome, so this is hidden until the
   presenter asks for it: any pointer move, any click, or a tab into it. It
   hides again after a few still seconds, unless the Work menu is open or
   focus is inside. Dark glass rather than the site's white bar, because it
   sits over the deck, and it is 56px tall so a scene's kicker at y=56 is the
   first thing clear of it once it goes.

   Links resolve one level up: the deck lives in dreamplay-concise/. */
#deckNav{position:fixed;top:0;left:0;right:0;z-index:60;
  transform:translateY(-100%);opacity:0;pointer-events:none;
  transition:transform 200ms var(--ease-decel),opacity 200ms linear}
#deckNav.on{transform:none;opacity:1;pointer-events:auto}
.dn-bar{display:flex;align-items:center;gap:var(--s5);height:56px;
  padding:0 var(--safe-x);
  background:rgba(11,6,18,.82);backdrop-filter:blur(18px);
  border-bottom:1px solid rgba(245,243,247,.12)}
.dn-logo{display:grid;place-items:center;width:30px;height:30px;flex:none;
  border-radius:7px;background:var(--chartreuse);color:#000;
  font-size:13px;font-weight:700;letter-spacing:.02em;text-decoration:none}
.dn-links{display:flex;align-items:center;gap:var(--s5);margin-left:auto}
.dn-link{position:relative;background:none;border:0;padding:6px 2px;cursor:pointer;
  display:inline-flex;align-items:center;gap:6px;
  font-family:var(--font-display);font-size:14px;font-weight:500;
  color:var(--ink-2);text-decoration:none;
  transition:color 140ms linear}
.dn-link:hover,.dn-link:focus-visible{color:var(--ink-1)}
.dn-link--back{color:var(--chartreuse)}
.dn-drop{position:relative}
.dn-menu{position:absolute;top:calc(100% + 12px);right:0;min-width:248px;
  display:flex;flex-direction:column;padding:8px;
  background:rgba(21,14,31,.98);backdrop-filter:blur(18px);
  border:1px solid rgba(245,243,247,.14);border-radius:12px;
  box-shadow:0 18px 44px rgba(0,0,0,.55)}
.dn-menu[hidden]{display:none}
.dn-menu a{padding:9px 12px;border-radius:8px;
  font-size:14px;color:var(--ink-2);text-decoration:none;white-space:nowrap;
  transition:background 140ms linear,color 140ms linear}
.dn-menu a:hover,.dn-menu a:focus-visible{background:rgba(245,243,247,.08);color:var(--ink-1)}
.dn-rule{height:1px;margin:6px 8px;background:rgba(245,243,247,.14)}

/* ── Reveals. Explicit enter and exit, so stepping back replays. ────── */
/* A group arrives whole. No fade, no rise, no stagger: the slide either has
   a thing on it or it does not. Stepping is still per group on a held scene,
   but the group lands in one frame instead of assembling itself. */
.rv{opacity:0}
.is-in .rv{opacity:1}
/* A whole scene still arrives as a cut: the frame changed, so a fade buys
   nothing. A STAGED release is the opposite case. When one group appears on a
   slide that is otherwise identical, an instant flip gives the room no cue
   where to look and reads as the browser repainting. 110ms sits under the
   180ms input lock in app.js, so it is finished before the next press can
   land and the presenter never waits for it. The cover is excluded: its
   re-arm at 260ms fires while the gate is still fading out, and a fade there
   would blink the first slide of the talk. */
.scene[data-hold]:not(.scene--cover).is-in .rv{
  transition:opacity 110ms var(--ease-decel)}
/* Stepping back re-hides a group. That is a correction, not a performance,
   and a correction that fades looks like an undo the room watches. */
.is-in .rv.held{opacity:0;transition:none}
/* The happy flow's container holds three videos positioned in percentages
   against the raster behind them. It fades, it never moves. */
.rv--noshift{transform:none!important}
.scene--payne .rv{transform:none}
.scene--payne.is-in .rv{transition:none}

/* ── Chrome ─────────────────────────────────────────────────────────── */
/* No plate behind either of these. The counter used to sit on a grey capsule,
   which read as a bar across the bottom of every slide; the numerals carry
   the same information without putting furniture on the page. */
/* The box is pinned rather than left to line-height, because --chrome-box is
   what --foot-safe is composed from and the scene's bottom padding is
   composed from that. If this height drifts, every scene's floor drifts. */
#counter,#mute{position:fixed;bottom:var(--chrome-inset);z-index:40;
  padding:8px 14px;line-height:14px;height:var(--chrome-box);
  font:inherit;font-size:12px;letter-spacing:.1em;text-transform:uppercase;
  background:none;color:var(--dark-ink-3);border:0;
  text-shadow:0 1px 3px rgba(0,0,0,.65), 0 0 10px rgba(0,0,0,.5);
  opacity:0;pointer-events:none;transition:opacity 220ms var(--ease-standard,ease)}
/* Chrome is chrome: it waits for the pointer, like the nav bar above it. */
body.chrome-on #counter{opacity:1}
body.chrome-on #mute{opacity:1;pointer-events:auto}
#mute{display:grid;place-items:center;padding:8px;line-height:0}
#mute svg{width:16px;height:16px;display:block}
#mute .spk{fill:currentColor;stroke:currentColor;stroke-width:1.6;stroke-linejoin:round}
#mute .wav path,#mute .x{fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round}
#mute .x{opacity:0}
#mute[aria-pressed="true"] .wav{opacity:0}
#mute[aria-pressed="true"] .x{opacity:1}
#counter{right:var(--chrome-inset);font-variant-numeric:tabular-nums}
/* Matched to the Social deck: 19px between the two chips, 41px between the
   speaker glyph and the first digit. `right` sets this element's right edge,
   so what it has to clear is the counter's width, not its own. 62px was
   derived from the button's width when it became an icon, which put it 16px
   inside the counter and made the left of the page number a live toggle. */
#mute{right:calc(var(--chrome-inset) + 97px);cursor:pointer}
.on-payne #counter,.on-payne #mute{color:rgba(255,255,255,.82)}

/* Short viewports. The air goes before the inset does, and the band floors
   at 42px plus the device safe area. The 12px type never shrinks, so the
   band can never be squeezed under what the chips actually measure. */
@media (max-height:760px){ :root{--chrome-air:12px} }
@media (max-height:620px){ :root{--chrome-air:0px;--chrome-inset:12px} }

/* #rail, the column of index dots, was deleted as chrome the room reads and
   the presenter never uses. Its CSS outlived its markup and is gone too. */

#overview{position:fixed;inset:0;z-index:60;background:rgba(0,0,0,.94);
  display:grid;place-items:center;padding:var(--s7)}
#overview[hidden]{display:none}
#ovGrid{display:grid;grid-template-columns:repeat(8,1fr);gap:var(--s4);max-width:1280px;width:100%}
/* The label was 10px, under the deck's own 12px floor, and its .55 scrim was
   not dark enough over the light Figma board posters: measured 3.10:1 on the
   Foundation and Download states tiles. 12px, and the label now sits on its
   own solid strip rather than on the artwork, so the ratio no longer depends
   on which poster is behind it. */
#ovGrid button{aspect-ratio:16/10;border:2px solid transparent;border-radius:8px;
  background-size:cover;background-position:center;background-color:#2E2240;
  cursor:pointer;color:var(--ink-1);font-size:12px;letter-spacing:.04em;display:grid;
  place-items:end stretch;padding:0;text-transform:uppercase;overflow:hidden}
#ovGrid button > span{display:block;width:100%;padding:5px 6px;
  background:rgba(6,3,10,.86);text-align:center;line-height:1.25}
#ovGrid button[aria-current="true"]{border-color:var(--chartreuse)}

/* ── The gate ───────────────────────────────────────────────────────────
   The name hangs off the top of the content well and the instruction sits on
   its floor, both on the 64px margin, so the first thing a room sees is the
   frame the next 35 scenes use. The die fills the space between them: one
   object common to every game in the app, drawn as a line figure in the
   deck's own ink. Its face cross-fades every 4.5s and nothing translates or
   rotates, so from the back of a hall the screen reads as still. */
#gate{position:fixed;inset:0;z-index:80;display:block;
  background:var(--dark-ground);color:var(--dark-ink-1);cursor:pointer;
  transition:opacity 240ms var(--ease-in-out)}
#gate.gone{opacity:0;pointer-events:none}

#gate .gate-block{position:absolute;inset:0;padding-bottom:var(--foot-clear);
  display:flex;flex-direction:column;align-items:center;justify-content:center;
  gap:var(--s7)}
#gate .gate-name{font-family:var(--font-display);font-size:var(--display);
  font-weight:700;letter-spacing:-.03em;line-height:1;color:var(--ink-1);margin:0;
  text-align:center}

/* The chrome size is the deck's near-invisible register, where #counter and
   #mute live. A live instruction to a presenter belongs in the signal ink at a
   size the back row reads. Padding is reserved so the focus fill cannot reflow. */
#gate .gate-prompt{font-family:var(--font-display);font-size:14px;font-weight:600;
  letter-spacing:.14em;line-height:1.2;text-transform:uppercase;
  color:var(--chartreuse);margin:0;padding:9px 14px;text-align:center;
  border-radius:var(--r-capsule);background:transparent;
  transition:background 140ms var(--ease-standard),color 140ms var(--ease-standard)}

/* The gate's border box is the viewport, so the global 3px offset ring draws
   off screen and indicates nothing. The indicator moves onto the thing the key
   press actually operates. */
#gate:focus-visible{outline:none}
#gate:focus-visible .gate-prompt,
#gate:active .gate-prompt{background:var(--chartreuse);color:#000}
#gate:active .gate-prompt{transition:none}

.gate-die{position:relative;width:260px;height:260px;overflow:visible;flex:none;
  transition:transform 160ms var(--ease-standard)}
#gate:hover .gate-die,#gate:focus-visible .gate-die{transform:scale(1.04)}

/* The roll, built to the Ludo dice in the product. Measured off the capture
   at 30fps: the die gathers and shrinks to .92 for ~100ms, launches, swells to
   about 1.3x while it tumbles off-centre, decelerates over the last five
   frames and lands at 633ms. Rotation is front-loaded and eased out by the
   keyframe spacing, so the timing function stays linear and never eases twice.
   rotateY stays under 55deg: the artwork is a 2.5D isometric cube, and a wider
   turn would flatten it to nothing instead of revealing a face. The product's
   die resolves to a flat face at rest and so ends ~10% smaller than its idle
   pose; ours returns to the same isometric die it started from. */
#gate.rolling .die-cube{animation:gate-roll 633ms linear forwards}
#gate.rolling .die-cast{animation:gate-cast 633ms linear forwards}
@keyframes gate-roll{
  0%  {transform:translate(0,0)        rotate(0deg)    rotateY(0deg)   scale(1)}
  8%  {transform:translate(0,3px)      rotate(-14deg)  rotateY(-10deg) scale(.96)}
  16% {transform:translate(0,4px)      rotate(-24deg)  rotateY(-6deg)  scale(.92)}
  24% {transform:translate(2px,-8px)   rotate(60deg)   rotateY(30deg)  scale(1.06)}
  32% {transform:translate(4px,-14px)  rotate(240deg)  rotateY(8deg)   scale(1.16)}
  40% {transform:translate(6px,-18px)  rotate(420deg)  rotateY(-14deg) scale(1.28)}
  46% {transform:translate(3px,-19px)  rotate(560deg)  rotateY(-52deg) scale(1.30)}
  55% {transform:translate(-2px,-19px) rotate(700deg)  rotateY(-4deg)  scale(1.30)}
  62% {transform:translate(-4px,-17px) rotate(810deg)  rotateY(50deg)  scale(1.27)}
  72% {transform:translate(-5px,-13px) rotate(920deg)  rotateY(10deg)  scale(1.24)}
  80% {transform:translate(-1px,-9px)  rotate(985deg)  rotateY(-44deg) scale(1.19)}
  86% {transform:translate(3px,-5px)   rotate(1030deg) rotateY(-12deg) scale(1.12)}
  93% {transform:translate(2px,-1px)   rotate(1068deg) rotateY(20deg)  scale(1.04)}
  100%{transform:translate(0,0)        rotate(1080deg) rotateY(0deg)   scale(1)}}

/* The cast shadow reads the height: it tightens and fades as the die rises,
   and is back at full weight on the landing frame. */
@keyframes gate-cast{
  0%  {opacity:.7;transform:scale(1)}
  16% {opacity:.78;transform:scale(1.04)}
  40% {opacity:.34;transform:scale(.82)}
  55% {opacity:.3;transform:scale(.79)}
  86% {opacity:.5;transform:scale(.92)}
  100%{opacity:.7;transform:scale(1)}}

/* The body, lighting and pips are painted by the SVG's own gradients, so the
   only thing CSS owns here is the float, the ring and the throw. */
.die-cast{transform-box:fill-box;transform-origin:center;
  transition:opacity 160ms var(--ease-standard)}
.die-cube{transform-box:fill-box;transform-origin:center;
  animation:die-float 6s var(--ease-in-out) infinite}

@keyframes die-float{0%,100%{transform:translateY(0)}50%{transform:translateY(-5px)}}

:focus-visible{outline:3px solid var(--chartreuse);outline-offset:3px}
a:focus-visible{border-radius:4px}
.sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0)}
@media (prefers-reduced-motion:reduce){
  /* A 1ms transition still schedules a compositing pass and can still fire
     transitionend. none is the honest declaration. Scene changes are already
     cuts, so there is nothing else to disable there. */
  .rv{transform:none!important}
  .is-in .rv{transform:none!important;transition:none!important}
  .scene[data-hold]:not(.scene--cover).is-in .rv{transition:none!important}
  .is-in .rv.held{transition:none!important}
  /* The nav's translateY is the one real vestibular trigger left, and it
     fires on any pointer move. Cross-fade it in place instead. */
  #deckNav{transition:opacity 120ms linear}
  .scene--payne.is-in .rv{transition:none!important}
    #gate{transition:none}
  .die-cube{animation:none}
  .die-cast{animation:none}
}
@media (prefers-reduced-transparency:reduce){ .film__bg{display:none} }

/* Gone from this file, and why:
     .points--row, .points--row--even, --pcols
       - the horizontal rules that ran under a board. Rules now sit in a
         left rail beside the picture on every scene that has them, so the
         horizontal variant has no users left.
     .stage-sub{font-size:26px} and .stage-label{font-size:18px}
       - both were second heading sizes. A continuation scene now titles
         itself at 40px like every other scene: one heading size, read at
         one distance.
     .cards--3 .card{min-height:230px} and the 180 / 168 / 126 siblings
       - fixed floors borrowed from the longest copy in the deck. Cards are
         intrinsic now, equalised per row by the grid.
     .scene--board .plate{margin-bottom:44px}
       - a margin, not a cap, so it did nothing when a board rendered tall.
         Replaced by --gutter and by height-derived --pw / --mh.
     .split{padding-top:24px}
       - double counted with the band, which pushed split scenes low.
     .frame-row,.cards,.board,.spec,.split{max-height:600px}
       - one cap across five unrelated archetypes. */

/* ── Equal-weight cards ────────────────────────────────────────────────
   This restores a container the deck once removed, and the reason it was
   removed is still true: an identical box makes every item read as equal
   standing. That is exactly what is wanted here and was not wanted before,
   so the component is built to be light rather than to be a box.

   No fill gradient competing with the artwork elsewhere in the deck, one
   hairline rather than a ring, and the only saturated thing on the card is
   an eight-character numeral. Weight comes from the type, which is where
   the room is looking. On #000000 a 4.5% ink wash composites to about
   #0B0B0C: a step just perceptible at projector gamma without becoming a
   lit rectangle. Title 18.6:1, body 9.6:1, index 16.4:1, all AAA. */
/* Card grids used to centre in the well. With two rows of cards in an 800px
   well that put ~220px of slack above them, so the title read as belonging to
   a different slide while every other container in the deck starts at the top
   of the well. They now start too, and hang one group-gap below the title so
   the step is deliberate rather than leftover. */
.cards{list-style:none;margin:0;padding:0;min-height:0;
  display:grid;grid-template-columns:repeat(var(--cards-cols,3),minmax(0,1fr));
  grid-auto-rows:1fr;gap:var(--gap-set);align-content:start;
  padding-top:var(--gap-group)}
/* Rows were min-content, so each row sized itself independently: between
   roughly 1280 and 1680 one card in the top row takes a second line of body
   and that row grew while the bottom row did not, leaving six cards at two
   different heights. 1fr makes every row as tall as the tallest, but it can
   only resolve against content if the grid is content-sized, so the box stops
   stretching to the whole well. align-content:start already put it at the top,
   so nothing else moves. */
.scene > .cards{height:auto;align-self:start}
/* Inside a statement the cards already sit under a lead, which brings its own
   gap; a second one would double the step. */
.statement > .cards{padding-top:0}

.card{position:relative;display:flex;flex-direction:column;
  height:100%;min-height:0;gap:var(--gap-in);padding:clamp(16px,2.6svh,28px);
  border:1px solid rgba(245,243,247,.13);border-radius:var(--r-card);
  background:rgba(245,243,247,.07)}

.card-title{font-size:clamp(17px,1.62vw,23px);font-weight:600;
  letter-spacing:-.012em;line-height:1.25;color:var(--ink-1);margin:0}
.card-body{font-size:clamp(14px,1.28vw,18px);line-height:1.5;
  color:var(--ink-2);margin:0}

/* The author sets the count and nothing else. Four reads best as 2x2 at
   16:10, five and six as three across, eight as four across. */
.cards[data-count="2"]{--cards-cols:2}
.cards[data-count="3"]{--cards-cols:3}
.cards[data-count="4"]{--cards-cols:2}
.cards[data-count="5"],.cards[data-count="6"]{--cards-cols:3}
.cards[data-count="8"]{--cards-cols:4}
@media (max-width:1180px){ .cards[data-count]{--cards-cols:2} }

/* Inside a .statement the cards are the third beat, under the lead and its
   sub, and they take whatever height is left. */
.statement > .cards{flex:0 0 auto;min-height:0;width:100%}

/* ── Click to play ─────────────────────────────────────────────────────
   Nothing plays on its own. A clip is armed when its scene is entered and
   starts on a press, so the line lands before the footage does.

   The frame is the target: the whole device, not the badge. The badge is
   only the signal that the target exists, so it is drawn for legibility on
   an unknown screenshot rather than for size. Two layers carry it without
   anyone sampling the poster: a dark disc for a light product screen and a
   white ring for a dark one. rgba(6,3,10,.55) over white composites to
   L .164, so the white glyph is 4.9:1 there and higher everywhere else. */
.vframe{position:relative;cursor:pointer;-webkit-tap-highlight-color:transparent}
.vframe:focus{outline:none}
/* The ring is gone on clips only. On a projector a 3px chartreuse outline
   around a phone frame is the brightest thing on a dark slide, and tabbing to
   a clip is a normal part of driving the deck, so it read as a defect rather
   than an affordance. Focus is still shown here: the clip lifts and lightens
   its play badge, which is what .vframe:focus-visible .vbtn below does. Every
   other control keeps the ring. */
.vframe:focus-visible{outline:none}

.vbtn{position:absolute;left:12px;bottom:12px;z-index:4;
  width:40px;height:40px;display:grid;place-items:center;
  border-radius:50%;pointer-events:none;
  background:rgba(6,3,10,.55);
  box-shadow:inset 0 0 0 1.5px rgba(255,255,255,.9), 0 2px 10px rgba(0,0,0,.45);
  backdrop-filter:blur(6px) saturate(120%);
  transition:width 140ms var(--ease-standard),height 140ms var(--ease-standard),
             background 140ms var(--ease-standard),transform 140ms var(--ease-standard)}
.vbtn > svg{grid-area:1/1;width:16px;height:16px;fill:#fff;display:block;
  transition:width 140ms var(--ease-standard),height 140ms var(--ease-standard)}
/* Both glyphs share one grid cell, so exactly one may ever be displayed.
   These selectors have to outrank `.vbtn > svg{display:block}` above, which
   is (0,1,1): a bare `.vbtn-pause` is (0,1,0) and loses, which drew the
   triangle and the two bars on top of each other. */
.vbtn > .vbtn-pause{display:none}
.vbtn > .vbtn-play{display:block}

/* Playback is chrome, not content, so the controls stay neutral: chartreuse
   is the deck's signal colour and spending it on a hover disc reads as a
   point being made. Near-white measures 1.09:1 against a white app screen,
   so the disc carries its own dark edge or it has no boundary on half
   these posters. */
.vframe:hover .vbtn,
.vframe:focus-visible .vbtn{background:rgba(245,243,247,.94);transform:scale(1.06);
  box-shadow:inset 0 0 0 1.5px rgba(0,0,0,.42), 0 3px 14px rgba(0,0,0,.5)}
.vframe:hover .vbtn > svg,
.vframe:focus-visible .vbtn > svg{fill:#000}
.vframe:active .vbtn{transform:scale(.97);transition:none}

/* Playing: smaller, never fainter. A translucent disc over a light
   screenshot composites to about 1.4:1 and fails 1.4.11, and the ratio would
   change with whatever frame the clip happens to be on. Size is the only
   honest lever. */
.vframe.is-playing .vbtn{width:26px;height:26px}
.vframe.is-playing .vbtn > svg{width:11px;height:11px}
.vframe.is-playing:hover .vbtn,
.vframe.is-playing:focus-visible .vbtn{width:40px;height:40px}
.vframe.is-playing:hover .vbtn > svg,
.vframe.is-playing:focus-visible .vbtn > svg{width:16px;height:16px}
.vframe.is-playing .vbtn > .vbtn-play{display:none}
.vframe.is-playing .vbtn > .vbtn-pause{display:block}

/* The flow board is one picture with three clips pinned into it, so its
   badge sits outside the raster's own screens. */
.vframe--board .vbtn{left:auto;right:14px;bottom:14px}

/* A held group is invisible but still laid out and still hit-testable, so
   without this a clip under one can be started and heard with nothing on
   screen. syncHold() in app.js closes the audio half of the same hole. */
.is-in .rv.held{pointer-events:none}

@media (prefers-reduced-motion:reduce){
  .vbtn,.vbtn > svg{transition:none}
  .vframe:hover .vbtn,.vframe:focus-visible .vbtn,
  .vframe:active .vbtn{transform:none}
}

/* The cover's team and window. Four 48px stat boxes gave a headcount the
   same weight the deck gives an outcome, and the cover was the one slide
   still overflowing at 1280x800 because of it. One line, at body scale. */
/* ── Cover credits ─────────────────────────────────────────────────────
   These are metadata, not prose. Set as a sentence they were a third
   paragraph competing with the lede above and the card below, and the
   reader had to parse a sentence to find two facts. A labelled pair reads
   as data: the eye takes the label, then the value, and skips whichever it
   does not need. The hairline separates them from the lede without adding
   another container next to the card. */
.cover-meta{margin:clamp(22px,4.6svh,42px) 0 0;padding-top:clamp(16px,3svh,24px);
  border-top:1px solid rgba(245,243,247,.13);
  display:grid;gap:clamp(10px,1.8svh,14px)}
.cover-meta > div{display:grid;grid-template-columns:clamp(72px,8vw,92px) 1fr;
  gap:14px;align-items:baseline}
.cover-meta dt{font-size:12px;font-weight:700;letter-spacing:.12em;
  text-transform:uppercase;color:var(--signal,var(--chartreuse));margin:0}
.cover-meta dd{margin:0;font-size:var(--mediacap);line-height:1.45;color:var(--ink-2)}

/* The chapter question is the only thing on the card. It inherited the size
   the chapter NAME used to have, and that was wrong: a name is two or three
   words and can carry 104px, while a question is six to ten and at 104px it
   wrapped to three or four lines and filled half the viewport. This is a two
   second transition slide, so the line wants to be read at a glance, not
   scanned. 52px at 1440 puts every question on one or two lines. */
.ch-next{font-size:clamp(34px,5vw,76px);font-weight:800;letter-spacing:-.018em;
  line-height:1.06;max-width:min(1100px,100%)}
/* The question alone landed as a hard cut: a full-bleed pumpkin field, one
   line, nothing to rest on while the room catches up. This line says what
   the next few slides cover, so the divider hands over instead of stopping.
   Solid #fff, not a tint: .86 white on pumpkin measures 4.21:1, which fails
   AA for body sizes. Size and weight carry the hierarchy instead. */
.ch-sub{color:#fff;margin:var(--gap-set) 0 0;
  font-size:clamp(17px,1.5vw,24px);font-weight:500;line-height:1.35;
  max-width:min(720px,100%)}

/* Keyword rail. Same marks, no bold run inside a line, because a keyword
   list that bolds half of itself is a sentence wearing a list's clothes. */
.points--keys li{font-size:var(--lead-body);line-height:1.45;color:var(--ink-1)}
.points--keys li::before{top:calc(.725em - 5.5px)}

/* A card grid sits at its own content height rather than stretching, because
   two rows stretched to a 700px row would be mostly empty card interior. It
   therefore has to be centred in the row, or the slide reads bottom-heavy
   with a band of dead ground under it. */
/* Centring the whole split row dragged the RAIL down with the cards, so
   the lede floated ~150px below its own title with nothing in between.
   The row starts at the top, the rail sits under the title where it
   belongs, and only the card grid centres, inside its own track. */
.split:has(.cards){align-content:start;align-items:stretch}
.split > .cards{height:100%;min-height:0}
.statement:has(.cards){justify-content:center}


/* ── Narrow fallback ───────────────────────────────────────────────────
   The deck is presented at 1280 and up. Below that it is being previewed
   in a pane, and a two track .split there gives its rail a fixed 340 to
   720px while the media or the card grid takes whatever is left, which is
   how a six card grid ended up in columns an inch wide with one word per
   line. The rail stacks above its content instead of competing with it. */
@media (max-width:1100px){
  .split{display:flex;flex-direction:column;gap:var(--gap-set)}
  .split > .cards{height:auto}
  .cards[data-count]{--cards-cols:2}
  .frame-row{gap:var(--gap-set)}
}
/* One column was worse, not better: six stacked cards cannot fit 805px of
   height however small the type gets, and the deck clips rather than
   scrolls. Two columns halve the stack, and the card's own padding and the
   rhythm tighten with it. */
@media (max-width:900px){
  .cards[data-count]{--cards-cols:2}
  .card{padding:clamp(12px,2svh,18px);gap:6px}
  .card-title{font-size:clamp(15px,3.4vw,18px)}
  .card-body{font-size:clamp(13px,2.7vw,15px)}
  .split{gap:var(--gap-in)}
  .statement-sub,.split-lede{font-size:clamp(14px,2.8vw,17px)}
  /* The fairness scene is the deck's only two-prose-column split, and its
     demoted list is the last thing that will not fit 805px of height. */
  .rank-rest{padding-top:var(--gap-set);gap:var(--gap-in)}
  .rank-rest li{font-size:clamp(13px,2.7vw,16px);padding-left:22px}
  .split .statement-lead{font-size:clamp(22px,4.6vw,34px)}
  .statement-q{font-size:clamp(16px,3.2vw,20px);margin-bottom:var(--gap-set)}
  /* The identity spec is two 82px specimens and seven swatches. It is the
     one block that cannot fit a short window at full size. */
  .spec{gap:var(--gap-set)}
  /* Stacking the two specimens made the block TALLER, which is the wrong
     axis: the window is short, not narrow. They stay side by side and
     shrink instead. */
  .spec-type{grid-template-columns:repeat(2,minmax(0,1fr));gap:var(--gap-set);
    padding-top:var(--gap-in)}
  .spec-aa{font-size:clamp(30px,7.4vw,48px);line-height:.92;margin-bottom:6px}
  .spec-name{font-size:16px;margin-bottom:3px}
  .spec-role{font-size:13px}
  .swatch{height:clamp(38px,5.6svh,52px);padding:0 12px}
  .swatch b{font-size:13px}
  .swatch span{font-size:12px}
  .spec-row,.spec-row--accent{gap:8px}
}

/* ── Scrub bar ─────────────────────────────────────────────────────────
   A presenter needs to jump back to the moment someone asked about. The
   browser's own control bar would put a timecode, a volume slider and a
   fullscreen button across a phone mockup, so this is the one control that
   was actually load bearing, drawn at the deck's own weight. */
.vscrub{position:absolute;left:10px;right:10px;bottom:10px;z-index:5;height:22px;
  display:flex;align-items:flex-end;cursor:pointer;touch-action:none;
  opacity:0;transition:opacity 140ms var(--ease-standard)}
.vframe:hover .vscrub,.vframe:focus-visible .vscrub,
.vframe.is-playing .vscrub{opacity:1}
/* The 22px box is the grab target; the 4px rail is what is drawn. */
.vscrub::before{content:"";position:absolute;left:0;right:0;bottom:0;height:4px;
  border-radius:2px;background:rgba(6,3,10,.55);
  box-shadow:inset 0 0 0 1px rgba(255,255,255,.35)}
.vscrub i{position:relative;display:block;height:4px;width:0;border-radius:2px;
  background:var(--ink-1);pointer-events:none}
.vscrub i::after{content:"";position:absolute;right:-5px;bottom:-3px;
  width:10px;height:10px;border-radius:50%;background:var(--ink-1);
  box-shadow:0 0 0 1.5px rgba(6,3,10,.65);
  opacity:0;transition:opacity 120ms linear}
.vframe:hover .vscrub i::after,.vframe:focus-visible .vscrub i::after,
.vscrub.is-dragging i::after{opacity:1}
.vframe--board .vscrub{display:none}   /* three clips, one bar would lie */

@media (prefers-reduced-motion:reduce){
  .vscrub,.vscrub i::after{transition:none}
}

/* The opportunity slide's sub is the argument, not a caption. Its two
   reasons take the same emphasis a rail's lead words take. */
.statement-sub b{font-weight:600;color:var(--ink-1)}

/* ── Annotated card callouts ──────────────────────────────────
   The two cards are exported clean from Figma and keyed to transparency, and
   every label is real text placed over them: crisp at any size, scales with
   the viewport, rewordable without a re-export, and read by a screen reader
   through the art's alt text.

   --x and --y are the target point as a percentage of the ART box, so
   .co-frame wraps the image alone. A percentage measured against the padded
   box would drift every time the gutter changed width.

   Labels sit OUTSIDE the card, in the gutter, and each leader is its own
   absolutely positioned dashed rule ending in an arrowhead, matching the
   connectors on the happy flow board. A line drawn inside the label can only
   reach the label's own edge, which puts text back over the artwork.

   Everything here is HEIGHT driven so the lobby phone and the two stacked
   cards share one band and scale together. Sizing the cards by width instead
   pushed the mega card past the viewport edge at 1440. */
/* The two columns had 274px of width going spare between them at 1440.
   Nothing here is width bound, so the gap can take it. */
.callouts{display:flex;align-items:stretch;justify-content:center;
  gap:var(--callout-gap,clamp(34px,6.6vw,112px));height:100%;min-height:0}
.co{display:flex;flex-direction:column;align-items:center;justify-content:flex-start;
  gap:var(--gap-media);margin:0;min-height:0}
/* The phone is height driven and the two cards are width driven. Stacking the
   cards to make room for the phone halved them: 197px wide, too small to read
   the very values the labels point at. One row of three, with the cards capped
   by width, keeps all three legible. */
.co{height:100%}
/* .split is `var(--col) 1fr`, and a 1fr track carries an automatic min-content
   floor. With three unshrinkable figures in it the track grew to 1420px at a
   1440px viewport and pushed the mega card off screen. minmax(0,1fr) lets the
   track take the space that is actually left. */
/* .split is `var(--col) 1fr`, and a 1fr track carries a min-content floor, so
   unshrinkable figures grow the track past the box. minmax(0,1fr) lets it take
   only what is left. */
.split:has(.callouts){grid-template-columns:var(--col,340px) minmax(0,1fr)}
.callouts{min-width:0}
/* Both cards share one column so each can use the full band height. Side by
   side they were competing for the same width as the phone and neither could
   grow. */
/* Captions sit under their own card. What made the first one read as the
   second card's title was not its position but the spacing: an even gap above
   and below. The fix is the ratio, roughly 1:6 between a caption and its card
   versus the gap to the next group. */
.co-stack{display:flex;flex-direction:column;justify-content:flex-start;
  gap:clamp(22px,4vh,38px);height:100%;min-height:0;flex:0 1 auto;min-width:0}
/* Same card-to-caption gap the phone frames use. It only needed a bespoke
   value while the exports carried a transparent shadow margin: 104px of dead
   space under the challenge card made a 6px gap look like 27px. The PNGs are
   now cropped to the artwork, so the standard token reads correctly. */
.co-stack .co{gap:var(--gap-media)}
.co-stack > .co{height:auto;min-height:0}
.co--wide,.co--tall{flex:0 0 auto}
.co--wide .co-art,.co--tall .co-art{height:100%;width:auto;max-width:100%}
.co-stack > .co{flex:1 1 0;min-height:0;height:auto}
/* Flex grow carries each card's own aspect: the wide card wants 1/2.29 of
   a unit of height, the tall one 1/0.84, so the pair keep their drawn
   proportion at any height instead of splitting the column evenly. */
/* The grow values are 1/aspect for each card (1/2.2857 and 1/0.8407), which
   is what makes two height-driven cards resolve to the SAME width. That only
   holds if grow distributes stage height, but a .co is stage + gap + caption,
   so the fixed 39.4px of caption was being scaled in the ratio too and the
   widths came out 349 and 406 at 1920. Handing that overhead to flex-basis
   takes it out of the distribution, so grow divides only the artwork. */
.co-stack > .co--wide,
.co-stack > .co--tall{flex-basis:calc(var(--gap-media) + 1.3 * var(--mediacap-title))}
.co-stack > .co--wide{flex-grow:.4375}
.co-stack > .co--tall{flex-grow:1.1895}
.co-stage{display:flex;align-items:flex-start;justify-content:center;
  flex:1 1 auto;min-height:0}
.co--wide .co-stage,.co--tall .co-stage{padding-inline:var(--co-gut,clamp(46px,4vw,62px))}
.co-frame{position:relative;display:flex;align-items:center;height:100%;min-height:0}
.co-art{display:block;height:100%;width:auto;max-width:100%}
.co--wide .co-frame,.co--tall .co-frame{height:100%;justify-content:center}

/* The two stacked cards are height bound, so each resolves to its own width:
   349px and 378px inside a 1010px figure at 1920. .co centres its children,
   which centred that narrow stage, while the caption stretches the full figure
   and reads from the left. The label therefore sat 331px from the card it
   names at 1920 and 181px at 1440, near enough to the neighbouring card to
   look like its caption. Starting the stage, and the art within it, puts every
   caption directly under the left edge of its own frame. The phone keeps the
   centred default: its art already fills its stage, so it was never offset. */
.co--wide,.co--tall{align-items:flex-start}
.co--wide .co-stage,.co--tall .co-stage{justify-content:flex-start}
.co--wide .co-frame,.co--tall .co-frame{justify-content:flex-start}
/* app.js wraps every playable clip in a .vframe and moves the frame-* classes
   onto that wrapper, so the phone frame here is the wrapper, not a div of our
   own. Nesting one inside the other gave the wrapper auto height and the clip
   rendered at its natural 1384px. */
/* Width driven, not height driven. A video's intrinsic width does not shrink
   when its height is constrained, so under flex the phone reported a
   max-content width of 646px and pushed the cards off the slide. */
.co--phone{flex:none}
/* Sized so the frame lands at the 533px the rest of the deck's devices use.
   The bezel is border-box, so the frame runs 6px taller than its video. */
.co--phone .co-stage > .frame-phone{width:var(--phone-w,clamp(158px,17.35vw,250px));height:auto;flex:none}
.co--phone .frame-phone > video{display:block;width:100%;height:auto}
/* The caption starts where the artwork starts. Inside a .co the art is inset
   by the callout gutter, so the caption carries the same inset rather than
   centring under a figure that is wider than the card. */
.co figcaption{font-size:var(--mediacap-title);font-weight:400;color:var(--ink-cap);
  line-height:1.3;text-align:left;flex:none;align-self:stretch}
.co--wide figcaption,.co--tall figcaption{padding-inline:var(--co-gut,clamp(44px,3.9vw,58px))}
/* The phone is width driven (height:auto follows the clip's aspect), but the
   stage was flex:1 1 auto and took whatever height the column had left, so the
   two ended at different points. The 16px in .co's gap is measured from the
   stage, so the frame's real distance to its caption drifted with the viewport:
   8px at 1920, 16px at 1440, 12px at 1366, against a flat 16px on the other
   eighteen captions in the deck. Shrink-wrapping the stage to the phone makes
   the stage edge the frame edge, so the gap is the 16px it claims to be. This
   changes no size, only where the box ends. */
.co--phone .co-stage{justify-content:flex-start;flex:0 0 auto}

/* No target dot. Centred on the point it marks, it straddled the edge by half
   its width and sat on the artwork: on the league card it covered part of the
   PLAY FOR button's label. The arrowhead is the pointer, and it stops at the
   element's near edge rather than reaching its centre. */
.co-lead{position:absolute;top:var(--y);height:0;z-index:2;
  border-top:1px dashed rgba(245,243,247,.45);pointer-events:none}
.co-lead--l{left:0;width:var(--x)}
.co-lead--r{left:var(--x);right:0}
.co-lead::after{content:"";position:absolute;top:50%;width:0;height:0;
  border-block:4px solid transparent}
.co-lead--l::after{right:-1px;transform:translateY(-50%);
  border-left:7px solid rgba(245,243,247,.72)}
.co-lead--r::after{left:-1px;transform:translateY(-50%);
  border-right:7px solid rgba(245,243,247,.72)}
.co-tag{position:absolute;top:var(--y);transform:translateY(-50%);z-index:3;
  pointer-events:none;font-size:clamp(12px,.92vw,14px);font-weight:600;
  line-height:1.25;white-space:nowrap;color:var(--ink-1)}
.co-tag--l{right:calc(100% + 8px)}
.co-tag--r{left:calc(100% + 8px)}

@media (max-width:1100px){
  .callouts{flex-direction:column;gap:var(--gap-set)}
  .co--phone,.co-stack{height:auto}
}

/* ── Visual hierarchy pass ──────────────────────────────────────────────
   Colour, weight and size only. Nothing here moves an element, changes a
   word, or alters what is on a slide.

   The deck had three type volumes assigned by slide ARCHETYPE rather than by
   narrative weight, so an install-size problem shouted louder than the
   business case, and the close was the quietest big moment in the talk.
   Chartreuse had spread to 69 uses across 20 slides, 33 of them list bullets,
   so the one signal colour had become furniture. */

/* Bullets stop spending the signal colour. That leaves chartreuse four jobs:
   the statement rule, the cover's peak figure, the gate, and the close. */
.points li::before,.rank-rest li::before{background:rgba(245,243,247,.38)}

/* Cover: the kicker and the TEAM / TIMELINE labels were the same green as
   the peak daily figure, so a headcount label and the revenue number read as
   equally important. Only the number keeps it. */
.kicker{color:var(--ink-3)}
.cover-meta dt{color:var(--ink-3)}

/* A marked turn in a statement rendered identically to the words around it,
   three times over: "install worse", "Global expression", "cheating was
   prevented". A weight jump was the first attempt and read as a different
   typeface at 72px, so the turn is carried by colour instead. Indigo on dark
   is 5.37:1 and was declared but unused, which gives it one reserved job and
   keeps chartreuse for the cover figure and the close. */
.statement-lead{font-weight:600}
/* The marked turn takes no treatment of its own. A weight jump read as a
   different typeface at 72px and a colour pulled the eye off the sentence. */
.statement-lead em{font-weight:inherit;color:inherit}

/* The label above a rail was louder than the thesis beside it: it was 24px
   at full ink. Stepped down in size, but it keeps full ink so it never reads
   fainter than the list it heads. */
.points-head{font-size:var(--statement)}
.points--keys li{color:var(--ink-2)}
.split .statement-sub{color:var(--ink-1)}

/* Identity and Design system used to size their one claim larger than every
   other subtitle in the deck. They use .stage-cap now, like the rest. */

/* Colour slide: two 82px specimens were the brightest objects on a slide
   whose subject is the palette. */
.spec-aa{font-size:58px;color:var(--ink-2)}

/* The business case and the fairness argument sit in a 640px column, so any
   size above 40px wraps them to a third line. The volume cannot be raised
   without a layout change, so the emphasis is carried by weight and by the
   sub-line's colour instead. */
.split .statement-lead{line-height:1.1}

/* Nine dividers at one volume gave the deck no sense of approach. The early
   questions leave headroom; the last two land hardest. */
.scene--chapter[data-chapter="01"] .ch-next,
.scene--chapter[data-chapter="02"] .ch-next,
.scene--chapter[data-chapter="03"] .ch-next,
.scene--chapter[data-chapter="04"] .ch-next{font-size:clamp(30px,4.2vw,60px)}
.scene--chapter[data-chapter="09"] .ch-next,
.scene--chapter[data-chapter="10"] .ch-next{font-size:clamp(36px,5.4vw,80px);
  letter-spacing:-.024em}

/* Milestones is the last slide before the close and the only place the
   eleven months resolve. It was typeset as evidence. */
.scene[data-id="Milestones"] .stage-title{font-size:clamp(28px,2.9vw,42px);
  letter-spacing:-.022em}

/* The close. White at .82 on indigo is 6.4:1 and the chartreuse turn 5.87:1,
   both AA. It is now the largest type in the deck, as the last beat should
   be, and the second line is the one that lands. */
/* Lighter than the 700 it carried on the flat purple. On a dark ground the
   line already has all the contrast it needs, and a heavy weight over a soft
   bloom reads as shouting at the end of a quiet deck. */
/* Each sentence is written as its own line and has to read as one. At 56px
   the first line ran past the block's own measure and broke into two, so the
   close landed as three ragged lines instead of the two it was written as.
   52px clears it at 1920 with room to spare, and the gentler vw ramp keeps
   the same clearance down the range instead of only at the ceiling. */
.scene--payne .closer{font-size:clamp(32px,3.55vw,52px);font-weight:500;
  letter-spacing:-.022em;line-height:1.16;color:rgba(255,255,255,.94)}
.scene--payne .closer em{color:var(--chartreuse);font-weight:600}

/* ── Type scale: collapse steps that claim a difference they do not make ── */
/* The deck's first bullet list rendered 21.6px at full ink while every other
   bullet list in the deck is 19px at ink-2. Same role, louder voice, for no
   narrative reason. */
.points--keys li{font-size:var(--statement)}
/* The rank list sat 1px below .points li. The demotion is already carried by
   the hairline and the position; the pixel bought nothing. */
.rank-rest li{font-size:var(--statement)}
/* A points-head speaks for the whole slide, a card title for one tile in a
   six-up grid. They were 24px and 23px. */
.card-title{font-size:clamp(16px,1.4vw,20px)}
/* The deck's stated floor is 12px and this was the one label that broke it. */
.cover-meta dt{font-size:12px}

/* ── Full-width boards (Identity, Design system) ────────────────────────
   Both boards are very wide, 2.03:1 and 2.58:1. In a 340px rail split they
   were width bound at 927px and left 154px and 251px of band height unused.
   Moving the one line of copy under the title costs about 90px of height and
   returns 392px of width, which is the better trade for artwork this wide. */
.anchor--tl .split-lede{max-width:min(78ch,100%);margin:var(--gap-in) 0 0}
/* Left aligned, like every other board and like the title above it. Centring
   only showed on Identity, whose board is narrower than the slide, and it
   broke the deck's single left edge. */
.plate--full{width:100%;align-items:flex-start}
.plate--full img{margin-inline:0;object-position:left center}

/* ── The two-group rail (A dropped connection, The opportunity) ─────────
   A points-head at ink-3 sat below the bullets it labels, which are ink-2
   with ink-1 bold runs, so the heading was the dimmest thing in its own
   group. It is a label, not a quiet heading, so it takes a label's form:
   small, tracked, uppercase. That reads as a different class of thing rather
   than as a weaker version of the text beneath it. */
.points-head{font-size:var(--statement);font-weight:600;letter-spacing:-.006em;
  text-transform:none;color:var(--ink-1);line-height:1.35}
/* Two groups in one rail ran straight into each other. The gap has to beat
   the 24px between list items, or a head reads as another item, but stay
   under the 49px of air the rail has above and below it, or the two groups
   separate more than they separate from the slide. That puts it around 38,
   giving a clean ladder: 12 head to its list, 24 between items, 38 between
   groups, 49 to the edges. */
.points + .points-head{margin-top:clamp(26px,4.2svh,38px)}
/* The rail is shorter than the board beside it, so top-aligning it left a
   dead band under the copy. */
/* The rail starts level with the top of the board rather than centring
   against it, so the copy and the first row of screens share a baseline. */
.scene[data-id="A dropped connection"] .split{align-items:start}


/* ── The opportunity: an objective, stated twice ─────────────────────────
   Two peer statements under one label. They stack, so the divider is the
   horizontal hairline .rank-rest already uses to separate peers without
   drawing a box, and it sits between them rather than in front of each: a
   leading rule would push the first line off the deck's single left edge,
   which is the thing style.css defends by name further up this file.
   They sit one clear step under --stage-title and one clear step over the
   bullets, so the rail reads as title, claim, then supporting list. */
/* The two objectives are one thing the slide is asking for, so they sit in
   one box rather than floating as two loose lines. It uses --card-fill, the
   indigo panel the cover already gives to "What shipped": the deck's one
   surface for a small set of figures that belong together, and warmer than
   the flat grey .card tint, which suits a claim rather than a spec. */
.objective{list-style:none;margin:0 0 clamp(44px,6.4svh,68px);
  padding:clamp(20px,2.6svh,30px) clamp(22px,2.2vw,34px);
  display:flex;flex-direction:column;
  /* The panel is sized by the longer of the two lines, not by the rail: a box
     that runs to the column edge reads as a band across the slide rather than
     as a thing holding two statements. */
  width:fit-content;max-width:100%;
  background:var(--card-fill);
  border-radius:var(--r-card)}
.objective-item{position:relative;margin:0;
  font-size:clamp(21px,1.66vw,29px);font-weight:500;letter-spacing:-.016em;
  line-height:1.26;color:var(--ink-1)}
/* Inside the box the divider goes back to the full inner width: a short rule
   needed the surrounding space to read as a tick, and a box already provides
   the edge that space used to imply. Kept faint: on the indigo panel it only
   has to mark the join, and anything brighter drew a line across the box. */
.objective-item + .objective-item{margin-top:var(--gap-set);
  padding-top:var(--gap-set);border-top:1px solid rgba(245,243,247,.10)}

/* Strategy is now the only label on the slide, so it no longer has to shrink
   out of a second label's way: it takes the deck's own .points-head, full ink
   at 19px, and keeps the small bottom margin that binds a label to its list.
   All the separation is carried above it instead, by the objective's margin. */
/* The label over the objective is the same class of thing as the one over
   the strategy, so it is the same element. It carried ink-3 while
   .points-head carries ink-1, which made one of a matched pair read fainter
   than the other for no reason. */

/* ── Where a titleless slide sits on the screen ──────────────────────────
   A scene reserves --title-top above, --foot-safe below and --band-air
   between the title band and the well. On a slide with no title that
   band-air is paid for a row that does not exist, so the well ran
   199 -> 1002 and its centre sat at 600 while the screen's centre is 540.
   Anything centred in it therefore landed 60px low, which is what made the
   statement slides read bottom-heavy. Collapsing the gap puts the well back
   on the deck's own horizon. */
.scene:not(.scene--chapter):not(.scene--cover):not(.scene--payne):not(:has(.anchor--tl)){
  row-gap:0}

/* The opportunity is top-aligned, not centred, so it does not need the extra
   lift below. Collapsing row-gap here still does the right thing for the
   wrong-sounding reason above: with no title to sit under, "Objective" is
   this slide's opening mark, and the deck's one fixed y is --title-top, the
   same 129.6px every stage-title sits on across the other 28 slides. Landing
   the label there rather than one band-air lower reads as "this slide opens
   like every other slide," not as a headline pretending to be a label. */

/* That leaves the centred statements 26px low rather than 60. The last step
   is optical, not arithmetic: a block centred on true centre still reads low
   because the eye weights the top of a frame. Padding the foot of the well
   shortens the box the block centres in, lifting it without a transform,
   which is how every other centring in this file is declared. */
.statement:has(.cards){padding-bottom:clamp(48px,7svh,84px)}

/* Built to scale states a problem and an answer as a labelled pair. At the
   deck's --display size two such lines would fill the slide, so the pair
   steps down to the size the Figma file sets, and the chartreuse rule is
   dropped: with a label directly above it, it read as a divider between the
   label and the line it labels. */
/* Grouping is the whole job here: a label belongs to the line under it, and
   the two pairs have to read as two, not four. The label sits almost on its
   line, and a full group-gap opens between the pairs, so the distance does
   the grouping without a rule or a box. */
.statement-pair{display:flex;flex-direction:column;align-items:flex-start;
  gap:calc(var(--gap-in) * .45)}
.statement-pair > .points-head{margin:0}
.statement-pair > .statement-lead{margin:0;max-width:34ch;
  font-size:clamp(26px,2.9vw,42px);line-height:1.2;letter-spacing:-.02em}
.statement-pair > .statement-lead::before{display:none}
.statement-pair > .statement-lead + .points-head{margin-top:var(--gap-group)}

/* The fairplay slide centres a pair of columns rather than a statement, so it
   misses the rule above and would sit 26px lower than its neighbours. Same
   lift, same reason. */
.scene--vcentre > .split{padding-bottom:clamp(48px,7svh,84px)}


/* Slide 19 wanted the specimen block to drop a little clear of the body line
   under the title. Twenty pixels, asked for by eye, not derived. It has to be
   padding rather than margin: the block is height:100% inside the well, so a
   margin pushes its foot 20px into the chrome band instead of moving what is
   drawn inside it. */
.scene[data-id="Colour"] > .spec{padding-top:20px}



/* The title names the slide; the two lines under it are the thing the slide
   is about. At full ink a 40px title simply beat them, and the eye landed on
   the signpost rather than the claim. Holding the deck's one heading size but
   stepping the colour back to ink-2 keeps this slide's title the same shape
   as every other slide's while letting the objectives take the full ink.
   9.62:1 on this ground, so the step costs nothing in legibility. */
.scene[data-id="The opportunity"] .stage-title{color:var(--ink-2)}

/* The cover is the densest slide in the deck and the only one the fit pass
   drives to its floor. A line break that is a nicety at full height becomes
   an 18px push into the chrome band on a 768px screen, so it is spent only
   where there is height to spend. */
@media (max-height:850px){ .br-tall{display:none} }

/* ── Cards above a board ───────────────────────────────────────────────
   Four points read as one row here rather than the default 2x2, because
   the artwork underneath is what the slide is actually about: the row
   takes its own height and hands the rest of the band to the plate. */
/* outranks .cards[data-count="4"], which defaults four to a 2x2 */
.cards[data-count].cards--row{--cards-cols:4}
.stackv{position:relative;z-index:2;display:grid;
  grid-template-rows:auto minmax(0,1fr);gap:var(--gap-group);
  height:100%;min-height:0;width:100%}
.stackv > .cards{padding-top:0;align-self:start}
.stackv > .plate{min-height:0;align-self:stretch;justify-self:center}
@media (max-width:1180px){ .cards[data-count].cards--row{--cards-cols:2} }

/* The happy flow is one wide board and nothing else, so it runs all the way
   down to the page number rather than stopping short of it. --foot-safe is
   composed on :root, so its parts cannot be overridden from a descendant;
   the total is restated here as box plus inset, which is the chrome's own
   top edge. */
.scene--tochrome{--foot-safe:calc(var(--chrome-box) + var(--chrome-inset))}


/* The phone keeps its drawn proportion from width, with a height term so a
   short window shrinks it rather than pushing it through the floor. The
   cards beside it are height bound; the phone never was the overflow. */
/* Both boards are exported on #151515 in the source, so the page has to meet
   them or the plate sits in a visible rectangle. Set on the scenes, which each
   fill the viewport, so the rest of the deck keeps --ground-1. On Foundation
   the cards are translucent fills, so they lift with the ground and stay the
   lighter surface rather than inverting against it. */
.scene[data-id="Foundation"],
.scene[data-id="Download states"]{--ground-1:#151515}

/* Touchpoints runs 50 characters of title and 111 of subtitle. Against the
   deck's 944px measure both needed a few dozen pixels more than they had, 989
   and 986, so each broke to a second line at 1680 and above while fitting on
   one at 1366 where the type is smaller. Widening the measure on this scene
   alone holds both on one line at every width; every other scene keeps 944. */
.scene[data-id="Touchpoints"]{--title-measure:min(1040px,100%)}

.scene[data-id="Finding a contest"]{--phone-w:min(19.5vw, 352px, calc(39svh - 85px))}
/* The row was sized by its tallest child rather than by the band, so the
   cards could never reach the floor. It fills the band; the cards are height
   bound and take it, and the phone follows through its own height term. */
.scene[data-id="Finding a contest"] .split{height:100%;min-height:0}

/* The chapter cards and the cover keep the old top margin: the 12px lift was
   for the working slides, where it turns into artwork. On a field of colour
   or a cover it would only shift the type. */
.scene--chapter,.scene--cover{--title-top:calc(clamp(112px,12svh,144px) - 20px)}


.m{position:absolute;width:0;height:0;overflow:hidden}
/* Focus groups no longer dim or light one at a time: every frame on a slide
   arrives together and stays at full strength. The lift (deck-zoom.js) is
   the only state that dims the rest of the slide. */
.scene.no-fx .fg,.scene.no-fx .rv{transition:none!important}

/* ── Zoom (js/deck-zoom.js) ──────────────────────────────────────────────
   A lifted member carries one rigid transform. Dimming is a filter on the
   siblings along the member's ancestor path, so it works under any
   stacking context and is not undone by an opacity animation. */
.cz-mem{z-index:5;transform-origin:0 0;transition:transform 380ms var(--ease-standard)}
.cz-mem:not(.at){position:relative}
.cz-out{z-index:4}
.cz-path{z-index:5!important;filter:none!important;opacity:1!important;will-change:auto!important}
.cz-dim{filter:blur(7px) opacity(.08)!important;transition:filter 380ms var(--ease-standard)}
.scene.cz-can img{cursor:zoom-in}
.cz-mem img,img.cz-mem{cursor:zoom-out}
@media (prefers-reduced-motion:reduce){.cz-mem,.cz-dim{transition:none}}
