/*
  opening.css: owned by the opening worker (065/004), design package §6 (the hero, the arc and
  the day around one sticky stage holding the MacBook and the iPhone) and §19.

  Structure: .opening is full bleed. Its stage (.opening__stage) is sticky and 100svh tall; the
  copy (.opening__copy) is pulled up over it with margin-top: -100svh and holds nine blocks: 0 the
  hero, 1 the problem, 2 imagine, 3 the reveal, 4 to 8 the day. The copy sits above the stage and
  lets the pointer through everywhere but the text of the slide that shows, so the phone's Send
  reply can be pressed (§6.6).

  The slides (065/012, "cut and glide"): with JS the whole opening is pinned. The copy is sticky
  too, one screen tall, and its nine blocks are stacked in one grid cell; each is a slide, and
  exactly one shows (.is-on). The section is one screen plus 8.5 slices of scroll tall
  (--opening-slice, the "Already live" panel's pace per tab): story.js shows slide k while the
  scroll into the section is within half a slice of k slices, and slide 8 for a whole slice
  before the stage and the copy scroll away together. A slide that doesn't show is transparent
  and lets the pointer through, but stays in the accessibility tree and the tab order (no
  visibility or inert), so the whole story reads and every button can be reached; focus in it
  scrolls to it (story.js).

  The scroll only picks the slide; the change between two plays in time, as CSS transitions on
  the values story.js writes for the new slide, so the page never rests between two slides. The
  copy crossfades over --opening-cut; --opening-gap later the devices glide to the new pose over
  --opening-glide, and the screens, the brief card and the day rail fade over --opening-fade from
  the glide's start. A transition retargets from where it is when the slide changes again before
  it ends. story.js reads the four timings (in ms, keep the unit) and writes --opening-delay, the
  wait before the glide: the cut and the gap, or none when it turns a glide already under way.

  scenes/story.js poses the devices. On each .opening__dev it writes --w (the device's laid-out
  width, at measure time) and --x, --y, --s, --o (its place, scale and opacity, per slide), and on
  the iPhone .is-front, with .is-dipping and .is-rising while it changes depth. On each
  .opening__layer (a screen) it writes .is-on (it shows, on top), .is-under (it was in, and stays
  opaque under the incoming screen until the change settles) and .is-off (out and settled, not
  painted). It writes --o and --t on the brief card, --o on the day rail, .is-off on both once
  they're out and settled, and --fill on the rail; .is-on on the slide that shows; --opening-fit
  on .opening (short screens, below); .is-posed on .opening once the stage has its first pose;
  .is-instant for the writes that must not animate (on .opening: the first pose, a new layout,
  reduced motion; on the iPhone's screen: the press's sent state); and .is-near, .is-live on
  the stage. For the app switcher (18:00 to 21:00, below) it writes .is-switching and
  .is-switch-back on the iPhone's screen box and .is-outgoing, .is-incoming on its screens and
  label cards; for the hero's staged rise (at the end), .is-entering and .is-arc-held on
  .opening; around Imagine (slide 2: the stage clears before its line, and its line before
  the stage), .is-sinking and .is-surfacing on the stage, --opening-wait on .opening, and an
  offset and a move on the copy's blocks (below). Inside the stage, boxes are placed with
  physical left and top: the stage is a coordinate space the script fills in px, mirrored for
  RTL by the script (the devices themselves never mirror, devices.css).

  Without JS nothing is pinned and the stage isn't sticky: it shows once, in block 0, in the
  hero's pose with the reveal's screens, placed here in CSS with --u, one px of the mockup's
  frame; blocks 1 to 8 are plain text (§6.8 as amended on 2026-10-14). With JS the devices stay
  hidden until story.js has posed them.

  Layouts (§5): wide is (min-width: 1024px) and (orientation: landscape): the copy column at
  0.85fr, the stage across the page. Medium and narrow: the devices hold the stage's top half and
  each slide's text sits under them, from --opening-text: 57svh medium, about 53svh narrow
  (story.js reads that padding to size the band). On a screen too short for the tallest slide's
  copy, story.js writes --opening-fit, where the text must start for that copy to end above the
  copy's bottom padding, and every slide's text starts there; the band shrinks with it.
*/

