/* Set pieces.
   The page was reading as one card template repeated six times, so each
   section below has its own geometry: a dark stage for the worked example,
   a single horizontal run for the path, a moving strip for the live markets.
   Loaded after site.css so it wins where the two overlap. */

/* ---------- bigger glass bar ---------- */
.topbar { padding-top: 1.4%; }
.topbar-left, .topbar-right { height: 54px; }
.icon-btn { width: clamp(44px, 3.6vw, 56px); height: clamp(44px, 3.6vw, 56px); }
.icon-grid { width: clamp(40px, 3.1vw, 50px); height: clamp(40px, 3.1vw, 50px); }
.pill-tray { height: clamp(44px, 3.4vw, 54px); padding: 0 6px; gap: 4px; }
.pill-tray a { padding: 0 18px; font-size: clamp(13px, 1.05vw, 15.5px); }
.pill-white { height: clamp(44px, 3.4vw, 54px); padding: 0 20px; font-size: clamp(13px, 1.05vw, 15.5px); }
.topbar .ca { height: clamp(44px, 3.4vw, 54px); padding: 0 16px; font-size: clamp(11px, .95vw, 13px); }
.wordmark-lockup { font-size: clamp(18px, 1.7vw, 23px); }
.wordmark-lockup img { width: clamp(24px, 2.4vw, 30px); height: clamp(24px, 2.4vw, 30px); }
/* corners at half their old roundness (user, 2026-09-22). Every piece was a full pill, radius =
   height / 2; now height / 4. The links sit at 76% of the tray inside its 1px border. */
.icon-btn, .icon-grid { border-radius: 25%; }
.pill-tray, .pill-white, .topbar .ca { border-radius: calc(clamp(44px, 3.4vw, 54px) / 4); }
.pill-tray a { border-radius: calc((clamp(44px, 3.4vw, 54px) - 2px) * .76 / 4); }

/* the stray marker over the film is gone */

/* ---------- the worked example ---------- */
/* Flat #0b0c0e reads as a fill. A shallow gradient and one hairline at the
   top edge read as a surface with a light on it, at no cost to the palette. */
