/* Loads after icons.css: the marks on the nodes and the cards are .ico masks declared there, and the
 * sizes below (44px discs, 32px card marks) override the sizes that stylesheet sets. */
/* ---- film + stage ---- */
.film { position: relative; background: var(--ink); }
/* 100dvh, not 100vh, and the 100vh line is only the fallback for a browser that has no dvh.
 *
 * On iPad and iPhone Safari `100vh` is not the height you can see. It is the height the page would have
 * if the toolbars were out of the way, and the toolbars are not always out of the way - so a stage of
 * 100vh is taller than the window that is showing it, and everything the stage pins to its own bottom
 * edge (the hero's two buttons sit 6vh up from it) is pushed below the fold. Patrick's words: "I always
 * have to scroll up partially just to be able to see whatever screen I want to see at the time." In the
 * other state - toolbar retracted, visible area larger than 100vh - the stage is shorter than the window
 * and the page's own ground shows underneath it, which is the "bit of a blue bar at the bottom": --ground
 * is #0a0a1a, a blue black. dvh is the height actually on the screen, so neither can happen.
 *
 * The spacers that carry the film's scroll length stay in vh on purpose (main.mjs builds them). They must
 * not change when a toolbar slides, or the whole film would jump under the reader's thumb; only the stage
 * follows the window. Unverified on real hardware - this needs Patrick's iPad to confirm. */
.stage { position: sticky; top: 0; height: 100vh; height: 100dvh; overflow: hidden; }
.poster, .screen, .ambient, .sparks, .stills, .stars, .loop, .ember, .leader { position: absolute; inset: 0; width: 100%; height: 100%; }
.poster { object-fit: cover; object-position: 50% 50%; }
.stills img { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover; opacity: 0; transition: opacity .9s ease; }
.stills img.is-on { opacity: 1; }
.ambient, .sparks, .stars, .loop, .ember, .leader { pointer-events: none; }
/* The sky's stars only add light over the film; they stay under reduced motion (still, over the chapter still). */
.stars { mix-blend-mode: screen; }
.loop { object-fit: cover; opacity: 0; will-change: opacity; }
.loop.is-dead { display: none; }
.ember {
  opacity: 0; will-change: opacity; mix-blend-mode: screen;
  background: radial-gradient(ellipse at var(--ember-x, 50%) var(--ember-y, 78%), rgba(211, 165, 74, .6) 0%, rgba(211, 165, 74, .2) 10%, rgba(211, 165, 74, 0) 30%);
}
.vignette { position: absolute; inset: 0; pointer-events: none; background: radial-gradient(ellipse at 50% 50%, transparent 55%, rgba(5, 5, 7, .55) 100%); }
/* Carries the finale down to the exact colour the page below it opens on (ui.css --ground), over half a
   screen, on an eased curve. Patrick saw a line at the join; the cause was a short hard ramp to near-black
   in the last 12% of a 38vh strip, which read as a flat bar sitting between two pictures. This is the same
   dissolve spread over 52vh with no step in it, and it ends at 1 rather than .97, so the film's last row
   and the page's first row are the same value and there is nothing at the seam to see. */
.fade-bottom {
  position: absolute; left: 0; right: 0; bottom: 0; height: 52vh; pointer-events: none; opacity: 0;
  background: linear-gradient(to bottom,
    rgba(10, 10, 26, 0) 0%,
    rgba(10, 10, 26, .10) 24%,
    rgba(10, 10, 26, .30) 46%,
    rgba(10, 10, 26, .58) 66%,
    rgba(10, 10, 26, .84) 84%,
    rgba(10, 10, 26, .96) 94%,
    rgba(10, 10, 26, 1) 100%);
}
.spacer { width: 100%; }

/* layers */
.layer { position: absolute; inset: 0; opacity: 0; visibility: hidden; }
.layer.is-active { opacity: 1; visibility: visible; }
.layer-stations { transition: opacity .4s ease, visibility 0s .4s; }
.layer-stations.is-active { transition: opacity .4s ease; }

/* ---- beats ---- */
.beat { position: absolute; opacity: 0; visibility: hidden; pointer-events: none; will-change: opacity, transform; }
.beat.is-on { visibility: visible; pointer-events: auto; }
.beat-hero { inset: 0; display: grid; grid-template-rows: 1fr auto 1fr; justify-items: center; text-align: center; padding: 0 6vw; }
.hero-centre { grid-row: 2; display: flex; flex-direction: column; align-items: center; }
.hero-lower { grid-row: 3; align-self: end; display: flex; flex-direction: column; align-items: center; margin-bottom: 6vh; }

/* The mark as backdrop. Enormous, centred behind the type, and held down to a value just above the
 * ground so it reads as pressed into the dark rather than printed on top of it. It is the real brand
 * file rather than a redrawn etching, so it keeps the logo's own colour under the dimming. */