.opening {
  --opening-hero: 76svh; /* the hero's copy area; the MacBook's top edge sits at its foot */
  --opening-text: 57svh; /* medium (and narrow, below): where a slide's text starts */
  --opening-slice: 75vh; /* each slide's share of the scroll (with JS) */
  --opening-cut: 150ms; /* the copy's fade out and in */
  --opening-gap: 50ms; /* from the copy's swap to the glide */
  --opening-glide: 600ms; /* the devices' move to the next pose */
  --opening-fade: 300ms; /* the screens', the brief card's and the day rail's fade */
  --opening-switch: 1300ms; /* the iPhone's app switcher, 18:00 to 21:00 and back */
  /* Imagine (065/021): the stage clears before the line, and the line before the stage. */
  --opening-sink: 450ms; /* the devices sink and fade, the copy lifts; back up, they rise */
  --opening-line: 400ms; /* the line rises into place once the devices are gone */
  --opening-drop: 300ms; /* the line drops away before the devices rise back */
  --opening-sink-by: 140px; /* how far the devices sink (story.js reads it) */
  --opening-copy-by: 24px; /* how far the copy lifts, rises and drops */
  --opening-ease-in: cubic-bezier(0.55, 0, 1, 0.45); /* the sink's, the lift's and the drop's */
  --opening-delay: calc(var(--opening-cut) + var(--opening-gap)); /* story.js writes it */
  position: relative;
}
@supports (height: 1svh) {
  .opening {
    --opening-slice: 75svh;
  }
}
/* Narrow: the text starts higher, so each beat fits under the devices on a 390 × 844 phone
   (065/007). The devices' band ends 16px above it and is about 10% shorter than at 57svh, at
   any height: 0.9 of the band over a nav and day rail of about 135px. */
@media (max-width: 720px) {
  .opening {
    --opening-text: calc(51.3svh + 14px);
  }
}
@media (min-width: 721px) {
  .opening {
    --opening-hero: 72svh;
  }
}
/* Stacked (the devices above the copy): a 140px sink would carry them across the lifting copy,
   so they sink less. */
@media not all and (min-width: 1024px) and (orientation: landscape) {
  .opening {
    --opening-sink-by: 56px;
  }
}
@media (min-width: 1024px) and (orientation: landscape) {
  .opening {
    --opening-hero: 65svh;
  }
}

/* ---------- The stage ---------- */
.opening__stage {
  position: sticky;
  top: 0;
  z-index: 0;
  height: 100vh;
  height: 100svh;
  overflow: clip;
  container-type: size; /* cqw and cqh are the stage's own width and height (the no-JS pose) */
  pointer-events: none;
}

.opening__dev {
  position: absolute;
  top: 0;
  left: 0;
  width: var(--w);
  opacity: var(--o);
  transform: translate(var(--x), var(--y)) scale(var(--s));
  transform-origin: 0 0;
}
.opening__dev--mac {
  z-index: 1;
}
.opening__dev--iph {
  z-index: 0;
}
.opening__dev--iph.is-front {
  z-index: 2;
}
.opening__stage.is-near .opening__dev {
  will-change: transform, opacity;
}
.opening__rise {
  position: relative;
  container-type: inline-size; /* the brief card is sized in the MacBook's width */
}

/* With JS the pinned section: one screen, then 8.5 slices of scroll (story.js reads its height
   for the slice). Its stage and its copy stick together. */
.js .opening {
  height: calc(100vh + 8.5 * var(--opening-slice));
  height: calc(100svh + 8.5 * var(--opening-slice));
}

/* The glide: each device moves to its new pose, opacity included, after --opening-delay. */
.js .opening__dev {
  transition:
    transform var(--opening-glide) var(--ease-in-out) var(--opening-delay),
    opacity var(--opening-glide) var(--ease-in-out) var(--opening-delay);
}
/* The iPhone changing depth: it fades out over the glide's first half (.is-dipping); story.js
   swaps .is-front at the midpoint, and it fades back in over the second (.is-rising). */
.js .opening__dev--iph.is-dipping,
.js .opening__dev--iph.is-rising {
  transition-duration: var(--opening-glide), calc(var(--opening-glide) / 2);
}
.js .opening__dev--iph.is-rising {
  transition-delay: var(--opening-delay), 0s;
}
/* Imagine (065/021): into slide 2 the devices sink --opening-sink-by and fade over
   --opening-sink, easing in, from the moment the slide changes (.is-sinking); back toward the
   hero they rise into their pose over the same, easing out, once the line has gone
   (.is-surfacing, after --opening-wait). story.js writes the poses. */
