/* 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); }
.stage { position: sticky; top: 0; height: 100vh; 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);
}
.hero-blurb { margin-top: 1rem; max-width: 44rem; color: var(--mist); text-shadow: var(--shadow-text); }
.hero-actions { margin-top: 2.2rem; 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; }
.beat-finale h2 { margin: .8rem 0 .6rem; font-size: clamp(2rem, 3.6vw, 3.2rem); text-shadow: var(--shadow-text); }
.finale-lore { margin-bottom: 1.6rem; 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; }
.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. */
.stop > .callout-slot.far { position: absolute; top: 50%; right: 7vw; width: min(38vw, 30rem); transform: translateY(-50%); pointer-events: none; }
.stop[data-side="right"] > .callout-slot.far { right: auto; left: 7vw; }
.callout-slot.far .callout { 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 (spec 4.4): read, do not click. Four lines with a diamond, and one link out to /about#road. */
.canopy-list { margin: 1.2rem 0 0; padding: 0; list-style: none; }
.canopy-list li { position: relative; padding-left: 1.3rem; color: var(--mist); font-size: .97rem; line-height: 1.55; text-shadow: var(--shadow-text); }
.canopy-list li + li { margin-top: .7rem; }
.canopy-list li::before { content: ''; position: absolute; left: 2px; top: .58em; width: 5px; height: 5px; background: var(--mark-peri); transform: rotate(45deg); }
.canopy-list b { color: var(--ivory); font-weight: 500; }
.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; }
/* 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;
}
.node::before, .hotspot::before {
  content: ''; position: absolute; inset: 0; z-index: 1; border-radius: 50%;
  background:
    radial-gradient(circle at 50% 66%, rgba(143, 163, 255, .5) 0, rgba(107, 91, 184, .22) 42%, transparent 68%),
    radial-gradient(circle at 50% 50%, #2a1a4d 0, #3b2470 38%, #6b5bb8 78%, #8fa3ff 100%);
  box-shadow: inset 0 0 0 1px rgba(211, 165, 74, .55), inset 0 -6px 12px rgba(0, 0, 0, .45);
  transition: background .3s ease, box-shadow .3s ease;
}
/* The specular: the light's own reflection on the surface, small and hard, sitting on the side the light is
 * on. The rim light: the far limb catching it around the back, which is what stops a sphere reading as a
 * flat disc. Both are inside the sphere's circle and under the mark, so the mark stays legible. */
.node-spec, .hotspot .node-spec, .node-rim, .hotspot .node-rim {
  position: absolute; z-index: 1; inset: 0; border-radius: 50%; pointer-events: none;
}
.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%));
}
.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::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 refracting orbs (Plan 16). 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; }
/* With the layer running, the station orbs' painted layers stand down and the canvas draws them
 * instead. Every selector here names .layer-stations on purpose: the sphere, the bloom, the specular
 * and the rim are shared with the carousel's hotspots, which are rings on a screenshot and are not
 * drawn by this layer - a bare .node-spec rule would take their rings away. The seed is not named at
 * all: it is not in the anchor table, and the layer has no position for it. */