.hero-plate {
  /* --px/--py are the pointer's parallax, written on .beat-hero every frame over the void (Plan 12 H3).
     No transition: the easing is done in JS, and a CSS one on top of it would lag a frame behind. */
  position: absolute; left: 50%; top: 50%;
  transform: translate(calc(-50% + var(--px, 0px)), calc(-54% + var(--py, 0px))); transition: none;
  width: min(70vh, 54vw); height: auto; pointer-events: none;
  opacity: .055; filter: saturate(.45) brightness(1.15) blur(.6px);
  mask-image: radial-gradient(ellipse 62% 62% at 50% 48%, #000 42%, transparent 78%);
  -webkit-mask-image: radial-gradient(ellipse 62% 62% at 50% 48%, #000 42%, transparent 78%);
}
.beat-hero h1 { font-size: clamp(72px, 12vw, 180px); }

/* H4: a band of light travels through the wordmark once on hover. The second, solid gradient layer
 * keeps every stroke lit throughout — only the moving band on top of it brightens as it passes. */
.beat-hero h1 { cursor: default; }
.beat-hero h1:hover {
  background-image: linear-gradient(100deg, transparent 42%, rgba(255, 255, 255, .85) 50%, transparent 58%), linear-gradient(#F9F4F5, #F9F4F5);
  background-size: 220% 100%, 100% 100%; background-repeat: no-repeat;
  -webkit-background-clip: text; background-clip: text; color: transparent; -webkit-text-fill-color: transparent;
  animation: word-sweep 1.2s ease-in-out 1;
}
@keyframes word-sweep { from { background-position: 120% 0, 0 0; } to { background-position: -120% 0, 0 0; } }
.reduced .beat-hero h1:hover { animation: none; }

/* Hairline broken by a diamond, straight off the splash screen. */
.hero-rule {
  position: relative; margin-top: 1.1rem; width: min(26rem, 62vw); height: 1px;
  background: linear-gradient(to right, transparent, var(--silver-dim) 22%, var(--silver-dim) 78%, transparent);
}
.hero-rule::after {
  content: ''; position: absolute; left: 50%; top: 50%; width: 7px; height: 7px;
  transform: translate(-50%, -50%) rotate(45deg);
  background: var(--void); box-shadow: 0 0 0 1px var(--silver-dim);
}
.tagline { margin-top: 1.1rem; font: italic 400 clamp(1.15rem, 1.7vw, 1.5rem)/1.3 var(--font-head); color: rgba(200, 184, 219, .78); letter-spacing: .02em; }

/* The claim, set as an inscription rather than a sentence: small, widely tracked capitals well under
 * the wordmark. The words are unchanged; what was wrong before was that they read as a paragraph. */
.hero-line {
  margin: .95rem 0 0; max-width: 52em;
  font: 400 clamp(.66rem, .84vw, .78rem)/1.6 var(--font-caps);
  text-transform: uppercase; letter-spacing: .34em; text-indent: .34em;
  color: rgba(200, 184, 219, .62); text-shadow: var(--shadow-text);
}
/* The film's hero lost its paragraph in Plan 18 B1 - the name, the subtitle and the one line carry it
 * alone now - so the buttons are the whole lower group and have nothing above them to clear. */
.hero-actions { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 1.4rem; }
/* Short viewports and phones stack the two groups and centre the block. */
@media (max-height: 699px) {
  .beat-hero { display: flex; flex-direction: column; justify-content: center; }
  .hero-lower { align-self: center; margin-bottom: 0; margin-top: 6vh; } /* align-self: end would mean "right" in the column */
}
.beat-line {
  left: 50%; bottom: 16vh; transform: translateX(-50%); margin: 0; max-width: 80vw; text-align: center;
  font: italic 400 clamp(1.5rem, 2.6vw, 2.3rem)/1.2 var(--font-head); color: var(--ivory); text-shadow: var(--shadow-text);
}
.beat-crown { inset: 0; display: grid; place-items: center; text-align: center; padding: 0 6vw; }
.beat-crown h2 { font: italic 400 clamp(2rem, 4.6vw, 4rem)/1.1 var(--font-head); text-shadow: var(--shadow-text); }
.beat-finale { left: 7vw; bottom: 14vh; max-width: 34rem; }
.finale-mark { width: 56px; height: auto; }
/* The margins came in by .2rem and .3rem when the glass button (Plan 21) grew the pair under the lore by
 * 5px: past the end of the film the beat's heading was landing 5px under the header's brand. */
.beat-finale h2 { margin: .6rem 0 .6rem; font-size: clamp(2rem, 3.6vw, 3.2rem); text-shadow: var(--shadow-text); }
.finale-lore { margin-bottom: 1.1rem; font: italic 400 clamp(1.1rem, 1.5vw, 1.35rem)/1.4 var(--font-head); color: var(--mist); text-shadow: var(--shadow-text); }

/* ---- growth stations ---- */
.stop { position: absolute; inset: 0; pointer-events: none; visibility: hidden; }
.layer.is-active .stop.is-on { visibility: visible; }
.stop-copy { position: absolute; top: 50%; left: 7vw; width: min(38vw, 30rem); transform: translateY(-50%); pointer-events: auto; }
/* While a card is open the column comes up over .nodes, which is later in the station and paints over it.
 * At 1024x768 the World building card has to lie across the Writing orb, and that orb's name tag (z 3
 * inside .nodes) printed over the card read as a mistake; the card over the orb reads as a card. The
 * column is a stacking context of its own (the transform), so the lift has to be on the column. */
.stop.is-open .stop-copy { z-index: 2; }
.stop[data-side="right"] .stop-copy { left: auto; right: 7vw; }
.stop h2 { font-size: clamp(1.8rem, 3vw, 2.6rem); opacity: var(--heading, 0); text-shadow: var(--shadow-text); will-change: opacity; }
.stop-body { margin-top: .9rem; opacity: var(--body, 0); display: grid; grid-template-rows: 1fr; transition: grid-template-rows .26s ease; }
.stop-body > div { overflow: hidden; }
/* A swap callout takes the copy's place: the heading fades out and the paragraph folds while it is open,
 *  both back with a fade on close (was-swap carries the transition for that beat). A far callout leaves
 *  the copy alone. Phones keep every callout in the copy's flow, folding the paragraph. */
.stop.is-swap h2, .stop.was-swap h2 { transition: opacity .26s ease; }
.stop.is-swap h2 { opacity: 0; }
.stop.is-swap .stop-body, .phone .stop.is-open .stop-body { grid-template-rows: 0fr; }
/* The far slot: the empty half opposite the copy, vertically centred. */
/* --nudge is how far callouts.mjs has had to slide this card to keep it off an orb; 0 nearly always. */
/* z-index 2 lifts the card over .nodes, which is later in the station and paints over the slot. At 1024x768
 * the World building panel's card has to lie across the Writing orb (its tag has z 3 inside .nodes), and a
 * name tag printed over the card read as a mistake; the card over the orb reads as a card. */
.stop > .callout-slot.far { position: absolute; top: 50%; right: 7vw; width: min(38vw, 30rem); transform: translateY(calc(-50% + var(--nudge, 0px))); pointer-events: none; z-index: 2; }
.stop[data-side="right"] > .callout-slot.far { right: auto; left: 7vw; }
.callout-slot.far .callout { margin-top: 0; }
/* The third slot (Plan 18 B3): above the middle of the stage, for an orb at the top of a fan. The space
 * under such an orb is not free - the other two orbs of the fan and their name tags are in it - so its
 * card goes up, where there is nothing but sky. */
.stop > .callout-slot.top { position: absolute; left: 50%; top: 6vh; width: min(34vw, 28rem); transform: translateX(-50%); pointer-events: none; }
.callout-slot.top .callout { margin-top: 0; }
/* The canopy's World building panel opens under its own orb rather than in the copy column on the far
 * side of the page (Plan 18 B7). Its orb sits high on the right, so the panel hangs beneath it there. */
/* pointer-events: auto is not decoration. .stop switches the pointer off for the whole station and the copy
 * column switches it back on for itself; this box never did, so the six rows of the World building panel
 * could be reached by script and by keyboard but not by a mouse (Plan 21: "they don't actually work"). */
.stop > .bough-side { position: absolute; top: 26vh; right: 7vw; width: min(34vw, 26rem); pointer-events: auto; }
.stop > .bough-side .trunk-panel { margin-top: 0; }
.stop p { color: var(--mist); text-shadow: var(--shadow-text); }
.stop p + p { margin-top: 1.4rem; }
.stop-ask { margin-top: 1.4rem; font: italic 400 clamp(1.15rem, 1.6vw, 1.4rem)/1.35 var(--font-head); color: var(--ivory); }
.stop-either { margin-top: 1.4rem; color: var(--mist); }
/* The trunk (spec 4.2): the button sits under the paragraph; open, the paragraph folds (the same grid trick
 * the swap callout uses) and the panel takes the column: four rows of icon, name and two plain lines. */
/* Hidden, not just transparent, until the copy has arrived: the row rides --body like the paragraph it
 * follows, and a control at opacity 0 is still clickable and still in the tab order. */
.stop-actions { margin-top: 1.4rem; visibility: hidden; }
.stop.is-read .stop-actions { visibility: visible; }
.stop.is-unfolded .stop-body { grid-template-rows: 0fr; }
.trunk-panel { margin-top: .9rem; opacity: var(--body, 0); animation: callout-in .26s ease; }
.panel-row {
  display: grid; grid-template-columns: 26px 1fr; gap: 0 .9rem; align-items: start; width: 100%;
  padding: .8rem 0; border: 0; border-top: 1px solid var(--hair); background: none; color: inherit; text-align: left; cursor: pointer;
}
/* The rule that closes the list: the last row is the one Back follows. Not :last-of-type - Back is a
 * button too, and it is the last of that type - and not :nth-last-child(2), which counts instead of saying. */
.panel-row:has(+ .unfold-back) { border-bottom: 1px solid var(--hair); }
.panel-row:hover, .panel-row:focus-visible { background: rgba(143, 163, 255, .06); }
.panel-row:focus-visible { outline: 2px solid var(--mist); outline-offset: 2px; }
/* Open, a row lifts on its own tint and its mark stays periwinkle: gold belongs to the orbs, the seed and
 * the pixies, and has no place in the panel (Plan 13 palette). */
.panel-row[aria-expanded="true"] { background: rgba(143, 163, 255, .06); }
.row-glyph { width: 26px; height: 26px; margin-top: .1rem; background: var(--mark-peri); }
.row-text b { display: block; font: 500 1.05rem/1.3 var(--font-head); color: var(--ivory); }
.row-text span { display: block; margin-top: .25rem; color: var(--mist); font-size: .93rem; line-height: 1.5; text-shadow: var(--shadow-text); }
.unfold-back { margin-top: .9rem; padding: .3rem 0; border: 0; background: none; color: var(--mist); font: 500 .74rem/1 var(--font-mono); letter-spacing: .14em; text-transform: uppercase; cursor: pointer; border-bottom: 1px solid var(--silver-dim); }
.unfold-back:hover, .unfold-back:focus-visible { color: var(--ivory); border-bottom-color: var(--silver); }
.unfold-back:focus-visible { outline: 2px solid var(--mist); outline-offset: 4px; }
.reduced .trunk-panel { animation: none; }
/* The boughs' panels (Plan 15 spec 2). Two of them in one station, only ever one open, each headed by the
 * name of the orb that opened it - the trunk has a single panel and needs no such heading. */
.panel-title { margin: 0 0 .2rem; font: 600 .68rem/1 var(--font-body); letter-spacing: .16em; text-transform: uppercase; color: var(--mist); }
/* The card on phones is a fixed bottom sheet capped at 46vh (film.css ~395-396), so the panel keeps the top half. */
.phone .trunk-panel { max-height: 46vh; overflow: auto; }
/* The canopy's list is gone (Plan 19: the roadmap is the lanterns below). The Seed bullet he picked for it
 * (M1, `icons/lit/bullet-seed.png`) is kept as an asset and has no home on the film until he names one. */
.road-link { color: var(--mark-peri); text-decoration: none; font: 500 .8rem/1 var(--font-body); letter-spacing: .14em; text-transform: uppercase; border-bottom: 1px solid rgba(143, 163, 255, .35); padding-bottom: .25rem; }
.road-link:hover, .road-link:focus-visible { color: var(--ivory); border-bottom-color: var(--silver); }
.road-link:focus-visible { outline: 2px solid var(--mist); outline-offset: 2px; }
/* ---- the canopy as lanterns (Plan 19, spec 2026-09-19-canopy-lanterns-design.md) ----
   Four lights up the trunk, each holding its icon, each tethered to a card with the words; a stem of light
   joins them. The layer pins each .lantern at its anchor (a 0x0 box the orb, tether and card hang off) and
   sizes the stem; everything else is here. With GSAP the timeline owns every opacity and transform below
   and the group is simply visible (has-tl); without it the group rides --nodes like the list used to ride
   --body, so the section is there either way. Nothing is clickable but the link out. */
.lantern-head { position: absolute; top: 7vh; left: 50%; width: min(60vw, 34rem); transform: translateX(-50%); text-align: center; pointer-events: none; }
.lantern-head h2 { text-wrap: balance; }
.lanterns { position: absolute; inset: 0; visibility: hidden; pointer-events: none; opacity: var(--nodes, 0); }
.stop.is-on .lanterns { visibility: visible; }
.stop.has-tl .lanterns { opacity: 1; }
.lantern { position: absolute; width: 0; height: 0; --tether: clamp(28px, 3.4vw, 56px); }
/* The light orb of Plan 17 at 56px, no gold ring (gold is the seed's), with the icon laid inside as light.
 * Plan 21: the disc is solid ink under its glow, so the stem that runs up behind the lanterns stops at the
 * rim rather than showing through the icon ("ensure the lead line behind each icon does not show through"),
 * and the icon itself is brighter and carries a glow of its own. */
.lantern-orb {
  position: absolute; left: -28px; top: -28px; width: 56px; height: 56px; border-radius: 50%; display: grid; place-items: center;
  background:
    radial-gradient(circle at 50% 50%, rgba(255, 253, 246, .5) 0, rgba(255, 253, 246, .14) 30%, transparent 56%),
    radial-gradient(circle at 50% 50%, rgba(194, 135, 255, .7) 0, rgba(140, 92, 210, .5) 55%, rgba(90, 60, 150, .5) 82%, rgba(40, 30, 70, .7) 100%),
    rgb(14, 13, 26);
  box-shadow: 0 0 34px 8px rgba(143, 163, 255, .28), inset 0 0 0 1px rgba(207, 199, 216, .45);
  will-change: transform, opacity;
}
.lantern-orb img { width: 38px; height: 38px; filter: brightness(1.25) drop-shadow(0 0 5px rgba(255, 244, 222, .75)) drop-shadow(0 0 12px rgba(214, 166, 255, .55)); }
/* The tether: a hairline from the orb's edge to the card's near edge, drawn from the orb outward. */
.lantern-tether {
  position: absolute; top: 0; height: 1px; width: var(--tether); margin-top: -.5px;
  background: linear-gradient(to right, rgba(200, 184, 219, .55), rgba(200, 184, 219, .55));
}
.lantern[data-side="left"] .lantern-tether { right: 30px; transform-origin: right center; }
.lantern[data-side="right"] .lantern-tether { left: 30px; transform-origin: left center; }
/* The card: the callout's frost, vertically centred on its lantern, out past the tether. The centring is
 * on a wrapper (.lantern-side) and the card is inside it, because GSAP owns the card's transform (the lift
 * is a y tween) and clears anything CSS put there - a translateY(-50%) on the card itself was overwritten,
 * and so was the `translate` property. The wrapper is never tweened, so its centring holds. */
.lantern-side { position: absolute; top: 0; transform: translateY(-50%); width: min(30vw, 22rem); }
/* Plan 21: no box. The words float on the film ("they should float above the page, not within a box"),
 * with the same text shadow every other line of copy on the film has. The padding is the room the tether
 * lands in. */
.lantern-card {
  padding: .2rem .6rem .25rem;
  pointer-events: auto; will-change: transform, opacity;
}
.lantern[data-side="left"] .lantern-side { right: calc(30px + var(--tether)); }
.lantern[data-side="right"] .lantern-side { left: calc(30px + var(--tether)); }
/* The crown card hangs a little from its lantern rather than sitting centred on it: it carries the longest
 * of the four sentences, and centred it climbs into the heading on a short stage (1024x768). */
.lantern:last-of-type .lantern-side { transform: translateY(-18%); }
/* A short stage (1024x768, 1280x800): the lanterns are closer together, so the cards say the same in
 * smaller type and the crown card hangs lower still, clear of both the heading and the card beneath it. */
@media (max-height: 820px) {
  .lantern-head { top: 5vh; }
  .lantern-side { width: min(36vw, 25rem); }
  .lantern-card { padding: .15rem .5rem .2rem; }
  .lantern-card p { font-size: .84rem; line-height: 1.45; }
  .lantern:last-of-type .lantern-side { transform: translateY(-20%); }
}
.lantern-card h3 { font: 500 1.08rem/1.3 var(--font-head); color: var(--ivory); text-shadow: var(--shadow-text); }
.lantern-card p { margin-top: .35rem; color: var(--mist); font-size: .9rem; line-height: 1.5; text-shadow: var(--shadow-text); }
/* The stem: one hairline of periwinkle light between the lowest lantern and the highest, grown from its foot. */
.lantern-stem {
  position: absolute; width: 1px; margin-left: -.5px; transform-origin: 50% 100%;
  background: linear-gradient(to top, transparent, rgba(143, 163, 255, .6) 8%, rgba(143, 163, 255, .6) 92%, transparent);
  box-shadow: 0 0 10px rgba(143, 163, 255, .35);
}
.reduced .lantern-orb, .reduced .lantern-card { will-change: auto; }
/* Phones: a portrait crop has no room beside the trunk, so the lanterns become one column at the foot of
 * the stage, in the copy's place, and the same timeline drives the same properties; only the geometry
 * changes. The stem and the tethers are the desktop's; on a phone the column is the line. */
.phone .lantern-head { top: 8vh; left: 6vw; right: 6vw; width: auto; transform: none; text-align: left; }
.phone .lanterns { inset: auto 6vw 6vh 6vw; display: flex; flex-direction: column; gap: .35rem; max-height: 75vh; overflow: auto; }
.phone .lantern { position: relative !important; left: auto !important; top: auto !important; width: auto; height: auto; display: grid; grid-template-columns: 28px 1fr; gap: .7rem; align-items: start; }
.phone .lantern-orb { position: relative; left: auto; top: auto; width: 28px; height: 28px; box-shadow: 0 0 16px 4px rgba(143, 163, 255, .2); }
.phone .lantern-orb img { width: 18px; height: 18px; }
.phone .lantern-tether, .phone .lantern-stem { display: none; }
.phone .lantern-side { position: relative; left: auto !important; right: auto !important; transform: none !important; width: auto; }
.phone .lantern-card { padding: .1rem 0 .15rem; }
.phone .lantern-card h3 { font-size: .98rem; }
.phone .lantern-card p { font-size: .78rem; line-height: 1.42; }

/* Hidden, not just transparent: a node the station has not held yet must be out of the tab order too. */
.nodes { position: absolute; inset: 0; opacity: calc((var(--nodes, 0) - .5) * 2); visibility: hidden; will-change: opacity; }
.stop.is-held .nodes { visibility: visible; }
/* The pointer comes back on at the buttons, never on the box that holds them. .nodes is only the frame the
 * orbs are positioned in and it covers the whole stage; while it took the pointer itself - and it is
 * painted after the copy, being later in the station - it lay over that column like a sheet of glass, and
 * the boughs' six tool rows, Back, and the close rune on an open card could not be clicked at all. */
.stop.is-held .node, .stop.is-held .seed { pointer-events: auto; }
/* An orb (Plan 13 spec 3.1, remade by Plan 15 spec 4): a 48px sphere of magic built in CSS, in six layers.
 * ::before carries the core (deep violet-indigo lifting to a periwinkle limb), the subsurface scatter that
 * comes back up through it, and the antique-gold edge; .node-spec is the specular and .node-rim the rim
 * light on the far side; ::after is the bloom outside it, breathing on a slow cycle. The button itself is
 * transparent so the bloom can sit under the sphere inside the button's own stacking context (z 0 under
 * z 1), which a negative z-index cannot do.
 *
 * All six are lit from one point rather than each from its own top-left corner. --lx/--ly are a unit vector
 * toward that point, in screen axes (x right, y down), written onto the button by layers/stations.mjs. The
 * value here is the fallback and it is not decoration: hotspots sit on screenshots and are never placed by
 * that layer, phones lay their orbs out in CSS, and a page with no JavaScript never runs it at all. It is
 * the direction the light usually comes from, so an unlit orb looks like the others rather than wrong.
 *
 * The two lit layers are moved with transform, never by shifting a gradient's position: --lx/--ly are
 * rewritten every frame while the camera scrubs, and a gradient that moves with them is a repaint a frame.
 * A transform is not. That is the whole reason they are elements and not more background layers. */
.node, .hotspot, .seed { --lx: -.35; --ly: -.94; }
.node, .hotspot {
  position: absolute; width: 48px; height: 48px; margin: -24px 0 0 -24px; padding: 0; border: 0; border-radius: 50%; cursor: pointer;
  background: none; z-index: 0;
  transition: transform .28s cubic-bezier(.2, .8, .2, 1), opacity .32s ease, visibility 0s linear 0s;
}
/* The orb the canvas falls back to. Plan 16 built this as glass to match a shader that refracted;
 * Plan 17's orbs are light, so this is light too - white at the centre, violet outward, and nothing
 * on a surface, because it has no surface. The carousel's hotspots share this rule and are rings on a
 * screenshot, which is why the ring in the box-shadow stays. */
.node::before, .hotspot::before {
  content: ''; position: absolute; inset: 0; z-index: 1; border-radius: 50%;
  background:
    radial-gradient(circle at 50% 50%, rgba(255, 253, 246, .95) 0, rgba(255, 253, 246, .5) 18%, transparent 42%),
    radial-gradient(circle at 50% 50%, rgba(194, 135, 255, .78) 0, rgba(140, 92, 210, .42) 55%, rgba(90, 60, 150, .12) 82%, transparent 100%);
  box-shadow: inset 0 0 0 1px rgba(211, 165, 74, .55);
  transition: background .3s ease, box-shadow .3s ease;
}
/* The specular and the rim were the glass orb's surface: the light's own reflection on it, and the far
 * limb catching that light around the back. A thing made of light has no surface and so has neither.
 * They stay for the carousel's hotspots, which are rings on a screenshot and were always a different
 * object from a station orb. */
.hotspot .node-spec, .hotspot .node-rim {
  position: absolute; z-index: 1; inset: 0; border-radius: 50%; pointer-events: none;
}
.hotspot .node-spec {
  background: radial-gradient(circle at 50% 50%, rgba(255, 255, 255, .92) 0, rgba(255, 255, 255, .34) 9%, transparent 25%);
  transform: translate(calc(var(--lx) * 18%), calc(var(--ly) * 18%));
}
.hotspot .node-rim {
  background: radial-gradient(circle at 50% 50%, transparent 56%, rgba(143, 163, 255, .55) 82%, transparent 94%);
  transform: translate(calc(var(--lx) * -9%), calc(var(--ly) * -9%));
}
.node .node-spec, .node .node-rim { display: none; }
.node::after, .hotspot::after {
  content: ''; position: absolute; inset: -12px; z-index: 0; border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, rgba(143, 163, 255, .38), rgba(181, 123, 255, .18) 45%, transparent 70%);
  animation: orb-halo 4.6s ease-in-out infinite;
  transition: inset .3s ease, opacity .3s ease;
}
/* Opacity alone. The swell used to ride on transform, which costs nothing to composite but reads as the
 * whole orb pumping; with the sphere lit properly the bloom only has to brighten. */
@keyframes orb-halo { 0%, 100% { opacity: .62; } 50% { opacity: 1; } }
/* The orb layer (Plan 17): a canvas of light, added to the page rather than covering it. The canvas
 * covers the stage and sits above the film, the stars and
 * the sparks; the station buttons come later in the stage and paint over it, which is the order the
 * design asks for. It never takes a click: the button underneath is the control, with its own focus
 * ring, its own Escape and its own name tag. */
.orbs { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; mix-blend-mode: plus-lighter; }
/* With the layer running, the station orbs' painted layers stand down and the canvas draws them
 * instead. Every node selector here names .layer-stations on purpose: the sphere and the bloom are
 * shared with the carousel's hotspots, which are rings on a screenshot and are not drawn by this
 * layer - a bare .node::before rule would take their rings away. The seed is drawn too now (Plan 17),
 * so its gold core, its halo and its breath stand down with the rest; only its root lines stay,
 * because a root line is the drawing and not the orb. */
.orbs-gl .layer-stations .node::before,
.orbs-gl .layer-stations .node::after,
.orbs-gl .layer-stations .seed::before,
.orbs-gl .layer-stations .seed::after { display: none; }
.orbs-gl .layer-stations .seed { box-shadow: none; }
.node:hover, .node:focus-visible, .hotspot:hover, .hotspot:focus-visible { transform: scale(1.1); }
.node:hover::after, .node:focus-visible::after { inset: -18px; animation-duration: 2.2s; }
/* Always in front (spec 3.2): a hovered, focused or open orb rises above its siblings, tag and all. */
.node:hover, .node:focus-visible, .node[aria-expanded="true"] { z-index: 2; }
/* The station orbs carried a mark inside (.node-glyph) from Plan 13 to Plan 20; Plan 21 took them off
 * ("remove the icons from the orbs"), so an orb is its light and its name tag. The three roots orbs are
 * told apart by a tint instead (data-tint, drawn by layers/orbs.mjs; the fallback below). */
.node[data-tint="rose"]::before {
  background:
    radial-gradient(circle at 50% 50%, rgba(255, 253, 246, .95) 0, rgba(255, 250, 246, .5) 18%, transparent 42%),
    radial-gradient(circle at 50% 50%, rgba(232, 140, 205, .78) 0, rgba(180, 92, 170, .42) 55%, rgba(120, 60, 130, .12) 82%, transparent 100%);
}
.node[data-tint="cyan"]::before {
  background:
    radial-gradient(circle at 50% 50%, rgba(250, 253, 255, .95) 0, rgba(246, 252, 255, .5) 18%, transparent 42%),
    radial-gradient(circle at 50% 50%, rgba(150, 170, 255, .78) 0, rgba(100, 120, 220, .42) 55%, rgba(60, 80, 160, .12) 82%, transparent 100%);
}
/* Open: the core and the haze lift, which is the same thing the shader does. */
.node[aria-expanded="true"]::before {
  background:
    radial-gradient(circle at 50% 50%, rgba(255, 255, 252, 1) 0, rgba(255, 253, 246, .72) 26%, transparent 52%),
    radial-gradient(circle at 50% 50%, rgba(214, 166, 255, .9) 0, rgba(160, 112, 228, .5) 58%, rgba(100, 70, 165, .16) 84%, transparent 100%);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .7);
}
.node[aria-expanded="true"]::after { background: radial-gradient(circle, rgba(181, 123, 255, .42), rgba(194, 135, 255, .2) 45%, transparent 70%); inset: -18px; }
/* The name tag (spec 3.2): a small tag on a hairline. Beside the orb by default; above it where the station
 * says so (data-labels="above"); leftward past 70% of the stage (data-side="left"). Hidden until the station
 * says the tags are always on, and hidden while the orb's own card is open (its name is the card's heading). */
.node .node-card, .hotspot span {
  position: absolute; left: calc(100% + 8px); top: 50%; transform: translate(-6px, -50%); width: max-content; max-width: 16rem; pointer-events: none;
  padding: .4rem .6rem; border-radius: 6px; border: 1px solid var(--hair); background: rgba(9, 11, 18, .84); text-align: left;
  opacity: 0; transition: opacity .22s ease, transform .22s ease;
}
.node[data-side="left"] .node-card, .hotspot[data-side="left"] span { left: auto; right: calc(100% + 8px); transform: translate(6px, -50%); }
.stop[data-labels="above"] .node .node-card { left: 50%; right: auto; top: auto; bottom: calc(100% + 8px); transform: translate(-50%, 4px); }
.node .node-card b, .hotspot span b { display: block; font: 600 .68rem/1 var(--font-body); letter-spacing: .16em; text-transform: uppercase; color: var(--ivory); white-space: nowrap; }
/* A hotspot on a screenshot is not an orb: it keeps the hollow ring it has always been, and its bite still
 * carries a teaser line under the title. Only the tree's orbs took the sphere and dropped the teaser. */
.hotspot span em { display: block; margin-top: .3rem; font: italic 400 .95rem/1.35 var(--font-head); color: var(--mist); }
/* An always-on tag is part of the button: hovering it raises the orb, clicking it opens the card. */
.stop[data-labels] .node .node-card { opacity: .9; transform: translate(0, -50%); pointer-events: auto; z-index: 3; } /* above the halo (::after, z 0), which paints later and would wash the tag's near edge */
.stop[data-labels="above"] .node .node-card { transform: translate(-50%, 0); }
.node:hover .node-card, .node:focus-visible .node-card, .hotspot:hover span, .hotspot:focus-visible span { opacity: 1; transform: translate(0, -50%); }
/* An always-on tag is already .9 opaque at a specificity the plain hover rule cannot reach, so the hover
 * has to be said again with the station on the front of it: a hovered orb's tag goes to full. */
.stop[data-labels] .node:hover .node-card, .stop[data-labels] .node:focus-visible .node-card { opacity: 1; }
.stop[data-labels="above"] .node:hover .node-card, .stop[data-labels="above"] .node:focus-visible .node-card { transform: translate(-50%, 0); }
.node[aria-expanded="true"] .node-card, .hotspot[aria-expanded="true"] span { opacity: 0; }
/* Said again over the always-on rule: the open orb's name is the card's heading, so the tag stands down. */
.stop[data-labels] .node[aria-expanded="true"] .node-card { opacity: 0; }
.node:focus-visible, .hotspot:focus-visible { outline: 2px solid var(--mist); outline-offset: 6px; }
/* A panel orb (Plan 15 spec 2): the boughs' two orbs open a panel of three tools rather than a single
 * card, and are half again as wide for it. Every other orb rule reaches them through .node. */
.node-panel { width: 72px; height: 72px; margin: -36px 0 0 -36px; }
.node-panel::after { inset: -16px; }
.node-panel:hover::after, .node-panel:focus-visible::after { inset: -26px; }

/* The roots (spec 4.1): the seed is the glowing point on the mound, a 28px gold core with a violet halo, and a
 * button. Its orbs sprout from it along short root lines (gold, fading toward the seed) and fold back into it
 * on a click: each orb travels the vector the layer wrote (--fx, --fy) and shrinks to nothing. */
.seed {
  position: absolute; width: 28px; height: 28px; margin: -14px 0 0 -14px; padding: 0; border: 0; border-radius: 50%; cursor: pointer; z-index: 3;
  background: none;
  box-shadow: 0 0 22px 6px rgba(211, 165, 74, .45), 0 0 60px 18px rgba(181, 123, 255, .25);
  transition: transform .28s cubic-bezier(.2, .8, .2, 1);
}
/* The breath used to animate the box-shadow itself, which is a main-thread repaint on every frame of an
 * infinite loop - the one non-compositable animation on the film. It is the orb's arrangement instead:
 * the gold core on ::before (z 1), the resting glow still the static box-shadow (painted once), and only
 * the difference between resting and full on ::after (z 0, under the core, over the shadow), which fades
 * and swells on opacity and transform alone. The gradient was fitted against the old keyframe rather than
 * guessed: paused at rest the two are pixel for pixel the same, and at the peak of the breath they differ
 * by 0.7 of one value in 255 on average. Same gold, same violet, same reach, same 3.8s. */
.seed::before {
  content: ''; position: absolute; z-index: 1; inset: 0; border-radius: 50%; pointer-events: none;
  background: radial-gradient(circle, #fff7e0 0, #f2cf7a 30%, #d3a54a 60%, rgba(211, 165, 74, 0) 72%);
}
.seed::after {
  content: ''; position: absolute; z-index: 0; inset: -110px; border-radius: 50%; pointer-events: none;
  /* The resting state, said outright rather than left to the keyframes: with the animation off (reduced
   * motion) an undeclared opacity would be 1 and the seed would sit at the peak of its breath for ever. */
  opacity: 0; transform: scale(.9);
  background: radial-gradient(circle closest-side,
    rgba(209, 163, 83, .138) 0 16%,
    rgba(203, 154, 123, .11) 28%,
    rgba(189, 135, 205, .038) 40%,
    rgba(181, 123, 255, .027) 57%,
    rgba(181, 123, 255, .012) 77%,
    transparent 100%);
  animation: seed-pulse 3.8s ease-in-out infinite;
}
@keyframes seed-pulse { 0%, 100% { opacity: 0; transform: scale(.9); } 50% { opacity: 1; transform: scale(1); } }
.seed:hover, .seed:focus-visible { transform: scale(1.15); }
.seed:focus-visible { outline: 2px solid var(--mist); outline-offset: 6px; }
/* The root line runs from the orb's centre to the seed, so it starts 24px out, at the limb: at z 1 it is
 * above the halo (::after, z 0), which paints later and washed its near end, and the translate keeps it
 * off the sphere (::before, z 1, earlier in tree order, so a line at the centre would cross it). */
.node-root {
  position: absolute; left: 50%; top: 50%; height: 1px; width: calc(var(--root-len, 0px) - 24px); z-index: 1; pointer-events: none;
  transform-origin: 0 50%; transform: rotate(var(--root-ang, 0deg)) translateX(24px);
  background: linear-gradient(90deg, rgba(211, 165, 74, .55), rgba(211, 165, 74, 0));
}
.stop.is-folded .node { opacity: 0; visibility: hidden; pointer-events: none; transform: translate(var(--fx, 0px), var(--fy, 0px)) scale(.2); transition-delay: 0s, 0s, .32s; }

/* The one station that shows the app rather than the tree (spec 4.5): a screen panel opposite the copy,
 * arriving on the body envelope with the copy itself and playing the moment it lands. The figure carries
 * the caption as well as the video now, so the frame - the hairline, the radius and the shadow - sits on
 * the video and the figure keeps out of its way. */
.stop-panel {
  position: absolute; right: 8vw; top: 50%; transform: translateY(-50%); margin: 0;
  width: min(42vw, calc(62vh * 1.6)); border: 0; box-shadow: none; overflow: visible;
  opacity: var(--body, 0); will-change: opacity;
}
.stop-video-el {
  display: block; width: 100%; height: auto;
  border: 1px solid var(--hair); border-radius: 10px; box-shadow: 0 30px 80px -30px #000;
}
/* The crown's facts (Plan 22): the app page's six short facts as a quiet list under the Core copy. */
.stop-facts { margin: 1.1rem 0 0; padding: 0; list-style: none; display: grid; grid-template-columns: 1fr 1fr; gap: .35rem 1.2rem; }
.stop-facts li { position: relative; padding-left: .9rem; font-size: .92rem; line-height: 1.4; color: var(--mist); text-shadow: var(--shadow-text); }
.stop-facts li::before { content: ''; position: absolute; left: 0; top: .6em; width: 4px; height: 4px; border-radius: 50%; background: var(--gold, #d9b25f); box-shadow: 0 0 6px rgba(217, 178, 95, .8); }
/* A card's tool list (Plan 22): every tool of a set, named, two to a row, small under the paragraphs. */
.callout .tools { list-style: none; margin: .8rem 0 0; padding: 0; display: grid; grid-template-columns: 1fr 1fr; gap: .4rem .9rem; }
.callout .tools li { font-size: .86rem; line-height: 1.35; }
.callout .tools b { display: block; font-weight: 500; color: var(--ivory); }
.phone .callout .tools { grid-template-columns: 1fr; }
/* A card capped by callouts.mjs because no slide keeps it off an orb and on the stage (Plan 22): its
 * words scroll inside it, and the picture band scrolls away with them. */
.callout.is-capped { overflow-y: auto; overscroll-behavior: contain; scrollbar-width: thin; padding-bottom: 2rem; -webkit-mask-image: linear-gradient(to bottom, #000 calc(100% - 2rem), transparent); mask-image: linear-gradient(to bottom, #000 calc(100% - 2rem), transparent); }

/* The limbs (spec 4.5): one caption under the moving app. */
.stop-caption { margin-top: .6rem; font: italic 400 .95rem/1.4 var(--font-head); color: var(--mist); text-shadow: var(--shadow-text); }

/* ---- callouts: every bite opens as a card over the film ---- */
.callouts, .callout-src { display: none; }
.callout {
  position: relative; margin-top: .9rem; max-width: 30rem; padding: 1rem 2.8rem .4rem 0;
  border: 0; background: none; pointer-events: auto; animation: callout-in .26s ease; isolation: isolate;
}
/* The card paints its own picture band over ::before, so no scrim is declared here. ::after was the old
 * 40px hairline above the heading; the card hides it, and it is kept only for a callout without .callout-card. */
.callout::after { content: ''; position: absolute; left: 0; top: 0; width: 40px; height: 1px; background: rgba(143, 163, 255, .45); }
.callout h3 { font-size: 1.5rem; margin-bottom: .6rem; outline: none; text-shadow: var(--shadow-text); }
.callout p, .callout li { color: var(--mist); font-size: 1rem; line-height: 1.6; text-shadow: var(--shadow-text); }
.callout p + p { margin-top: .9rem; }
.callout ul { margin: .6rem 0 0; padding-left: 1.1rem; }
.callout-close {
  position: absolute; top: -.4rem; right: -.2rem; width: 36px; height: 36px; padding: 0; border: 0; border-radius: 50%; background: none; cursor: pointer;
  display: grid; place-items: center; color: rgba(249, 244, 245, .6); transition: color .2s;
}
.callout-close:hover, .callout-close:focus-visible { color: var(--ivory); outline: none; }
.callout-close:focus-visible { outline: 2px solid var(--mist); outline-offset: 2px; }
.callout-close .glyph { width: 14px; height: 14px; }
.callout-close .glyph path { fill: none; stroke: currentColor; stroke-width: 1.25; stroke-linecap: round; }
.callout-close .glyph circle { fill: currentColor; }
@keyframes callout-in { from { opacity: 0; transform: translateY(8px); } }
.leader { overflow: visible; z-index: 3; }
.leader[hidden] { display: none; }
.leader path { fill: none; stroke: rgba(143, 163, 255, .75); stroke-width: 1; transition: stroke-dashoffset .35s ease; }
.leader circle { fill: var(--mark-peri); }
/* The bite as a card (Plan 21, his note: "revamp the cards that open from each button"). A picture of
 * the room runs as a band across the top, fading down into the pane; under its lower edge a hairline of
 * light, gold into violet; the node's mark sits on that line in a small ink disc, half on the picture
 * and half on the words; then the heading, the room's own line in the heading face, and the words.
 * Until Plan 20 the picture was a strip fading in from the right, beside the words, which needed a
 * scrim to keep the last of every line off it and still read as words laid over a screenshot.
 *
 * ::before is the band and ::after the hairline, both inside the isolate on .callout, so no card needs
 * more markup than the article it is cloned from. --band is the band's height: 150px where a card has
 * the stage to itself, and 88px in the top slot over the roots, whose card has to end above the orb
 * it hangs from. */
.callout.callout-card {
  --band: 150px;
  --pane: rgb(12, 12, 28);
  min-width: min(360px, 80vw); max-width: 34rem; padding: calc(var(--band) + 1.9rem) 1.3rem 1.2rem; border-radius: 14px;
  background: rgba(12, 12, 28, .78); border: 1px solid rgba(143, 163, 255, .22); backdrop-filter: blur(8px); overflow: hidden;
  box-shadow: 0 24px 60px rgba(5, 5, 7, .55), inset 0 1px 0 rgba(249, 244, 245, .06);
}
.callout-slot.top .callout.callout-card { --band: 88px; }
.callout.callout-card h3 { margin-bottom: .35rem; }
/* The room's own line, the first paragraph, in the heading face: the sentence the app gives the room.
 * first-of-type, not h3 + p: the close button is cloned in between the heading and the paragraph. */
.callout.callout-card p:first-of-type { font: italic 400 1.05rem/1.45 var(--font-head); color: var(--ivory); }
.callout.callout-card p:first-of-type + p { margin-top: .7rem; }
/* The picture has to survive being a near-black screenshot laid over a near-black pane: .75, brightened
 * and a little more saturated to lift the app's dark UI off the card. The fade holds the top 55% whole
 * and lets the bottom of the band go into the pane, so the hairline reads as the picture's own edge. */
.callout.callout-card::before {
  content: ''; position: absolute; z-index: -1; inset: 0 0 auto 0; width: auto; height: var(--band); border-radius: 0;
  background: var(--shot) center / cover no-repeat; opacity: .75; filter: brightness(1.35) saturate(1.15);
  -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent); mask-image: linear-gradient(to bottom, #000 55%, transparent); pointer-events: none;
}
/* The hairline of light under the band: gold at the mark, violet across, gone by the far edge. width and
 * height are said again because the plain .callout::after is a 40x1 rule and an explicit width beats
 * `right`; top is the band's own edge, so the two move together. */
.callout.callout-card::after {
  content: ''; position: absolute; z-index: -1; left: 0; right: 0; top: calc(var(--band) - 1px); width: auto; height: 1px; pointer-events: none;
  background: linear-gradient(to right, rgba(211, 165, 74, .9), rgba(181, 123, 255, .75) 45%, rgba(143, 163, 255, .35) 80%, transparent);
}
/* A hotspot's bite has no screenshot and no mark: it is the words, on the pane, with a short rule of the
 * same light along the top edge so it still belongs to the family. */
.callout.callout-card.no-shot { --band: 0px; padding-top: 1.3rem; }
.callout.callout-card.no-shot::before { display: none; }
.callout.callout-card.no-shot::after { top: 0; right: auto; width: 56px; height: 2px; }
/* Plan 22, his note on the roots and the trunk: "only the writing directly on the page with the image and
 * no box surrounding it. Make it cleaner a little." On a station the card loses its pane: no fill, no
 * edge, no blur, no shadow, nothing clipped. What is left is what the lanterns already are - the words on
 * the film with the film's own text shadow - with the picture floating above them as a soft-cornered
 * band that fades out at its foot, the hairline of light under it, and the mark on its disc on that line.
 * The carousel's hotspot notes keep their pane (he did not name them), and a phone keeps the sheet below,
 * because a sheet at the foot of a screen needs a ground to be a sheet. */
.stop .callout.callout-card {
  background: none; border: 0; backdrop-filter: none; box-shadow: none; border-radius: 0; overflow: visible;
  padding-left: .2rem; padding-right: 2.6rem;
}
.stop .callout.callout-card::before {
  border-radius: 12px; opacity: .85;
  -webkit-mask-image: linear-gradient(to bottom, #000 40%, transparent 96%); mask-image: linear-gradient(to bottom, #000 40%, transparent 96%);
}
.stop .callout.callout-card::after { left: .2rem; right: 2.6rem; }
.stop .callout.callout-card .callout-glyph { left: .9rem; }
/* The close rune stands off the picture's corner now that there is no pane corner to sit in. */
.stop .callout.callout-card .callout-close { top: .2rem; right: 0; }
/* A card with a mark but no picture (the Writing rows since Plan 21: the tools inside the writing room,
 * which have no screen of their own) keeps the disc, in the flow above the heading, and a longer rule. */
.callout.callout-card.no-shot.has-mark { padding-top: 1.4rem; }
.callout.callout-card.no-shot.has-mark::after { width: 120px; }
/* The card clips, so the close rune comes inside its corner: on the picture, so it carries a shadow of its
 * own rather than a plate. */
.callout.callout-card .callout-close { top: .45rem; right: .45rem; z-index: 1; filter: drop-shadow(0 1px 2px rgba(5, 5, 7, .9)); }
/* The mark, in a 44px ink disc ringed in periwinkle, centred on the hairline: half on the picture, half
 * on the words. Absolute, so the heading's own top padding is what clears it. The disc is a wrapper and
 * the mark its child, because .ico masks the element it is on, and a mask on the disc would cut the
 * disc to the mark's own shape. The mark keeps the periwinkle: near-ink on the dark pane would read at
 * about 1.2:1 and vanish. A card with no picture puts the disc back in the flow, above the heading. */
.callout-glyph {
  position: absolute; left: 1.2rem; top: calc(var(--band) - 22px); width: 44px; height: 44px; border-radius: 50%;
  display: grid; place-items: center; background: var(--pane);
  box-shadow: 0 0 0 1px rgba(143, 163, 255, .35), 0 0 18px rgba(143, 163, 255, .25);
}
.callout-glyph .ico { width: 26px; height: 26px; }
.callout.callout-card.no-shot .callout-glyph { position: static; margin: .1rem 0 .9rem; }

/* ---- carousel: six screenshots on a strip, the one in hand centred, its neighbours faded either side ---- */
.layer-carousel { will-change: opacity; }
.layer-carousel:focus-visible { outline: 2px solid var(--mist); outline-offset: -6px; }
.cards { position: absolute; inset: 0; }
.cstop { position: absolute; inset: 0; pointer-events: none; visibility: hidden; }
.layer.is-active .cstop { visibility: visible; }
.cstop-copy { position: absolute; left: 7vw; top: 11vh; max-width: 30rem; opacity: 0; transition: opacity .32s ease; z-index: 4; }
.cstop.is-on .cstop-copy { opacity: 1; }
.cstop-group { font: 600 .7rem/1.6 var(--font-mono); letter-spacing: .16em; text-transform: uppercase; color: var(--mark-peri); text-shadow: var(--shadow-text); }
.cstop-copy h2 { margin-top: .3rem; font-size: clamp(1.8rem, 3.2vw, 2.8rem); text-shadow: var(--shadow-text); }
.cstop-copy p + h2 + p, .cstop-copy h2 + p { margin-top: .9rem; color: var(--mist); text-shadow: var(--shadow-text); }
.cstop .callout-slot { position: absolute; left: 7vw; top: 34vh; width: min(30vw, 30rem); z-index: 4; pointer-events: none; }
.cstop .callout-slot .callout { pointer-events: auto; }
.carousel-live { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
.carousel-nav { position: absolute; left: 50%; bottom: 4vh; z-index: 5; width: 1px; height: 1px; margin: 0; padding: 0; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; background: none; }
.carousel-nav:focus-visible { width: auto; height: auto; padding: .5rem 1rem; clip: auto; transform: translateX(-50%); border: 1px solid var(--mist); border-radius: 999px; background: rgba(9, 11, 18, .84); color: var(--ivory); font: 600 .7rem/1 var(--font-mono); letter-spacing: .14em; text-transform: uppercase; outline: none; cursor: pointer; }
/* The strip: every card is positioned by its slot; --x is the centre offset in percent of the card width. */
.card {
  position: absolute; left: 50%; top: 60%; width: min(62vw, 1040px); margin: 0; pointer-events: auto;
  transform: translate(calc(-50% + var(--x, 0%)), -50%) scale(var(--scale, 1)); opacity: var(--alpha, 1);
  transition: transform .9s cubic-bezier(.45, 0, .55, 1), opacity .9s cubic-bezier(.45, 0, .55, 1);
}
.layer.is-active .card { will-change: transform, opacity; }
.card[data-slot="0"] { z-index: 3; }
.card[data-slot="1"] { z-index: 2; cursor: pointer; }
.card[data-slot="-1"] { z-index: 2; cursor: pointer; }
.card[data-slot="2"], .card[data-slot="3"] { z-index: 1; pointer-events: none; }
.card[data-slot="-2"] { z-index: 1; pointer-events: none; }
.card-frame { position: relative; }
.card img {
  width: 100%; height: auto; aspect-ratio: 16 / 10; object-fit: cover; border-radius: 6px;
  border: 1px solid rgba(249, 244, 245, .18); box-shadow: 0 30px 80px rgba(5, 5, 7, .8), 0 0 60px rgba(21, 16, 107, .3);
}
/* Hotspots: a hollow ring, distinct from the tree's silver discs; live on the centre card only. */
.hotspot {
  margin: 0; transform: translate(-50%, -50%); width: 11px; height: 11px;
  background: none; box-shadow: inset 0 0 0 1.5px rgba(249, 244, 245, .85);
}
/* Neither the orb's sphere nor its halo: a hotspot is a ring on a screenshot, and stays one. */
.hotspot::before, .hotspot::after { display: none; }
/* Centred by a transform rather than a margin, so the grow has to carry the centring with it. */
.hotspot:hover, .hotspot:focus-visible { transform: translate(-50%, -50%) scale(1.15); background: radial-gradient(circle at 32% 30%, var(--mark-cyan) 0%, var(--mark-peri) 45%, var(--mark-violet) 100%); box-shadow: none; }
.hotspot[aria-expanded="true"] { background: var(--ivory); box-shadow: none; }
.card:not([data-slot="0"]) .hotspot { pointer-events: none; visibility: hidden; }

/* ---- phones ---- */
.phone .beat-hero h1 { font-size: 2.2rem; }
.phone .hero-line { padding-bottom: .9rem; }
.phone .beat-hero { display: flex; flex-direction: column; justify-content: center; }
.phone .hero-lower { align-self: center; margin-bottom: 0; margin-top: 6vh; }
.phone .hero-actions { flex-direction: column; gap: .9rem; }
.phone .beat-line { bottom: 12vh; font-size: 1.4rem; }
.phone .beat-finale { left: 6vw; right: 6vw; bottom: 12vh; max-width: none; }
.phone .stop-copy { top: auto; bottom: 6vh; left: 6vw; right: 6vw; width: auto; transform: none; }
.phone .stop[data-side="right"] .stop-copy { left: 6vw; right: 6vw; }
.phone .stop h2 { font-size: 1.5rem; }
.phone .stop p { font-size: .95rem; }
.phone .stop p + p { margin-top: .8rem; }
.phone .nodes { inset: auto 6vw auto 6vw; top: 11vh; display: flex; flex-wrap: wrap; column-gap: .4rem; row-gap: 2.6rem; }
/* A phone crops most of the frame's width away, so its nodes are a row of marks rather than pips on the
 * bark: in the flow, and positioned only so the title has something to hang off. The side margins buy each
 * ring the width of its own title, so the titles sit clear of one another. */
.phone .node { position: relative; width: 32px; height: 32px; margin: 0 1.1rem; }
.phone .node::after { inset: -8px; }
.phone .node-panel { width: 48px; height: 48px; margin: 0 1.1rem; }
.phone .node-panel::after { inset: -10px; }
.phone .node-root, .phone .seed { display: none; }
/* Touch has no hover, so the name is always drawn, under the orb, plain. */
.phone .node .node-card, .phone .stop[data-labels] .node .node-card {
  left: 50%; right: auto; top: calc(100% + .3rem); bottom: auto; transform: translateX(-50%); width: 4.1rem; max-width: none;
  padding: 0; border: 0; background: none; text-align: center; opacity: 1;
}
.phone .node .node-card b { font: 600 .58rem/1.3 var(--font-mono); letter-spacing: .1em; color: var(--mist); white-space: normal; }
/* A tap leaves the orb hovered, and the grow would drag its name over its neighbour's. */
.phone .node:hover { transform: none; }
.phone .stop.is-folded .node { transform: none; }
.phone .callout { max-height: 46vh; overflow: auto; }
.phone .callout.callout-card {
  --band: 120px; position: fixed; left: 0; right: 0; bottom: 0; min-width: 0; max-width: none; border-radius: 16px 16px 0 0;
  /* The sheet keeps the pane the desktop stations gave up (Plan 22): said again here because the station
   * rule above takes it away, and a sheet on the foot of a phone needs a ground. */
  padding-left: 1.3rem; padding-right: 1.3rem; overflow: hidden;
  background: rgba(12, 12, 28, .9); border-top: 1px solid rgba(143, 163, 255, .22); box-shadow: 0 -24px 60px rgba(5, 5, 7, .55);
}
.phone .callout.callout-card::before { border-radius: 0; -webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent); mask-image: linear-gradient(to bottom, #000 55%, transparent); }
.phone .callout.callout-card::after { left: 0; right: 0; }
.phone .callout.callout-card .callout-glyph { left: 1.2rem; }
.phone .callout.callout-card .callout-close { top: .45rem; right: .45rem; }
/* Since Plan 21 the desktop card is the same shape as the sheet - a band across the top, the words under
 * it - so the phone changes nothing about the picture or the hairline. */
.phone .callout.callout-card::before { opacity: .6; }
.phone .stop > .callout-slot.far, .phone .stop > .callout-slot.top { display: none; }
/* On a phone the panel belongs back in the flow of the copy, like everything else. */
.phone .stop > .bough-side { position: static; width: auto; }
.phone .cstop-copy { top: 10vh; left: 6vw; right: 6vw; max-width: none; }
.phone .cstop .callout-slot { left: 6vw; right: 6vw; top: auto; bottom: 12vh; width: auto; }
.phone .card { width: 84vw; top: 46%; }
.phone .hotspot { display: none; }

/* On a phone the screen panel needs the width, so this one station reads top down: copy, then picture. */
.phone .stop-video .stop-copy { top: 8vh; bottom: auto; transform: none; }
.phone .stop-panel { top: auto; bottom: 8vh; left: 6vw; right: 6vw; width: auto; transform: none; }
.phone .stop-caption { font-size: .85rem; }

/* ---- reduced motion ---- */
.reduced .screen, .reduced .sparks, .reduced .loop { display: none; } /* .ambient stays: the hero draws one still frame; .stars draws still */
.reduced .stills { display: block; }
.reduced .stop h2, .reduced .nodes, .reduced .beat, .reduced .stop-panel { transition: opacity .6s ease; }
.reduced .stop-body, .reduced .callout, .reduced .card, .reduced .cstop-copy { animation: none; transition: none; }
.reduced .leader path { transition: none; }
.reduced .node, .reduced .hotspot, .reduced .seed, .reduced .node::before, .reduced .node::after, .reduced .hotspot::after, .reduced .seed::after { transition: none; animation: none; }
/* The bloom's keyframe is the only thing that moves on an orb, and with it off an undeclared opacity would
 * be 1: the resting value has to be said outright or every orb sits at the peak of its breath for ever. */
.reduced .node::after, .reduced .hotspot::after { opacity: .8; }
.reduced .node .node-card, .reduced .hotspot span { transition: none; }

/* ---- the ground's grain (Plan 11) ----
 * Patrick asked for nicer textures and for the hero to read like his app's splash, which is stone
 * rather than a flat fill. A 128px noise tile at very low opacity over the whole stage does it: it
 * costs one 21KB image, it tiles seamlessly, and it sits under every beat so the type stays clean.
 * soft-light keeps it from lifting the blacks the way a plain overlay would. */
.stage::after {
  content: ''; position: absolute; inset: 0; z-index: 3; pointer-events: none;
  background-image: url('../brand/grain.png');
  background-size: 128px 128px;
  opacity: .055; mix-blend-mode: soft-light;
}
.reduced .stage::after { opacity: .04; }