.js .opening__stage.is-sinking .opening__dev {
  transition:
    transform var(--opening-sink) var(--opening-ease-in),
    opacity var(--opening-sink) var(--opening-ease-in);
}
.js .opening__stage.is-surfacing .opening__dev {
  transition:
    transform var(--opening-sink) var(--ease-out) var(--opening-wait, 0ms),
    opacity var(--opening-sink) var(--ease-out) var(--opening-wait, 0ms);
}

/* A screen. Without JS the rest layers show. With JS the incoming screen (.is-on) fades in on
   top, from the glide's start; the screen that was in stays opaque under it (.is-under) until
   the change settles, so a swap never dims to the Night behind; any other fades out between
   them. A dark screen (no layer on) is a plain fade. Out and settled, a screen isn't painted
   (.is-off). */
.opening__layer {
  opacity: var(--o, 0);
}
.opening__layer[data-rest] {
  opacity: var(--o, 1);
}
.js .opening__layer {
  z-index: 1;
  opacity: 0;
  transition: opacity var(--opening-fade) var(--ease-out) var(--opening-delay);
}
.js .opening__layer.is-on {
  z-index: 2;
  opacity: 1;
}
.js .opening__layer.is-under {
  z-index: 0;
  opacity: 1;
  transition: none;
}
.opening__layer.is-off,
.opening__brief.is-off,
.opening__day.is-off {
  visibility: hidden;
}
.js :is(.opening__layer, .opening__brief, .opening__day).is-off {
  transition: none;
}
/* Writes that must not animate (story.js): the first pose, a new layout, reduced motion, the
   press's sent state on the phone (§6.6), and Imagine's moves where what moves is gone (on the
   stage, a block or the copy). */
.opening.is-instant
  :is(
    .opening__block,
    .opening__dev,
    .opening__layer,
    .opening__brief,
    .opening__day,
    .opening__day-fill
  ),
.opening .is-instant :is(.opening__layer, .opening__dev, .opening__block),
.opening .opening__block.is-instant {
  transition: none !important;
}
/* Rania's Lume dot never shows in the hero (§2). */
.opening__layer.no-lume .ph-lume {
  display: none;
}

/* The live Send reply (§6.6): pointer and touch only, and only at 14:30. */
.opening__press {
  pointer-events: none;
}
.opening__stage.is-live .opening__press {
  pointer-events: auto;
}

/* ---------- The app switcher, 18:00 to 21:00 (065/020, the owner's option B) ----------
   The iPhone's screen changes from NOCTS to Telegram through iOS's app switcher (phone.css,
   .ph-switch), compressed from the design canvas's 7s loop into one change of --opening-switch,
   1300ms, after --opening-delay like every change. story.js writes .is-switching on the
   phone's screen box (.is-switch-back from 21:00 to 18:00), and .is-outgoing and .is-incoming
   on the two screens and their label cards; the screens' own fade (above) plays under it, the
   incoming one while it is still off the screen. In ms from the start:
     - 0 to 380: a finger dot swipes up from the home bar;
     - 155 to 400: the outgoing screen shrinks to a card at 0.72;
     - 260 to 400: the labels fade in above the cards;
     - 400 to 845: the outgoing card slides off to the inline-start (86% of the screen's width)
       as the incoming card slides in from the inline-end to the centre, on --ease-in-out;
     - 805 to 1065: a tap ripples on the incoming card; 845 to 950, the labels fade out;
     - 950 to 1300: the incoming card opens to the full screen.
   21:00 to 18:00 plays the same from Telegram, the cards sliding the other way (--side); RTL
   mirrors both (--dir). Each screen keeps its own status bar, so the times stay 18:00 and
   21:00. A switch never rests half done: story.js settles it after it has played, and a slide
   that changes while it plays snaps the phone to the newest screen. */