.orbs-gl .layer-stations .node::before,
.orbs-gl .layer-stations .node::after,
.orbs-gl .layer-stations .node .node-spec,
.orbs-gl .layer-stations .node .node-rim { display: 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 mark inside: near-white silver at reduced opacity, full on hover; the heavier trace (icons/bold). */
.node-glyph {
  position: absolute; z-index: 2; inset: 11px; width: auto; height: auto; background: rgba(238, 234, 248, .72); pointer-events: none;
  transition: background .25s ease;
}
.node:hover .node-glyph, .node:focus-visible .node-glyph { background: var(--ivory); }
/* Open: the orb turns gold-white and the mark goes dark so it still reads. */
.node[aria-expanded="true"]::before {
  background:
    radial-gradient(circle at 50% 66%, rgba(255, 246, 226, .55) 0, rgba(240, 223, 176, .25) 42%, transparent 68%),
    radial-gradient(circle at 50% 50%, #fff6e2 0, #f0dfb0 45%, #d3a54a 100%);
  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, .7), inset 0 -6px 12px rgba(120, 80, 20, .35);
}
.node[aria-expanded="true"] .node-rim { background: radial-gradient(circle at 50% 50%, transparent 56%, rgba(255, 246, 226, .6) 82%, transparent 94%); }
.node[aria-expanded="true"]::after { background: radial-gradient(circle, rgba(211, 165, 74, .5), rgba(181, 123, 255, .25) 45%, transparent 70%); inset: -18px; }
.node[aria-expanded="true"] .node-glyph { background: #1a1626; }
/* 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 .node-glyph { inset: 17px; }
.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 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: the words on the left, a picture of the room fading in from the right. The band sits
 * at z-index: -1, under the words, inside the isolate on .callout. */
.callout.callout-card {
  /* --shot-room is the strip the picture is given on the right. Fixed at 8.5rem while the card has room
   * for it - which is every card at the reference sizes - and a share of a narrower one, so a card the
   * orbs have squeezed spends what is left on words rather than on a picture nobody can see. */
  --shot-room: min(8.5rem, 32%);
  min-width: min(360px, 80vw); max-width: 34rem; padding: 1.2rem var(--shot-room) 1.1rem 1.2rem; border-radius: 12px;
  background: rgba(12, 12, 28, .78); border: 1px solid rgba(143, 163, 255, .22); backdrop-filter: blur(8px); overflow: hidden;
}
/* The picture has to survive being a near-black screenshot laid over a near-black pane. At .4 it was
 * spec-compliant on the number and invisible in the result, so the number moved: .75, brightened and a
 * little more saturated to lift the app's dark UI off the card, and the fade held back to 55% so more
 * than the last sliver of the shot comes through. Spec section 6 asks for half words and half picture. */
.callout.callout-card::before { content: ''; position: absolute; z-index: -1; inset: 0 0 0 auto; width: 55%; 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 left, #000 55%, transparent); mask-image: linear-gradient(to left, #000 55%, transparent); pointer-events: none; }
/* The scrim between the picture and the words. The far slot caps the card at 480px, so the text column
 * ends where the right padding starts while the band's mask is already fully opaque there: without this
 * the last of every line sat on a brightened screenshot. Both pseudos sit at z-index -1 inside the card's
 * own stacking context (backdrop-filter), and ::after paints after ::before, so this lands between the
 * picture and the text. The solid stop is the right padding itself, so the two move together. width/height
 * are said again because the plain .callout::after is a 40x1 hairline and an explicit width beats `right`. */
.callout.callout-card::after {
  content: ''; position: absolute; z-index: -1; inset: 0; width: auto; height: auto; pointer-events: none;
  background: linear-gradient(to right, rgb(12, 12, 28) 0 calc(100% - var(--shot-room)), transparent 100%);
}
/* A hotspot's bite has no screenshot of its own, so it keeps no room for one - and no scrim either, which
 * with nothing under it would only be a dark band fading across the card. */
.callout.callout-card.no-shot { padding-right: 1.2rem; }
.callout.callout-card.no-shot::after { display: none; }
/* The card clips, so the close rune comes inside its corner. */
.callout.callout-card .callout-close { top: .45rem; right: .45rem; }
/* The card wears the same mark, bigger. It sits on the dark pane rather than on silver, so it keeps the
 * periwinkle: near-ink here would read at about 1.2:1 against the card and vanish. */
.callout-glyph {
  display: block; width: 32px; height: 32px; margin-bottom: .6rem; background: var(--mark-peri);
  filter: drop-shadow(0 1px 0 rgba(0, 0, 0, .5));
}

/* ---- 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 .hero-blurb { font-size: .95rem; }
.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-glyph { inset: 7px; }
.phone .node::after { inset: -8px; }
.phone .node-panel { width: 48px; height: 48px; margin: 0 1.1rem; }
.phone .node-panel .node-glyph { inset: 11px; }
.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 { position: fixed; left: 0; right: 0; bottom: 0; min-width: 0; max-width: none; border-radius: 16px 16px 0 0; padding: 130px 1.2rem 1.2rem; }
.phone .callout.callout-card::before { inset: 0 0 auto 0; width: auto; height: 120px; opacity: .6; mask-image: linear-gradient(to bottom, #000 60%, transparent); -webkit-mask-image: linear-gradient(to bottom, #000 60%, transparent); }
/* On a phone the picture is a band across the top of the sheet, with the words under it rather than
 * beside it, so there is nothing for a left-to-right scrim to protect. */
.phone .callout.callout-card::after { display: none; }
.phone .stop > .callout-slot.far { display: none; }
.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; }