.stage {
  background: linear-gradient(180deg, #131417 0%, #0b0c0e 46%, #08090b 100%);
  color: #fff; padding-block: clamp(60px, 7.5vw, 116px); overflow: hidden;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
}
.stage h2, .stage .sub { color: #fff; }
.stage .sub { color: rgba(255,255,255,.62); }
/* the worked example: one still of three devices holding real captures of the
   app, baked offline (tools in the session scratchpad) so nothing here is live 3D */
/* sized so the heading, the phones and the lines under them fit one screen */
/* the -1px overlap: at 125% and 150% display scaling the join with the hero falls
   on a fractional pixel, neither section paints that row fully, and the white
   curtain behind them shows as a hairline. Overlapping by one pixel leaves no gap. */
.see-still {
  background: linear-gradient(180deg, #131417 0%, #0b0c0e 46%, #08090b 100%);
  padding-top: clamp(64px, 8vh, 96px); scroll-margin-top: 40px; margin-top: -1px;
  box-shadow: inset 0 1px 0 rgba(255,255,255,.07);
}
.see-head { margin: 0 auto; max-width: 30em; text-align: center; text-wrap: balance; }
/* The height term keeps the phones inside a tall viewport, but on its own it
   also decided the width on any short one: a maximised window on a 1366x768
   laptop left a 377px composite in a 1366px band. The floor stops that; the
   section scrolls when it has to, which is better than unreadable. */
.still { margin: clamp(18px, 2.4vw, 34px) auto 0; width: min(1080px, 100%, max(620px, (100svh - 350px) * 1.341)); }
.still-stage { position: relative; aspect-ratio: 2360 / 1760; }
.still-layer { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
.still-single { display: none; width: 100%; height: auto; }

/* The entrance. The front phone rises into place; the other two start hidden
   behind it and slide out to their places; the lines under them follow. */
.still-centre { opacity: 0; transform: translateY(5%) scale(.97);
  transition: opacity .55s ease, transform .9s cubic-bezier(.16,1,.3,1); }
.still-left, .still-right { opacity: 0;
  transition: opacity .45s ease .22s, transform 1s cubic-bezier(.16,1,.3,1) .22s; }
.still-left { transform: translateX(17%) scale(.94); }
.still-right { transform: translateX(-17%) scale(.94); }
.still-single { opacity: 0; transform: translateY(6%) scale(.97);
  transition: opacity .55s ease, transform .9s cubic-bezier(.16,1,.3,1); }
.still.in .still-layer, .still.in .still-single { opacity: 1; transform: none; }
.still-notes li { opacity: 0; transform: translateY(12px);
  transition: opacity .5s ease, transform .6s cubic-bezier(.2,.7,.2,1); transition-delay: calc(.62s + var(--i, 0) * 90ms); }
.still.in .still-notes li { opacity: 1; transform: none; }
/* the heading, quick */
.see-head { opacity: 0; transform: translateY(14px);
  transition: opacity .4s ease, transform .5s cubic-bezier(.2,.7,.2,1); }
.see-still.in .see-head { opacity: 1; transform: none; }
/* one line under each phone: the phones sit at 25, 50 and 75 percent of the
   image, so three equal columns across the middle 75 percent line up with them */
.still-notes { list-style: none; margin: clamp(-26px, -1.6vw, -10px) auto 0; padding: 0; width: 75%;
  display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: clamp(14px, 2vw, 30px); }
.still-notes li { display: grid; justify-items: start; align-content: start; gap: 8px; text-align: left; }
.still-notes b { font-family: var(--mono); font-weight: 400; font-size: 11.5px; letter-spacing: .06em; color: rgba(255,255,255,.34); }
.still-notes p { margin: 0; max-width: 30ch; font-size: clamp(13px, 1.1vw, 15px); line-height: 1.5; color: rgba(255,255,255,.7); }
/* Inside the figure now, so 75% resolves against the picture and the note
   opens on the same left edge as line 01. Two columns hold it near 40
   characters a line; across the full shell it was running to 165. */
.see-still .demo-note-mid {
  margin: clamp(22px, 2.6vw, 36px) auto 0; width: 75%; max-width: none; text-align: left;
  columns: 2; column-gap: clamp(14px, 2vw, 30px);
}
@media (max-width: 900px) {
  .see-still .demo-note-mid { width: 100%; columns: 1; }
}

.demo-note { margin: clamp(4px, .6vw, 10px) 0 0; }
.walk { list-style: none; margin: clamp(4px, .6vw, 10px) 0 0; padding: 0; display: grid; gap: clamp(15px, 1.7vw, 23px); max-width: 54ch; }
.walk li { display: grid; grid-template-columns: 26px minmax(0, 1fr); gap: 14px; align-items: start; }
.walk li > b { font-family: var(--mono); font-weight: 400; font-size: 11.5px; letter-spacing: .06em; color: rgba(255,255,255,.34); padding-top: .42em; }
.walk p { margin: 0; font-size: clamp(14px, 1.3vw, 17px); line-height: 1.5; color: rgba(255,255,255,.7); }
.walk .hi { color: #fff; font-weight: 500; }
.walk .hi.mono { font-family: var(--mono); font-weight: 400; font-size: .94em; }

.demo-note { margin-top: 20px; font-size: 12px; line-height: 1.6; color: rgba(255,255,255,.42); max-width: 78ch; }

/* ---------- the path ---------- */
.path { position: relative; margin-top: clamp(40px, 5vw, 76px); }
.path-run { position: relative; height: 3px; background: var(--line); border-radius: 3px; }
.path-run::after { content: ""; position: absolute; inset: 0; width: 0; background: var(--accent); border-radius: 3px; transition: width 2s cubic-bezier(.3,.7,.2,1); }
.path.in .path-run::after { width: 100%; }
.path-marks { display: grid; grid-template-columns: repeat(5, 1fr); margin-top: -9px; }
.path-mark { display: grid; justify-items: start; gap: 12px; }
.path-dot { width: 15px; height: 15px; border-radius: 50%; background: #fff; border: 3px solid var(--line); transition: border-color .4s ease, transform .4s cubic-bezier(.2,.7,.2,1); }
.path-mark.is-on .path-dot { border-color: var(--accent); transform: scale(1.14); }
.path-label { font-size: clamp(12px, 1.1vw, 14.5px); color: var(--ink-3); max-width: 16ch; transition: color .4s ease; }
.path-mark.is-on .path-label { color: var(--ink); }
.path-ours { margin-top: clamp(34px, 4vw, 58px); display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: clamp(12px, 1.6vw, 24px); }
.path-ours > div { background: var(--accent); color: #fff; border-radius: var(--r-card); padding: clamp(22px, 2.6vw, 36px); font-size: clamp(22px, 2.9vw, 40px); font-weight: 600; letter-spacing: -0.028em; opacity: 0; transform: translateY(20px); }
.path.in .path-ours > div { opacity: 1; transform: none; transition: opacity .6s ease 1.5s, transform .7s cubic-bezier(.2,.7,.2,1) 1.5s; }
.path.in .path-ours > div:last-child { transition-delay: 1.75s; }

/* ---------- the live strip ---------- */
.strip { overflow: hidden; padding-block: clamp(52px, 5.6vw, 84px); background: var(--grey); }
/* A tape should run off both edges rather than stop at them. The mask does
   that without a gradient overlay that would have to know the ground colour. */
.strip-vp {
  -webkit-mask-image: linear-gradient(90deg, transparent 0, #000 7%, #000 93%, transparent 100%);
  mask-image: linear-gradient(90deg, transparent 0, #000 7%, #000 93%, transparent 100%);
}
.strip-head { display: flex; justify-content: space-between; align-items: baseline; gap: 20px; margin-bottom: clamp(20px, 2.4vw, 34px); }
@media (max-width: 560px) {
  /* side by side these two left the heading about 103px wide */
  .strip-head { display: grid; gap: 6px; }
}
.strip-track { display: flex; gap: 30px; width: max-content; animation: slide 70s linear infinite; }
.strip:hover .strip-track { animation-play-state: paused; }
@keyframes slide { to { transform: translateX(-50%); } }
/* No fill and no border: a hairline between entries is all a tape needs, and
   it stops the row reading as a carousel of cards. */
.strip-card {
  display: grid; gap: 2px; min-width: 232px; padding: 2px 30px 2px 0;
  border-right: 1px solid var(--line); background: none;
}
.strip-sym { font-size: 12px; font-weight: 500; letter-spacing: .07em; text-transform: uppercase; color: var(--ink-3); }
.strip-name { font-size: 16px; font-weight: 500; letter-spacing: -0.018em; white-space: nowrap; color: var(--ink); }
.strip-px { font-size: 14px; color: var(--ink-2); font-variant-numeric: tabular-nums; }
.strip-card.is-none .strip-sym { color: var(--ink-3); }

@media (max-width: 860px) {
  .topbar { padding: clamp(10px, 2.4vw, 16px) var(--gutter) 0; }
  .topbar-left, .topbar-right { height: 46px; }
  .still { width: min(340px, 86vw); }
  .still-stage { display: none; }
  .still-single { display: block; }
  .still-notes { width: 100%; margin-top: 8px; grid-template-columns: minmax(0, 1fr); gap: 14px; }
  .still-notes li { grid-template-columns: 26px minmax(0, 1fr); justify-items: start; text-align: left; gap: 12px; }
  .still-notes b { padding-top: .35em; }
  .still-notes p { max-width: none; }
  .path-marks { grid-template-columns: 1fr; gap: 18px; margin-top: 16px; }
  .path-run { display: none; }
  .path-mark { grid-template-columns: 15px 1fr; align-items: center; justify-items: stretch; }
  .path-label { max-width: none; }
  .path-ours { grid-template-columns: minmax(0, 1fr); }
}

@media (prefers-reduced-motion: reduce) {
  .strip-track { animation: none !important; }
}


/* the bar floats over both dark film and light sections, so it repaints */
.topbar.on-light .wordmark-lockup { color: var(--ink); text-shadow: none; }
/* Over light content the bar sits on top of body text, and the wordmark is
   the only piece of it without a backdrop. It takes the tray's glass so the
   text can pass underneath without either becoming unreadable. Over the dark
   hero it stays bare, which is the look that was designed. */
.topbar.on-light .wordmark-lockup {
  padding: 5px 12px 5px 8px; border-radius: calc(clamp(44px, 3.4vw, 54px) / 4);
  background: rgba(255,255,255,.74); border: 1px solid rgba(0,0,0,.07);
  backdrop-filter: blur(22px) saturate(150%); -webkit-backdrop-filter: blur(22px) saturate(150%);
}
/* the mark is its own artwork now, not a silhouette to be recoloured */
.topbar.on-light .pill-tray { background: rgba(255,255,255,.72); border-color: rgba(0,0,0,.08); }
.topbar.on-light .pill-tray a { color: var(--ink-2); }
.topbar.on-light .pill-tray a:hover { color: var(--ink); background: rgba(0,0,0,.05); }
.topbar.on-light .pill-tray a.is-current { background: var(--ink); color: #fff; }
.topbar.on-light .pill-white { box-shadow: 0 1px 2px rgba(0,0,0,.12); }
.topbar.on-light .icon-btn { background: #1d1d1f; color: #e8e8ea; }

/* the two runs of the wordmark butt together, no gap */
.wordmark-lockup { gap: 10px; }


/* ---------- the page body ----------
   This used to be a curtain: a white panel pulled up over a scroll-scrubbed
   film by a whole viewport height. The film is gone (user, 2026-09-23:
   "remove the icicle/motion background completely"), so the opening screen is
   simply the first thing on the page and this is a plain wrapper. The pull-up,
   the rounded top edge and the shadow that sold the overlap all went with it. */
.after {
  position: relative; z-index: 2;
  background: var(--white);
}


/* the phone shows the product interface, not a drawing of an object */


/* ---------- headings: Geist Sans 700, tight ----------
   One heading voice across the site, per the owner: geometric grotesk, heavy,
   tracking -0.04em, leading just under 1. */
:root { --display: "Geist", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
h1, h2, h3,
.t-hero, .t-1, .t-2, .t-3,
.stage h2, .pane h3 {
  font-family: var(--display);
  font-weight: 700;
  letter-spacing: -0.04em;
  line-height: .97;
}
.t-hero { line-height: .95; }
.t-1 { line-height: .96; }
.t-2, .t-3 { line-height: 1.0; letter-spacing: -0.035em; }
.pane h3 { letter-spacing: -0.03em; line-height: 1.05; }

/* ---------- text is simply there ----------
   No fade, rise or pop-in on any text (user, 2026-09-22: "i dont like the text pop up/fade in
   animation"). Headings, subheads, notes, tier cards, chain rows, the statement cards and the
   footer are visible from the first frame; pictures (the phone stills, the tiles) keep their own
   motion. site.js still adds .in, which now changes nothing here. */
.rise, [data-stagger] > *, .see-head, .still-notes li, .path-ours > div {
  opacity: 1; transform: none; transition: none;
}


/* =========================================================================
   Spottr: the mechanic, said twice.

   Once as a rail under the hero promise, so the loop is legible before a
   visitor scrolls at all, and once in full on the band below it. Both mark
   the two steps that are not built, because the page must not read as though
   a photo already pays. Nothing here is decorative: the "Not built" state is
   the honest half of the design, so it is styled to be noticed, not hidden.
   ========================================================================= */

/* ---------- the rail under the hero ---------- */
.loop-rail {
  list-style: none; margin: clamp(38px, 5.4vh, 60px) 0 0; padding: 0;
  display: flex; flex-wrap: wrap; align-items: center; justify-content: center;
  gap: 8px 6px; max-width: 860px;
}
.loop-rail li { display: flex; align-items: center; gap: 6px; }
/* the arrow between steps, drawn in CSS so there is no icon to load */
.loop-rail li:not(:last-child)::after {
  content: ""; width: 12px; height: 1px; background: rgba(221,223,226,.34);
}
/* Wrapped onto three lines, every connector that lands at a line end points
   at nothing, so below this width the steps read as a set instead. */
@media (max-width: 700px) {
  .loop-rail li::after { display: none; }
  .loop-rail { gap: 8px; }
}
.loop-rail li > span {
  display: inline-flex; align-items: center; height: 30px; padding: 0 12px;
  border-radius: var(--r-chip, 10px); border: 1px solid rgba(255,255,255,.12);
  background: rgba(255,255,255,.05);
  backdrop-filter: blur(14px) saturate(160%); -webkit-backdrop-filter: blur(14px) saturate(160%);
  box-shadow: inset 0 1px 0 rgba(255,255,255,.09);
  font-size: 12.5px; font-weight: 500; letter-spacing: -.005em;
  color: rgba(235,236,238,.82); white-space: nowrap;
}
/* the two that do not run yet are dimmed and dashed, never coloured in */
.loop-rail li.is-soon > span {
  border-style: dashed; border-color: rgba(255,255,255,.34);
  background: none; color: rgba(221,223,226,.62);
}

/* the rail rises with the rest of the opening screen */
.loop-rail li { clip-path: inset(0 0 100% 0); transform: translateY(.6em); }
.open-screen.text-in .loop-rail li { animation: riseLine .95s cubic-bezier(.16,1,.3,1) both; }
.open-screen.text-in .loop-rail li:nth-child(1) { animation-delay: .62s; }
.open-screen.text-in .loop-rail li:nth-child(2) { animation-delay: .66s; }
.open-screen.text-in .loop-rail li:nth-child(3) { animation-delay: .70s; }
.open-screen.text-in .loop-rail li:nth-child(4) { animation-delay: .74s; }
.open-screen.text-in .loop-rail li:nth-child(5) { animation-delay: .78s; }
.open-screen.text-in .loop-rail li:nth-child(6) { animation-delay: .82s; }
.open-screen.text-in .loop-rail li:nth-child(7) { animation-delay: .86s; }
@media (prefers-reduced-motion: reduce) {
  .loop-rail li { clip-path: none; transform: none; animation: none !important; }
}
@media (max-width: 560px) {
  .loop-rail { gap: 7px 5px; }
  .loop-rail li > span { height: 27px; padding: 0 10px; font-size: 11.5px; }
  .loop-rail li:not(:first-child)::before { width: 8px; }
}


/* ---------- what is real today ---------- */
/* Two facing columns with a rule between them, not two cards. The right
   column is set back rather than boxed off, so the section reads as one
   statement with two halves instead of two objects side by side. */
.ledger {
  margin-top: clamp(40px, 4.6vw, 68px);
  display: grid; grid-template-columns: 1.04fr 1fr;
  gap: clamp(32px, 5vw, 78px);
  align-items: start;
}
.ledger-col { position: relative; }
.ledger-col:last-child::before {
  content: ""; position: absolute; top: 4px; bottom: 4px;
  left: calc(clamp(32px, 5vw, 78px) / -2); width: 1px; background: var(--line);
}
.ledger-k {
  display: inline-flex; align-items: center; gap: 8px;
  font-size: 13px; font-weight: 600; letter-spacing: 0; line-height: 1;
  color: var(--ink-2); text-transform: uppercase;   /* 4.39:1 at 13px was under AA */
}
.ledger-k::before {
  content: ""; width: 7px; height: 7px; border-radius: 50%;
  border: 1px solid var(--ink-3);
}
.ledger-k.is-live { color: var(--accent); }
.ledger-k.is-live::before { background: var(--accent); border-color: var(--accent); }
.ledger-col ul { list-style: none; margin: 22px 0 0; padding: 0; display: grid; gap: clamp(12px, 1.1vw, 16px); }
/* What runs reads first and in the ink colour; what does not runs quieter and
   a size down. The hierarchy is the point of the section, so the type carries
   it rather than a fill and a dashed border. */
.ledger-col li {
  font-size: clamp(15px, 1.28vw, 17.5px); line-height: 1.5; color: var(--ink);
  padding-left: 20px; position: relative; max-width: 40ch;
}
.ledger-col li::before {
  content: ""; position: absolute; left: 0; top: .66em;
  width: 9px; height: 1px; background: var(--ink-3);
}
.ledger-col:last-child li { color: var(--ink-3); font-size: clamp(14px, 1.15vw, 16px); }
.ledger-col:last-child li::before { background: none; border-top: 1px dashed var(--line); }
.ledger-note { margin-top: clamp(30px, 3.4vw, 48px); max-width: 62ch; }

@media (max-width: 760px) {
  .ledger { grid-template-columns: minmax(0, 1fr); }
  /* the divider was drawn down the gap between columns; stacked, it has to be
     the line above the second one instead */
  .ledger-col:last-child::before {
    top: calc(clamp(32px, 5vw, 78px) / -2); bottom: auto; left: 0; right: 0;
    width: auto; height: 1px;
  }
}

/* =========================================================================
   The scroll story: one photo through the whole loop.

   Built from the product's own surfaces, never an illustration (the standing
   rule here): a viewfinder made of the app's chrome on the left, the
   dashboard's own cards on the right, and the only figures in it read live
   from /api/resolve. The two unbuilt beats are dashed and carry no number.

   The stage is sticky and the captions scroll past it, so the panel fills as
   the words are read. It ACCUMULATES rather than swapping, so the last frame
   holds the whole story at once; below 900px the sticky behaviour is dropped
   and the stage simply rests on that complete final state, because on a phone
   you should not have to scroll anything to see all of it.
   ========================================================================= */

/* The sticky stage centres itself in the viewport, so anything added here is
   void the reader scrolls through before the panel arrives. The caption
   column's own 46vh lead-in is the pacing; this only has to clear the header. */
.story { margin-top: clamp(10px, 1.2vw, 22px); }

@media (min-width: 901px) {
  .story {
    display: grid; grid-template-columns: minmax(0, 1.08fr) minmax(0, .82fr);
    gap: clamp(28px, 4vw, 72px); align-items: start;
  }
  .story-stage { position: sticky; top: 0; height: 100svh; display: grid; align-content: center; }
  /* The pacing is set by these two numbers. The stage is pinned only while the
     story has 100svh of travel left, so the first and last captions must centre
     INSIDE that range: with 46vh of padding either end, caption 01 centres 14vh
     after the stage pins and caption 07 centres 14vh before it lets go. Shrink
     the padding and the stage slides away while the last caption is still being
     read, which is exactly how this looked before. */
  .story-steps li { min-height: 36vh; display: grid; align-content: center; }
}

/* ---------- the stage ---------- */
.story-panes { display: grid; grid-template-columns: minmax(0, 1fr); }


/* the app pane: the dashboard's own cards */
.story-app {
  --slide-gap: 14px; --slide-w: 76%;
  position: relative;
  border-radius: var(--r-card, 16px); background: var(--white, #fff);
  border: 1px solid var(--line, #d2d2d7);
  padding: 12px; display: grid; align-content: start; gap: 12px;
  box-shadow: 0 30px 60px -44px rgba(11,12,14,.5);
}

/* The slider. One beat per slide, moving along x as the captions are read.
   The viewport clips; the track carries the slides and is the only thing that
   moves, so nothing reflows as the story advances.

   Each slide is narrower than the frame so the next one shows at the right
   edge: that peek is what says there is more to come, and it is why the track
   steps by the slide width rather than by a full 100%. Flex-basis resolves
   against the track, and the track is the viewport's width, so the two agree. */
.story-vp { overflow: hidden; }
.story-track {
  display: flex; gap: var(--slide-gap); align-items: stretch;
  transition: transform .62s cubic-bezier(.2,.7,.2,1);
}
.story-track > * {
  flex: 0 0 var(--slide-w); min-width: 0; min-height: 96px;
  opacity: .42; transition: opacity .5s ease;
}
/* only the beat being read is at full strength */
.story-track > .now, .story-track > .story-idle { opacity: 1; }
.story[data-step="camera"] .story-track > .story-idle,
.story[data-step="snap"] .story-track > .story-idle { opacity: 1; }
@media (prefers-reduced-motion: reduce) { .story-track { transition: none; } }
.story-app-bar { display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 2px 4px 8px; border-bottom: 1px solid var(--line, #d2d2d7); }
.story-app-title { font-family: var(--display); font-weight: 700; font-size: 15px; letter-spacing: -.04em; }
.story-app-tag { font-size: 10.5px; font-weight: 500; letter-spacing: .06em; text-transform: uppercase; color: var(--ink-2); }

/* the empty state is the first slide: what the app holds before anything is read */
.story-idle {
  border-radius: 12px; background: var(--grey, #f5f5f7); padding: 12px 13px;
  display: grid; gap: 3px; align-content: center;
}
.story-idle b { font-size: 14.5px; letter-spacing: -.02em; }
.story-idle span { font-size: 12.5px; line-height: 1.5; color: var(--ink-3); }

/* every slide is the same card, so the frame does not change shape as it moves */
.story-card {
  border-radius: 12px; background: var(--grey, #f5f5f7); padding: 12px 13px;
  display: grid; align-content: center;
  transition: box-shadow .45s ease, border-color .45s ease;
}
.story-card > * {
  opacity: 0; transform: translateY(8px);
  transition: opacity .45s ease, transform .55s cubic-bezier(.2,.7,.2,1);
}
.story-card.on > * { opacity: 1; transform: none; }
.story-card.now { box-shadow: 0 0 0 2px rgba(58,63,70,.22); }
/* the two beats that do not exist: outlined, never filled, and holding no figure */
.story-card.is-soon { background: none; border: 1px dashed var(--line, #d2d2d7); }
.story-card.is-soon .story-row-title { color: var(--ink-2); }
.story-card.is-soon.now { box-shadow: none; border-color: var(--ink-3); }

.story-card-head { display: flex; align-items: center; justify-content: space-between; gap: 10px; }
.story-brand { font-size: 16px; font-weight: 600; letter-spacing: -.02em; }
.story-pill { font-size: 10px; font-weight: 600; letter-spacing: .04em; padding: 3px 8px; border-radius: var(--r-pill, 999px); background: var(--accent, #17191d); color: #fff; }
.story-sub { margin: 2px 0 0; font-size: 11.5px; line-height: 1.45; color: var(--ink-3); }

.story-kv { margin: 0; display: grid; gap: 7px; }
.story-kv > div { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; }
.story-kv dt { font-size: 11.5px; color: var(--ink-3); }
.story-kv dd { margin: 0; font-size: 12.5px; font-weight: 500; }
.story-mono { font-variant-numeric: tabular-nums; letter-spacing: 0; }

.story-row { display: flex; align-items: center; gap: 10px; }
.story-badge {
  flex: none; display: inline-flex; align-items: center; justify-content: center;
  min-width: 46px; height: 26px; padding: 0 7px; border-radius: 7px;
  background: rgba(23,25,29,.09); color: var(--accent, #17191d);
  font-size: 11.5px; font-weight: 600; letter-spacing: 0;
}
.story-badge.is-empty { background: none; border: 1px dashed var(--line, #d2d2d7); }
.story-row-main { display: grid; gap: 1px; min-width: 0; }
.story-row-title { font-size: 13.5px; font-weight: 500; letter-spacing: -.015em; }
.story-row-px { margin-left: auto; font-size: 13px; font-weight: 500; font-variant-numeric: tabular-nums; white-space: nowrap; }
.story-row-px.is-empty { font-size: 11px; font-weight: 400; color: var(--ink-3); }

/* ---------- the captions beside the stage ---------- */
.story-steps { list-style: none; margin: 0; padding: 0; }
.story-steps li { display: grid; align-content: start; gap: 6px; opacity: .32; transition: opacity .45s ease; }
.story-steps li.is-on { opacity: 1; }
.story-steps li.is-done { opacity: .5; }
/* Resting on the last state, every caption is legible at once. This lives
   outside the stacked media query because demo.js also sets is-static when the
   visitor asked for reduced motion, at any width; inside it, those visitors
   were left reading the whole explanation at 1.9:1 with no way back. */
.story.is-static .story-steps li { opacity: 1; }
.story-steps li > b { font-family: var(--mono); font-weight: 400; font-size: 11.5px; letter-spacing: .06em; color: var(--ink-2); }
.story-steps h3 { font-size: clamp(22px, 2.4vw, 30px); letter-spacing: -.03em; }
.story-steps p { margin: 0; font-size: clamp(14px, 1.2vw, 16.5px); line-height: 1.5; color: var(--ink-2); max-width: 38ch; }
.story-state {
  justify-self: start; margin-top: 6px; padding: 4px 10px; border-radius: var(--r-pill, 999px);
  font-size: 11px; font-weight: 500; letter-spacing: 0;
  background: rgba(23,25,29,.08); color: var(--accent, #17191d);
}
.story-steps li.is-soon .story-state { background: none; border: 1px dashed var(--line, #d2d2d7); color: var(--ink-2); }

/* ---------- stacked, under 900px ---------- */
@media (max-width: 900px) {
  .story-steps { display: grid; gap: clamp(20px, 4vw, 30px); margin-top: clamp(26px, 5vw, 40px); }
}
/* The panel is what the reader is looking at, so on a large screen it is
   given real presence instead of being left at the size a phone needs.
   This sits below the base rules deliberately: a media query adds no
   specificity, so above them it would simply lose. */
@media (min-width: 901px) {
  /* the panel is the thing being read here, so it is given real presence
     rather than being left at the size a phone needs */
  .story-app { --slide-gap: 18px; padding: 16px; }
  .story-card, .story-idle { padding: 16px 18px; }
  .story-row-title { font-size: 15px; }
  .story-sub { font-size: 12.5px; }
  .story-brand { font-size: 18px; }
  .story-kv dt { font-size: 12.5px; }
  .story-kv dd { font-size: 15px; }
  .story-row-px { font-size: 14.5px; }
  .story-badge { min-width: 52px; }
  .story-vp, .story-track { min-height: 180px; }
}

/* Below the sticky breakpoint the stage rests on the last beat, which as a
   slider is the empty portfolio slot: the live ISIN, symbol and price would be
   rendered and never reachable. So on a phone the reader drives the track
   directly, by swiping it, and every beat stays available. The transform is
   dropped here because the script would otherwise fight the scroll. */
@media (max-width: 900px) {
  .story-vp {
    overflow-x: auto; scroll-snap-type: x mandatory;
    -webkit-overflow-scrolling: touch; scrollbar-width: none;
    margin-inline: -2px; padding-inline: 2px;
  }
  .story-vp::-webkit-scrollbar { display: none; }
  .story-track { transform: none !important; transition: none; }
  .story-track > * { scroll-snap-align: start; opacity: 1; }
}

@media (max-width: 560px) {
  /* The peek costs a quarter of the pane, which a phone does not have to give:
     below this width a slide takes the whole frame. The captions carry the
     sense of travel here, and the track still slides. */
  .story-app { --slide-gap: 10px; --slide-w: 100%; padding: 10px; }
  .story-card, .story-idle { padding: 11px 12px; }
  .story-track > * { opacity: 1; }        /* nothing beside it to rank against */

  /* the price stops competing with the title for the same line */
  .story-row { align-items: start; row-gap: 2px; flex-wrap: wrap; }
  .story-row-main { flex: 1 1 0; min-width: 0; }
  .story-row-px { margin-left: 0; flex: 0 0 100%; padding-left: 56px; font-size: 12.5px; }
  .story-badge { flex: none; }
  .story-kv { grid-template-columns: minmax(0, 1fr); gap: 4px; }
}

/* =========================================================================
   Scroll reveals, on pictures and panels only.

   The 22 Sep decision stands for text: headings, subheads and notes are
   simply there from the first frame, and the rule above still cancels .rise
   and [data-stagger]. This is a separate, opt-in class put only on figures,
   cards and panels, so nothing anyone has to READ arrives late.

   Hidden only once the script has confirmed it can reveal them again, so a
   failed or blocked script leaves a complete page rather than an empty one.
   ========================================================================= */
.js-reveal .reveal {
  opacity: 0; transform: translateY(20px);
  transition: opacity .65s ease, transform .75s cubic-bezier(.2,.7,.2,1);
  transition-delay: calc(var(--i, 0) * 80ms);
}
.js-reveal .reveal.is-in { opacity: 1; transform: none; }
@media (prefers-reduced-motion: reduce) {
  .js-reveal .reveal { opacity: 1; transform: none; transition: none; }
}

/* .demo-note is set for the dark stage it was written for; on the grey band
   under the story it needs the page's own note colour. */
.story-note { color: var(--ink-3); margin-top: clamp(26px, 3vw, 44px); }

/* Declared last on purpose: `.story-steps { padding: 0 }` above resets it at the
   same specificity, so this has to come after to survive. The 46vh is what keeps
   captions 01 and 07 centred while the stage is still pinned. */
@media (min-width: 901px) {
  .story-steps { padding-block: 46vh; }
}

/* The dimmed-tier treatment went with the tier that needed two recognisers to
   agree. Both outcomes on this page are reachable now, so nothing here is
   drawn as unavailable. */
.recog-state { margin-top: clamp(12px, 1.4vw, 18px); max-width: 68ch; }
.tier { position: relative; }
.tier-off[hidden] { display: none; }

/* The captured app screens predate the current recogniser config, so they show a
   tier the page says is unreachable. The caption explains that rather than
   leaving the picture to contradict the copy. */
.shot-note { margin-top: 10px; max-width: 62ch; }

/* The two unbuilt beats carry the words "not yet", not just a dashed border:
   a purely visual signal is invisible to a screen reader and easy to miss
   against a 112px headline. */
.loop-rail li.is-soon > span em {
  font-style: normal; font-size: 9.5px; letter-spacing: .08em; text-transform: uppercase;
  color: rgba(221,223,226,.5); margin-left: 6px;
}

/* When /api/status cannot be read, no tier may look confirmed in either
   direction: HIGH must not read as reachable and MEDIUM must not read as
   guaranteed. site.js adds this in the catch. */
.tiers.is-unconfirmed .tier { background: none; border: 1px dashed var(--line); }
.tiers.is-unconfirmed .tier-k, .tiers.is-unconfirmed .tier p { color: var(--ink-3); }