.opening .is-switching {
  --side: var(--dir);
}
.opening .is-switching.is-switch-back {
  --side: calc(-1 * var(--dir));
}
.opening .is-switching .is-outgoing {
  animation: opening-switch-out var(--opening-switch) var(--ease-in-out) var(--opening-delay) both;
}
.opening .is-switching .is-incoming {
  animation: opening-switch-in var(--opening-switch) var(--ease-in-out) var(--opening-delay) both;
}
.opening .is-switching .ph-switch__label {
  animation: opening-switch-label var(--opening-switch) var(--ease-out) var(--opening-delay) both;
}
.opening .is-switching .ph-switch__swipe {
  animation: opening-switch-swipe var(--opening-switch) var(--ease-out) var(--opening-delay) both;
}
.opening .is-switching .ph-switch__tap {
  animation: opening-switch-tap var(--opening-switch) var(--ease-out) var(--opening-delay) both;
}
@keyframes opening-switch-out {
  0%,
  12% {
    transform: translateX(0) scale(1);
  }
  31% {
    transform: translateX(0) scale(0.72);
  }
  65%,
  100% {
    transform: translateX(calc(-86% * var(--side))) scale(0.72);
  }
}
@keyframes opening-switch-in {
  0%,
  31% {
    transform: translateX(calc(86% * var(--side))) scale(0.72);
  }
  65%,
  73% {
    transform: translateX(0) scale(0.72);
  }
  100% {
    transform: translateX(0) scale(1);
  }
}
@keyframes opening-switch-label {
  0%,
  20% {
    opacity: 0;
  }
  31%,
  65% {
    opacity: 1;
  }
  73%,
  100% {
    opacity: 0;
  }
}
@keyframes opening-switch-swipe {
  0% {
    opacity: 0;
    transform: translateY(0);
  }
  5% {
    opacity: 1;
    transform: translateY(0);
  }
  29%,
  100% {
    opacity: 0;
    transform: translateY(calc(-150 * var(--ph-u)));
  }
}
@keyframes opening-switch-tap {
  0%,
  62% {
    opacity: 0;
    transform: scale(0.6);
  }
  68% {
    opacity: 1;
    transform: scale(1);
  }
  82%,
  100% {
    opacity: 0;
    transform: scale(1.5);
  }
}

/* ---------- The brief card (slide 5) ----------
   Glass with a gold border at 50% (§6.4), over the MacBook's calendar, sized in the MacBook's
   width (cqw of .opening__rise): the mockup's 330px card on a 780px MacBook. The display spans
   10.26% to 89.74% of the device's width, so the card's right edge sits 2.56cqw (its own padding)
   inside the display's: at 51.28cqw it ran 4% past it, under the bezel (round 3). Physical left
   and top: it belongs to the screenshot, which never mirrors. */
.opening__brief {
  position: absolute;
  top: 11.54cqw;
  left: 44.87cqw;
  display: grid;
  gap: 1.03cqw;
  width: 42.31cqw;
  padding: 2.56cqw;
  border: 1px solid color-mix(in srgb, var(--gold) 50%, transparent);
  border-radius: 1.8cqw;
  background: var(--glass-bg);
  -webkit-backdrop-filter: var(--glass-filter);
  backdrop-filter: var(--glass-filter);
  box-shadow: var(--shadow-float);
  color: var(--ivory);
  font-family: var(--font-plex), var(--font-system);
  line-height: 1.5;
  opacity: var(--o, 0);
  transform: translateY(calc(var(--t, 1) * 2cqw));
}
/* It fades and rises in with the screens, and out the same way. */
.js .opening__brief {
  transition:
    opacity var(--opening-fade) var(--ease-out) var(--opening-delay),
    transform var(--opening-fade) var(--ease-out) var(--opening-delay);
}
.opening__brief-eyebrow {
  color: var(--gold);
  font-family: var(--font-mono);
  font-size: 1.35cqw;
  font-weight: 500;
  letter-spacing: 0.1em;
  text-transform: uppercase;
}
.opening__brief-title {
  margin-block-end: 0.3cqw;
  font-size: 2.05cqw;
  font-weight: 600;
  line-height: 1.3;
}
.opening__brief-line {
  color: var(--ash);
  font-size: 1.73cqw;
}

/* ---------- The day rail (slides 4 to 8), under the nav ----------
   Five times on one hairline; past times and the line up to the active time are Gold, the
   active time has the larger gold dot. story.js writes --o, --fill and .is-past / .is-on: the
   rail fades with the screens, its times and its line switch with the copy. */
.opening__day {
  position: absolute;
  inset-block-start: calc(var(--nav-bottom) + 12px);
  inset-inline: 0;
  width: min(520px, 100% - 80px);
  margin-inline: auto;
  opacity: var(--o, 0);
}
.js .opening__day {
  transition: opacity var(--opening-fade) var(--ease-out) var(--opening-delay);
}
.opening__day::before,
.opening__day-fill {
  content: '';
  position: absolute;
  inset-block-start: 5px;
  inset-inline: 0;
  height: 1px;
}
.opening__day::before {
  background: color-mix(in srgb, var(--ivory) 15%, transparent);
}
.opening__day-fill {
  background: var(--gold);
  transform: scaleX(var(--fill, 0));
  transform-origin: calc(50% - 50% * var(--dir)) 50%;
  transition: transform 0.35s var(--ease-out);
}
.opening__day-list {
  position: relative;
  display: flex;
  justify-content: space-between;
}
.opening__tick {
  display: flex;
  flex-direction: column;
  align-items: center;
  gap: 7px;
  width: 0;
}
.opening__dot {
  width: 7px;
  height: 7px;
  margin-block-start: 2px;
  border-radius: 50%;
  background: color-mix(in srgb, var(--ash) 50%, transparent);
}
.opening__at {
  color: var(--ash);
  font-family: var(--font-mono);
  font-size: 10.5px;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.opening__tick.is-past .opening__dot,
.opening__tick.is-on .opening__dot {
  background: var(--gold);
}
.opening__tick.is-on .opening__dot {
  width: 11px;
  height: 11px;
  margin-block: 0 -2px;
}
.opening__tick.is-on .opening__at {
  color: var(--gold);
  font-weight: 500;
}
@media (min-width: 1024px) and (orientation: landscape) {
  .opening__day {
    inset-block-start: calc(var(--nav-bottom) + 40px);
  }
  .opening__at {
    font-size: 12px;
  }
}

/* ---------- The copy ---------- */
.opening__copy {
  position: relative;
  z-index: 1;
  margin-block-start: -100vh;
  margin-block-start: -100svh;
  pointer-events: none;
}
.opening__block {
  position: relative;
  min-height: 100vh;
  min-height: 100svh;
}
.opening__text,
.opening__hero > * {
  pointer-events: auto;
}

/* With JS, the slides: the copy sticks with the stage, its blocks stacked in one cell, and only
   the slide that shows (.is-on) is opaque and takes the pointer. They crossfade over
   --opening-cut. Until story.js has posed the stage, the hero shows. */
.js .opening__copy {
  position: sticky;
  top: 0;
  display: grid;
  height: 100vh;
  height: 100svh;
}
.js .opening__block {
  grid-area: 1 / 1;
  opacity: 0;
  transition:
    opacity var(--opening-cut) var(--ease-out),
    translate var(--opening-cut) var(--ease-out);
}
.js .opening__block.is-on,
.js .opening:not(.is-posed) .opening__block--hero {
  opacity: 1;
}
.js .opening__block:not(.is-on) :is(.opening__text, .opening__hero > *) {
  pointer-events: none;
}
/* Imagine (065/021): around slide 2 a block moves --opening-copy-by as it fades. story.js gives
   it an offset (.is-above, .is-below; none is its place) and the move it makes there:
     - .is-lifting: the copy that showed lifts away with the devices' sink, easing in;
     - .is-rising: the line rises into place once the devices are gone (--opening-wait);
     - .is-dropping: the line drops away before the devices rise back, easing in;
     - .is-returning: the copy comes down into place as they rise, after the line has gone.
   Once the change has settled every block is back at its place. */
.js .opening__block.is-above {
  translate: 0 calc(-1 * var(--opening-copy-by));
}
.js .opening__block.is-below {
  translate: 0 var(--opening-copy-by);
}
.js .opening__block:is(.is-lifting, .is-rising, .is-dropping, .is-returning) {
  transition:
    opacity var(--opening-copy-time) var(--opening-copy-ease) var(--opening-copy-wait),
    translate var(--opening-copy-time) var(--opening-copy-ease) var(--opening-copy-wait);
}
.js .opening__block.is-lifting {
  --opening-copy-time: var(--opening-sink);
  --opening-copy-ease: var(--opening-ease-in);
  --opening-copy-wait: 0ms;
}
.js .opening__block.is-rising {
  --opening-copy-time: var(--opening-line);
  --opening-copy-ease: var(--ease-out);
  --opening-copy-wait: var(--opening-wait, 0ms);
}
.js .opening__block.is-dropping {
  --opening-copy-time: var(--opening-drop);
  --opening-copy-ease: var(--opening-ease-in);
  --opening-copy-wait: 0ms;
}
.js .opening__block.is-returning {
  --opening-copy-time: var(--opening-sink);
  --opening-copy-ease: var(--ease-out);
  --opening-copy-wait: var(--opening-wait, 0ms);
}

/* Block 0, the hero: centred in the top of the screen, over the devices' peek. */
.opening__hero {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  min-height: var(--opening-hero);
  padding-block: calc(var(--nav-bottom) + 1.25rem) 1.5rem;
  text-align: center;
}
.opening__hero .lead {
  margin-inline: auto;
}
.opening__hero .cta {
  justify-content: center;
}

/* Blocks 1 to 8, medium and narrow: the text under the devices' band. (.opening__hold is named
   for the holds of 065/007; it is each block's box.) With JS, on a screen too short for the
   tallest slide's copy, the text starts higher, at --opening-fit (story.js). */
.opening__hold {
  min-height: 100vh;
  min-height: 100svh;
  padding-block: var(--opening-text) 2rem;
}
@media not all and (min-width: 1024px) and (orientation: landscape) {
  .js .opening__hold {
    padding-block-start: min(var(--opening-text), var(--opening-fit, 100vh));
  }
}

.opening__text {
  max-width: 36rem;
}
.opening__block--center .opening__text {
  margin-inline: auto;
  text-align: center;
}

/* The anchor for #how lands with block 4's top at the top of the screen. With JS, where the
   copy is pinned, story.js lands it on slide 4's centre instead. */
.opening #how {
  top: calc(var(--header-h) + var(--space-4));
}

/* Block 1: the problem, its gold line inside the same heading. */
.opening__punch {
  display: block;
  margin-block-start: 0.55em;
  color: var(--gold);
  font-size: 0.6em;
  line-height: 1.2;
}
.opening__word {
  display: inline-block;
}

/* Block 2: imagine. */
.opening__imagine {
  max-width: 16em;
  margin-inline: auto;
}

/* Block 3: the reveal, at H1 size: "That's" in Ivory, the wordmark and the full stop in Gold. */
.opening__reveal {
  font-family: var(--font-heading);
  font-size: var(--fs-hero);
  font-weight: 600;
  line-height: var(--lh-hero);
  letter-spacing: var(--track-h1);
}
.opening__name {
  color: var(--gold);
  white-space: nowrap;
}
.opening__mark {
  display: inline-block;
  width: auto;
  height: 0.86em;
  vertical-align: -0.12em;
}
.opening__stop {
  margin-inline-start: -0.04em;
}

/* Blocks 4 to 8: the time and the place, the H3, the paragraph, NOCTS's quote, the button. */
.opening__when {
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  column-gap: 0.875rem;
  margin-block-end: 0.875rem;
}
.opening__time {
  color: var(--gold);
  font-family: var(--font-mono);
  font-size: 1rem;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
}
.opening__place {
  color: var(--ash);
  font-family: var(--font-label);
  font-size: var(--fs-eyebrow);
  font-weight: 500;
  letter-spacing: var(--track-label);
  text-transform: uppercase;
}
.opening__place:lang(ar) {
  font-size: 0.8125rem;
  text-transform: none;
}
.opening__title {
  margin-block-end: 1.125rem;
}
.opening__quote {
  max-width: 27.5rem;
  margin-block-start: 1.625rem;
  padding: 1.25rem;
  border-radius: var(--radius-md);
}
.opening__who {
  display: flex;
  align-items: center;
  gap: 0.5rem;
  margin-block-end: 0.625rem;
  color: var(--ivory);
  font-family: var(--font-mono);
  font-size: 0.6875rem;
  font-weight: 500;
  letter-spacing: 0.12em;
  line-height: 1;
}
.opening__icon {
  display: grid;
  flex: none;
  place-items: center;
  width: 22px;
  height: 22px;
  border-radius: 5px;
  background: var(--night);
  box-shadow: inset 0 0 0 1px var(--hairline);
  color: var(--gold);
}
.opening__icon svg {
  width: 14px;
  height: 8px;
}
.opening__said {
  color: var(--ivory);
  font-size: 1rem;
  line-height: 1.5;
}
.opening__tip {
  display: flex;
  align-items: center;
  gap: 0.625rem;
  margin-block-start: 1.625rem;
  color: var(--ivory);
  font-family: var(--font-mono);
  font-size: 0.8125rem;
  line-height: 1.5;
}
.opening__lume {
  flex: none;
  width: 7px;
  height: 7px;
  border-radius: 50%;
  background: var(--lume);
}
.opening__see {
  margin-block-start: 1.375rem;
}
.opening__see svg {
  width: 16px;
  height: 16px;
  transform: scaleX(var(--dir));
}

/* Narrow: each beat fits under the devices on a 390 × 844 phone, in every language (065/007),
   its end at least 16px above the screen's foot. In order: the band above (--opening-text),
   then the beat's spacing, then its H3 one step down the scale (the panel's slide H3) and its
   paragraph at 1.5 (Arabic keeps its own leading, and fits without). Only with JS: without it
   nothing is pinned, and the beats keep their spacing. */
@media (max-width: 720px) {
  .js .opening__when {
    margin-block-end: 0.5rem;
    line-height: 1.25;
  }
  .js .opening__title {
    margin-block-end: 0.625rem;
    font-size: var(--fs-slide);
  }
  .js .opening__text > .para:not(:lang(ar)) {
    line-height: 1.5;
  }
  .js .opening__quote,
  .js .opening__tip,
  .js .opening__see {
    margin-block-start: 0.75rem;
  }
  .js .opening__quote {
    padding: 0.875rem;
  }
  .js .opening__who {
    margin-block-end: 0.5rem;
  }
}

/* ---------- Wide: the copy column at 0.85fr, imagine and the reveal centred ---------- */
@media (min-width: 1024px) and (orientation: landscape) {
  .opening__hold {
    display: grid;
    grid-template-columns: minmax(0, 0.85fr) minmax(0, 1.15fr);
    column-gap: clamp(1.5rem, 3vw, 3rem);
    align-content: center;
    padding-block: calc(var(--nav-bottom) + 4rem) 2.5rem;
  }
  .opening__text {
    grid-column: 1;
    max-width: none;
  }
  .opening__text > .para {
    max-width: var(--measure-copy);
  }
  .opening__block--center .opening__text {
    grid-column: 1 / -1;
  }
  /* The reveal's line sits above the devices, which are centred under it (§6.3). */
  .opening__block--reveal .opening__hold {
    align-content: start;
    padding-block-start: calc(var(--nav-bottom) + 7vh);
    padding-block-start: calc(var(--nav-bottom) + 7svh);
  }
}

/* ---------- Without JS (§6.8 as amended on 2026-10-14) ----------
   The stage isn't sticky: it shows once, inside block 0's 100svh, in the hero's pose with the
   reveal's screens (the rest layers: the desktop chrome, Needs you · 3 and the clocks at 08:59),
   and scrolls away with block 0. Blocks 1 to 8 then read as plain text blocks at their natural
   height, with no devices behind them. The pose is story.js's hero pose: --u is one px of the
   mockup's frame (1440 × 900 wide, at most 1.15; 375 × 620 medium and narrow, frame M01), and
   the MacBook's top edge sits at the foot of the hero's copy area (--opening-hero). */
html:not(.js) .opening__stage {
  position: relative;
  /* The devices fade out over the stage's last 15% instead of ending in a hard cut at its foot.
     Only the mask's alpha counts: they fade into the Night behind them. */
  -webkit-mask-image: linear-gradient(to bottom, var(--night) 85%, transparent);
  mask-image: linear-gradient(to bottom, var(--night) 85%, transparent);
}
html:not(.js) .opening__dev {
  --o: 1;
  --x: 0px;
  --y: 0px;
  --s: 1;
  --u: min(100cqw / 375, 100cqh / 620);
}
html:not(.js) .opening__dev--mac {
  --w: calc(400 * var(--u));
  left: calc(50% - 199.5 * var(--u));
  top: var(--opening-hero);
}
html:not(.js) .opening__dev--iph {
  --w: calc(106.93 * var(--u));
  left: calc(50% - 167.06 * var(--u));
  top: calc(var(--opening-hero) + 23.03 * var(--u));
}
html:not(.js) .opening__block:not(.opening__block--hero),
html:not(.js) .opening__hold {
  min-height: 0;
}
html:not(.js) .opening__hold {
  padding-block-start: var(--space-8);
}
@media (min-width: 1024px) and (orientation: landscape) {
  html:not(.js) .opening__dev {
    --u: min(100cqw / 1440, 100cqh / 900, 1.15px);
  }
  html:not(.js) .opening__dev--mac {
    --w: calc(1060 * var(--u));
    left: calc(50% - 530 * var(--u));
  }
  html:not(.js) .opening__dev--iph {
    --w: calc(218.31 * var(--u));
    left: calc(50% - 469.35 * var(--u));
    top: calc(var(--opening-hero) + 49.84 * var(--u));
  }
}
html:not(.js) .opening__layer:not([data-rest]),
html:not(.js) .opening__day,
html:not(.js) .opening__brief,
html:not(.js) .opening__tip,
html:not(.js) .opening__see {
  display: none;
}
/* With JS the stage stays hidden until story.js has posed it. */
.js .opening:not(.is-posed) .opening__dev {
  visibility: hidden;
}

/* ---------- The hero's staged rise and the problem's lines (§19; 065/018) ----------
   Only under .js and never with reduced motion. story.js marks .opening .is-entering (and
   .is-arc-held) at its first pose when the page loads showing slide 0 and nothing has scrolled
   it yet; a page loaded at another slide or on #how shows that slide at once. The rise waits for
   .is-ready, the page's final text, then plays once, on --ease-out:
     - the H1 rises 16px and fades in over 600ms;
     - the sub, 150ms later, and the buttons, 250ms later, the same over 500ms;
     - the MacBook and the iPhone, together, rise 60px into place and fade in over 800ms from
       450ms (60 screen px: the rise is inside each device's own --s);
     - the arc's cue (an animation on .opening that changes nothing) ends at 1050ms, when the
       devices are mostly in: story.js then drops .is-arc-held and layout.css fades the arc up
       over its 0.6s. An arc drawn later than that fades up on its first frame, as before.
   The hero's copy is hidden until story.js has posed the stage, and while the rise waits for
   .is-ready; the failsafes show the copy and the devices after 4s should neither come. The
   problem's heading shows line by line (story.js writes each word's --line), its gold line
   last. */
@media (prefers-reduced-motion: no-preference) {
  .js .opening:not(.is-posed) .opening__hero > *,
  .js:not(.is-ready) .opening.is-entering .opening__hero > *,
  .js:not(.is-ready) .opening.is-entering .opening__rise {
    opacity: 0;
    animation: opening-failsafe 0.6s var(--ease-out) 4s both;
  }
  .js .opening:not(.is-posed) .opening__dev {
    animation: opening-shown 0s 4s forwards;
  }
  .js.is-ready .opening.is-entering {
    animation: opening-arc-cue 1.05s linear;
  }
  .js.is-ready .opening.is-entering .opening__hero > * {
    animation: opening-rise 0.5s var(--ease-out) var(--in, 0s) both;
  }
  .js.is-ready .opening.is-entering .opening__hero > :first-child {
    animation-duration: 0.6s;
  }
  .js.is-ready .opening.is-entering .opening__hero > :nth-child(2) {
    --in: 150ms;
  }
  .js.is-ready .opening.is-entering .opening__hero > :nth-child(3) {
    --in: 250ms;
  }
  .js.is-ready .opening.is-entering .opening__rise {
    animation: opening-rise-device 0.8s var(--ease-out) 0.45s both;
  }

  .js .opening__word,
  .js .opening__punch {
    transition:
      opacity 0.5s var(--ease-out) calc(var(--line, 0) * 90ms),
      transform 0.5s var(--ease-out) calc(var(--line, 0) * 90ms);
  }
  .js .opening__problem:not(.is-in) .opening__word,
  .js .opening__problem:not(.is-in) .opening__punch {
    opacity: 0;
    transform: translateY(0.35em);
    transition-delay: 0s;
  }
}
@keyframes opening-failsafe {
  to {
    opacity: 1;
    transform: none;
  }
}
@keyframes opening-shown {
  to {
    visibility: visible;
  }
}
@keyframes opening-rise {
  from {
    opacity: 0;
    transform: translateY(16px);
  }
}
@keyframes opening-rise-device {
  from {
    opacity: 0;
    transform: translateY(calc(60px / var(--s, 1)));
  }
}
@keyframes opening-arc-cue {
  to {
    --opening-arc-cue: 1;
  }
}
