/* Framecore Digital — page layout.
 *
 * The site is ONE pinned sequence: the viewport holds still and its contents come in and out
 * as the page scrolls.
 *
 * TWO CLOCKS, and the split is the whole design:
 *   SCROLL opens GATES. A scroll-driven animation on a wrapper flips one discrete property,
 *     `display`, and nothing else.
 *   TIME plays SECTIONS. Everything inside a gate is an ordinary timed animation. Inside
 *     display:none it does not exist; the moment the wrapper gets boxes it starts from zero.
 *     Measured: ~13ms on first show, ~13ms again on re-entry. So entering a section plays it.
 *
 * WHY THE GATES ARE HTML DIVS AND THE MARK IS FOUR LAYERS. Two routes were measured and
 * rejected before this one, both worth knowing so neither gets retried:
 *   - display:none on an SVG <g> does NOT tear down its descendants' animations. A finished
 *     one stays finished, so the beat was over before it was ever on screen.
 *   - `animation-name: var(--a)` with the gate writing the name is dead by spec, not by bug:
 *     a custom property set by an animation is animation-tainted, and tainted values are
 *     forbidden in animation-*, so the name resolves to none. The token arrived correctly and
 *     did nothing.
 * An HTML wrapper does tear the subtree down, SVG descendants included, so each stage of the
 * mark is its own <svg> inside its own div, all four stacked in one grid cell on a shared
 * viewBox.
 *
 * The consequence to know: a section on its own clock can no longer be scrubbed. Scrolling
 * faster does not run it faster; it only arrives sooner. Autoplay and scrub are exclusive.
 *
 * Scope note (AGENTS.md): the Tier 3 override file is custom properties only and may not
 * carry layout, so page composition lives here. This file never targets a .spectrum-* class,
 * reads Framecore semantic tokens, and contains no !important.
 */

*, *::before, *::after { box-sizing: border-box; }

/* The scrollbar is hidden because the dot rail replaces it - it says the same thing (where
   am I, how much is left) in the site's own language. Scrolling itself is untouched: wheel,
   trackpad, keyboard, touch and the rail's own links all still work. */
/* THE KEYBOARD, WITHOUT SCRIPT. Up and Down have to move a whole screen, not 40 pixels, and
   Tier 1 forbids the keydown handler that would normally do it. Scroll snapping gets there
   instead: the arrow key scrolls a little, and the snap resolves that nudge to the nearest
   stop - which is one section away. The same mechanism catches the wheel and the trackpad,
   so every input lands on a section rather than between two.

   `mandatory`, not `proximity`: proximity only engages near a snap point, and a single arrow
   press moves far too little to get near one, so the keys would have done nothing. The cost
   is that the space between sections is no longer a resting place - correct here, since the
   page IS five screens rather than a continuous document. */
html {
  background: var(--fc-black);
  scroll-snap-type: y mandatory;
  scrollbar-width: none;          /* Firefox */
  -ms-overflow-style: none;       /* old Edge */
}

html::-webkit-scrollbar { inline-size: 0; block-size: 0; }

/* MANDATORY IS A KEYBOARD DECISION, AND A THUMB IS NOT A KEYBOARD. The reason above is the
   arrow key: it nudges too little to reach a proximity threshold, so proximity would leave
   the keys doing nothing. None of that applies to a finger, which arrives with momentum and
   needs no help reaching a stop - and mandatory actively fights it, re-targeting mid-flick
   and, on iOS, swallowing the anchor jump a tapped cue asks for. Coarse pointers get
   proximity; the keys keep mandatory, because the keys are why it is there. */
@media (pointer: coarse) {
  html { scroll-snap-type: y proximity; }
}

body {
  margin: 0;
  background: var(--fc-black);
  color: var(--fc-white);
  font-family: var(--fc-font-sans);
  -webkit-font-smoothing: antialiased;
}

/* ---- the lockup ---------------------------------------------------------
   Used twice: the screen at rest, and the closing screen that echoes it. */

/* The two type sizes are declared ONCE, here, because the mark is derived from them. Both
   lines are set at line-height 1, so the text block is exactly name + gap + tagline - which
   is therefore what the mark measures. Change a size and the mark follows; there is no
   second clamp to keep in sync. */
.fc-lockup {
  --fc-name-size: clamp(1.75rem, 5.2vw, 4rem);
  --fc-tag-size: clamp(0.7rem, 1.45vw, 1.1rem);
  --fc-lockup-gap: 0.45rem;

  display: flex;
  align-items: center;
  gap: clamp(1rem, 3vw, 2.25rem);
}

/* Deliberately NOT `align-self: stretch` with `aspect-ratio`. A flex item whose width is
   derived from a height it only gets by stretching is circular: the width has to be resolved
   to lay the line out, before the line has a height to stretch to. Chrome breaks the loop
   with the SVG's default intrinsic width - 300px - which then drives the line height, so the
   lockup rendered at 300px instead of ~89. Computing the size instead keeps it one-way. */
.fc-lockup__mark {
  display: block;
  flex: none;
  inline-size: calc(var(--fc-name-size) + var(--fc-tag-size) + var(--fc-lockup-gap));
  aspect-ratio: 1;
  color: var(--fc-white);
}

.fc-lockup__mark svg { inline-size: 100%; block-size: 100%; display: block; }

/* ---- INK-TO-INK ALIGNMENT ------------------------------------------------
   Matching the two BOXES is not the same as matching what you see in them, and that gap is
   what made the mark sit high with daylight under it. Both boxes did line up exactly; but
   the mark's glyph is inset 12.207% inside its own viewBox (the square's centreline runs
   15.8691..84.1309, plus half a 7.3242 stroke), while the text's ink is inset by leading -
   ascender above the cap at the top, descender below the baseline at the bottom. Two
   different insets inside two equal boxes leaves the mark ~11% short and ~9px high.

   So both sides are trimmed down to their ink and then matched:
     - the lockup SVGs carry a viewBox cropped to 12.207 12.207 75.586 75.586, so the box IS
       the mark (see index.html - the .fc-build layers keep the full viewBox, they align to
       nothing but themselves);
     - `text-box: trim-both cap alphabetic` cuts each line's box to cap-height..baseline, so
       the text block's box is likewise exactly its ink, and --fc-lockup-gap stops being a
       box gap and becomes the VISIBLE gap. It is restated as a ratio of the name size to
       land on the same 16px it optically had before, and to keep scaling with the lockup.
     - the mark is then sized to that block: cap-height is the font's own metric, so `1cap`
       measured at (name + tag) IS capName + capTag with no ratio typed in, and the height
       the text now occupies is that plus the gap.
   Left in @supports so a browser without text-box keeps the old box-matched lockup rather
   than a mark sized against an untrimmed block. */
@supports (text-box: trim-both cap alphabetic) {
  .fc-lockup { --fc-lockup-gap: calc(var(--fc-name-size) * 0.25); }

  .fc-lockup__name,
  .fc-tagword { text-box: trim-both cap alphabetic; }

  .fc-lockup__mark {
    font-size: calc(var(--fc-name-size) + var(--fc-tag-size));
    inline-size: calc(1cap + var(--fc-lockup-gap));
    block-size: calc(1cap + var(--fc-lockup-gap));
    aspect-ratio: auto;
  }
}

.fc-lockup__text {
  margin: 0;
  display: flex;
  flex-direction: column;
  gap: var(--fc-lockup-gap);
  text-align: start;
}

.fc-lockup__name {
  font-size: var(--fc-name-size);
  font-weight: 700;
  line-height: 1;
  letter-spacing: 0.01em;
  text-transform: uppercase;
}

/* The tagline hangs off the wordmark's own left edge and tracks wider, so the two lines read
   as one lockup rather than as a heading with a subtitle under it. */
.fc-lockup__tag {
  display: flex;
  flex-wrap: wrap;
  gap: 0.4em;
  font-size: var(--fc-tag-size);
  font-weight: 500;
  line-height: 1;
  letter-spacing: 0.12em;
  text-transform: uppercase;
}

/* The closing screen states the name once more, a size down from the opening. Only the size
   token is restated - the wordmark and the mark both follow it by themselves. */
.fc-lockup--outro { --fc-name-size: clamp(1.5rem, 3.6vw, 2.75rem); }

/* ---- the screen at rest -------------------------------------------------
   The lockup sits a little above centre: the gold cue needs room under it, and an optically
   centred group reads better high than mathematically centred. */

/* Four rows now, not three. The statement gets its own band under the lockup rather than
   sharing the lockup's, so the two stay optically grouped while the space above and below
   the pair keeps flexing with the window. The cue's band shrinks to make room. */
.fc-intro {
  min-block-size: 100svh;
  display: grid;
  grid-template-rows: 1fr auto auto 1fr;
  justify-items: center;
  align-items: center;
  padding: var(--spectrum-spacing-500, 1.5rem);
}

.fc-lockup--intro { grid-row: 2; }

.fc-intro__copy {
  grid-row: 3;
  align-self: start;
  margin-block-start: clamp(1.25rem, 4vh, 2.5rem);
  display: flex;
  flex-direction: column;
  gap: clamp(0.5rem, 1.4vh, 0.85rem);
  /* Wider than the 46-54ch used for the section copy: this block sits under a lockup about
     760px across, and a short measure under a wide mark left a four-line rag that read as
     cramped rather than as a deliberate column. */
  max-inline-size: 60ch;
  text-align: center;
}

.fc-intro__line {
  margin: 0;
  font-size: clamp(0.92rem, min(1.15vw, 1.8vh) + 0.35rem, 1.2rem);
  line-height: var(--spectrum-body-line-height);
  color: var(--fc-ink-body);
  text-wrap: pretty;
}

/* ---- the scroll cue -----------------------------------------------------
   A gradient cannot fill a CSS border-triangle, so the shape is a clipped box carrying the
   gold pattern. That keeps the metal on the one element meant to be gold. */

/* THE ARROW IS THE CONTROL. Every screen carries one and each points at the next stop, so
   the sequence can be walked by clicking as well as by scrolling - and with no script, since
   each is an ordinary anchor to a .fc-stop.

   The gold triangle is a CLIPPED BOX, not a border-triangle, because a gradient cannot fill
   a border. The clip lives on ::before so the link itself keeps a rectangular hit area: a
   clipped element only takes pointer events inside the clip, which would leave a 16px arrow
   with a sliver of a target. The link is padded out to a comfortable tap size around it. */
.fc-cue {
  display: grid;
  place-items: center;
  inline-size: clamp(2.75rem, 6vw, 3.5rem);
  aspect-ratio: 1;
  color: inherit;
  text-decoration: none;
  cursor: pointer;
}

/* Half the height it used to have - a flatter chevron rather than a tall spike - and held at
   70% so it sits under the copy instead of competing with it. Full strength on hover and
   focus, which is the moment it stops being a hint and becomes a target. */
.fc-cue::before {
  content: "";
  inline-size: clamp(1.1rem, 2.2vw, 1.6rem);
  aspect-ratio: 1 / 0.525;
  background-image: var(--fc-gold-pattern);
  clip-path: polygon(0 0, 100% 0, 50% 100%);
  opacity: 0.7;
  transition: translate 0.18s cubic-bezier(.16,.84,.30,1), opacity 0.18s ease;
}

.fc-cue--up::before { rotate: 180deg; }

.fc-cue:hover::before, .fc-cue:focus-visible::before { opacity: 1; translate: 0 18%; }
/* the up arrow leans the way it points */
.fc-cue--up:hover::before, .fc-cue--up:focus-visible::before { translate: 0 -18%; }
.fc-cue:focus-visible { outline: 2px solid var(--fc-white); outline-offset: 2px; }

/* the label names the destination for anyone not looking at a triangle */
.fc-cue__label {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

/* THE OPENING SCREEN'S arrow sits in the intro's own last row and waits for the mark to
   finish assembling. The ones on the three sections are placed against the stage instead,
   below, and arrive with their section. */
.fc-cue--intro {
  grid-row: 4;
  align-self: start;
  margin-block-start: clamp(0.5rem, 2vh, 1.5rem);

  /* it arrives last, once there is something worth scrolling away from */
  opacity: 0;
  animation: fc-cue-in 0.5s cubic-bezier(.16,.84,.30,1) var(--fc-d-cue) both;
}

/* EDGE ARROWS. Every screen after the first carries a pair - back at the top, forward at the
   bottom - pinned to the stage rather than to the copy, so they hold the same two positions
   no matter how long that section's text runs. They live inside the screen they belong to,
   so each is gated on and off with it and can point at its own neighbours. */
.fc-cue--edge {
  position: absolute;
  inset-inline-start: 50%;
  translate: -50% 0;
  z-index: 2;
}

/* Pulled in off the stage edges rather than sitting on them. At the extremes the pair read as
   two unrelated marks in opposite corners of the screen; closer together they read as one
   control with a direction. The inset is capped so they stay clear of the copy block, which
   on the tallest beat runs from about a quarter to two thirds of the stage. */
.fc-cue--edge.fc-cue--up { inset-block-start: clamp(2rem, 12vh, 7rem); }
.fc-cue--edge.fc-cue--down { inset-block-end: clamp(2rem, 12vh, 7rem); }

@media (prefers-reduced-motion: reduce) {
  .fc-cue::before { transition: none; }
}

@keyframes fc-cue-in {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* The cue is an affordance, not decoration: it has to read as "keep scrolling" at a glance,
   so it travels a full arrow-height and eases at both ends rather than drifting.
   The bob owns TRANSFORM only, and the arrival owns OPACITY - two animations on one property
   would mean the later one silently wins and the fade-in never renders. */
/* The DEFINITION stays at top level while only the USE sits behind the media query. Nested
   inside it, these keyframes simply do not exist under `reduce` - and then the Full-motion
   checkbox, which is there precisely to opt back in, names an animation that is not defined
   and silently gets nothing. The bob never came back. */
/* Half the travel it used to have. At full amplitude it was the loudest thing on a screen
   that is otherwise still, and now that the same arrow repeats on every section the opening
   one only has to suggest the gesture, not perform it. */
@keyframes fc-cue-hint {
  0%       { transform: translateY(-19%); }
  45%      { transform: translateY(36%); }
  70%      { transform: translateY(23%); }
  100%     { transform: translateY(-19%); }
}

/* ONLY the opening screen bobs. The three section arrows are the same control standing
   still: by then the gesture has been taught, and three of them nodding at once would read
   as noise rather than as an invitation. */
@media (prefers-reduced-motion: no-preference) {
  .fc-cue--intro {
    animation: fc-cue-in 0.5s cubic-bezier(.16,.84,.30,1) var(--fc-d-cue) both,
               fc-cue-hint 1.9s cubic-bezier(.45,0,.55,1) var(--fc-d-cue) infinite;
  }
}

/* ---- the opening screen assembles itself --------------------------------
   Every section plays when its gate opens. The first screen has no gate - it is already
   there on load - so it plays on load instead, in the same grammar: the mark draws, the
   name arrives, the three words land one by one, and the cue comes last.

   Timings are stated once here rather than inline, because the order is the point: each
   delay is the previous beat plus its overlap. */

/* Only the WORDS are timed here now. The mark's own beats moved to the block that builds it
   (--fc-d-icore / -iframe / -itail / -itick), next to the animations they delay, because
   three of them used to name a draw-on sequence this mark no longer performs. */
/* The name arrives WITH the ball, so the screen has a subject before anything is said about
   it. The three words no longer sit on a shared delay - each is derived from the mark beat
   it belongs to, further down. */
.fc-lockup--intro {
  --fc-d-name: 0.15s;
}

/* The statement lands after the last tagword (1.05 + 2 x 0.16 = 1.37s) and before the cue,
   which is pushed back to keep it last. Two lines, one step apart, same 0.16s the tagline
   uses - the whole opening runs on one step size. */
/* Declared on .fc-intro, not on the lockup: the statement lines are the lockup's SIBLINGS,
   so a token held on the lockup would not reach them and each would need its own literal. */
.fc-intro {
  --fc-t-tagstep: 0.16s;   /* between one statement line and the next */
  /* The frame the last tick finishes on. Declared here, not on the lockup, because the name
     is INSIDE the lockup and the statement lines are its siblings - both must read the same
     value or the screen would resolve in two stages instead of one. */
  --fc-d-ink: 2.80s;
  /* after Innovation, which now waits for the opening at 1.90s */
  --fc-d-intro-copy: 2.10s;
  /* after the mark finishes assembling - the last tick lands at 2.80s - so the cue is still
     the thing that arrives once there is something worth scrolling away from */
  --fc-d-cue: 2.95s;
}

/* Same two-part treatment as the name: arrives on its own beat in waiting grey, then comes
   up to body ink on the shared reveal. --fc-d-ink is declared on .fc-lockup--intro, which is
   these lines' SIBLING, so it is restated on .fc-intro where both can read it. */
.fc-intro__line {
  animation: fc-intro-rise 0.6s cubic-bezier(.16,.84,.30,1)
             calc(var(--fc-d-intro-copy) + var(--fc-i-line, 0) * var(--fc-t-tagstep)) both,
             fc-ink-reveal-body 0.55s ease var(--fc-d-ink) both;
}

.fc-intro__line:nth-child(1) { --fc-i-line: 0; }
.fc-intro__line:nth-child(2) { --fc-i-line: 1; }

/* THE INTRO MARK BUILDS ITSELF THE WAY THE SCROLLED ONE DOES.
   It used to draw: the ring and the square inked themselves on, which was a different idea
   from the one the three sections tell. Now it is the same story on a different clock -
   ball, square, opening - so the opening screen states the grammar the rest of the page
   then repeats one beat per section.

   The beats overlap deliberately: the square starts while the ball is still settling, and
   the ticks land while the stub is still retracting, so it reads as one build rather than
   four things queueing. Everything is over by 2.77s and the cue follows at 2.95s. */
.fc-lockup--intro {
  --fc-d-icore: 0.15s;   /* the ball falls */
  --fc-d-iframe: 0.85s;  /* the square draws around it, over the settle */
  --fc-d-itail: 1.80s;   /* the stub closes it, holds, then retracts: the opening */
  --fc-d-itick: 2.45s;   /* the ticks land in the gap the stub left */
}

@keyframes fc-ball-drop-calm {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* HOISTED TO TOP LEVEL. These three are used by the scrolled mark inside
   @supports (animation-timeline: view()) AND by the intro mark, which runs on a time
   clock and must play with or without scroll-driven animations. A @keyframes defined
   inside an at-rule does not exist outside it, so leaving them there gave the intro
   three animation names that resolved to nothing. */
@keyframes fc-ball-drop {
  0%    { opacity: 0; transform: translateY(-160%) scale(0.94, 1.1);
          animation-timing-function: cubic-bezier(.40,0,.90,.40); }
  6%    { opacity: 1; transform: translateY(-150%) scale(0.94, 1.1);
          animation-timing-function: cubic-bezier(.40,0,.90,.40); }
  46%   { opacity: 1; transform: translateY(0)     scale(1.20, 0.78);
          animation-timing-function: cubic-bezier(.30,0,.60,1); }
  62%   { opacity: 1; transform: translateY(-26%)  scale(0.97, 1.04);
          animation-timing-function: cubic-bezier(.40,0,.90,.40); }
  78%   { opacity: 1; transform: translateY(0)     scale(1.10, 0.90);
          animation-timing-function: cubic-bezier(.30,0,.60,1); }
  88%   { opacity: 1; transform: translateY(-8%)   scale(0.99, 1.01);
          animation-timing-function: cubic-bezier(.40,0,.90,.40); }
  100%  { opacity: 1; transform: none; }
}

@keyframes fc-draw-main {
  from { stroke-dashoffset: 221.1914; }
  to   { stroke-dashoffset: 0; }
}

@keyframes fc-tick {
  from { stroke-dashoffset: 4.1016; }
  to   { stroke-dashoffset: 0; }
}

/* the same rebound the sections use, anchored at its own bottom edge so it squashes on
   contact rather than scaling about its middle */
.fc-lockup--intro #intro-core {
  transform-box: fill-box;
  transform-origin: center bottom;
  animation: fc-ball-drop var(--fc-t-ball) linear var(--fc-d-icore) both;
}

.fc-lockup--intro #intro-frame-main {
  animation: fc-draw-main 0.95s cubic-bezier(.42,.06,.75,.75) var(--fc-d-iframe) both;
}

/* ONE animation, three jobs: draw the stub, hold it closed, retract it. Splitting this into
   a draw and an open would put two animations on one stroke-dashoffset, and the later one
   silently wins. Retracting to the full 51.8555 leaves the stub gone, which IS the logo. */
.fc-lockup--intro #intro-tail {
  animation: fc-intro-tail 0.95s linear var(--fc-d-itail) both;
}

@keyframes fc-intro-tail {
  0%   { stroke-dashoffset: 51.8555;
         animation-timing-function: cubic-bezier(.30,.30,.80,1); }
  30%  { stroke-dashoffset: 0; }
  46%  { stroke-dashoffset: 0;
         animation-timing-function: cubic-bezier(.45,0,.35,1); }
  100% { stroke-dashoffset: 51.8555; }
}

.fc-lockup--intro #intro-tick2 {
  animation: fc-tick var(--fc-t-tick) cubic-bezier(.25,.85,.35,1) var(--fc-d-itick) both;
}
.fc-lockup--intro #intro-tick1 {
  animation: fc-tick var(--fc-t-tick) cubic-bezier(.25,.85,.35,1)
             calc(var(--fc-d-itick) + 0.11s) both;
}

/* HELD BACK UNTIL THE MARK IS BUILT. The name arrives first, at 150ms, but in waiting grey:
   it is present without being finished, the same way the mark is. When the last tick lands
   at 2.80s the whole opening comes up to its real ink at once - name and statement together
   - so the screen resolves on one beat rather than fading up in pieces.

   A SECOND animation rather than a longer first one: the rise owns opacity and transform,
   this owns colour. Putting colour into fc-intro-rise would mean the name could not arrive
   before the reveal, which is the entire point. */
/* THE WORDMARK WAITS AS AN OUTLINE. Hollow while the mark is still being built, then the
   fill comes up and the rim goes with it - the word finishes at the same moment the logo
   does. The fill interpolates from `transparent`, so it washes in behind the stroke rather
   than replacing it on one frame.

   Switched by CUSTOM PROPERTY, not by animation-name. Two rules already override
   animation-name on this element - the reduced-motion block and the Full-motion checkbox -
   and adding a third switch there would mean keeping three lists in step. The keyframes read
   these two properties off the element instead, so @supports can change what the hold looks
   like without any of those rules knowing. */
.fc-lockup--intro .fc-lockup__name {
  --fc-name-hold: var(--fc-ink-waiting);   /* no text-stroke: hold as a flat grey fill */
  --fc-name-rim: transparent;

  animation: fc-intro-rise 0.7s cubic-bezier(.16,.84,.30,1) var(--fc-d-name) both,
             fc-ink-reveal 0.55s ease var(--fc-d-ink) both;
}

/* Where the rim can actually be drawn, the hold becomes hollow instead. Guarded because
   without text-stroke `color: transparent` would render nothing at all for 2.8s. */
@supports (-webkit-text-stroke: 1px currentColor) {
  .fc-lockup--intro .fc-lockup__name {
    --fc-name-hold: transparent;
    --fc-name-rim: var(--fc-ink-waiting);
    /* in em so the rim tracks the wordmark's own clamp rather than a fixed pixel weight */
    -webkit-text-stroke-width: 0.022em;
  }
}

@keyframes fc-ink-reveal {
  from { color: var(--fc-name-hold); -webkit-text-stroke-color: var(--fc-name-rim); }
  to   { color: var(--fc-ink-strong); -webkit-text-stroke-color: transparent; }
}

/* the statement resolves to body ink, not to full white - it was never the loudest thing */
@keyframes fc-ink-reveal-body {
  from { color: var(--fc-ink-waiting); }
  to   { color: var(--fc-ink-body); }
}

/* THE THREE WORDS RIDE THE THREE BEATS.
   They used to arrive on a metronome - one fixed step apart - which put them out of phase
   with the mark building beside them. Each word now names the beat it belongs to, the same
   pairing the three sections use: circle rebound = Design, square = Tech, opening =
   Innovation. Each delay is DERIVED from that beat's own token, so moving a beat moves its
   word with it and the two can never drift apart.

   The small lead-in on each is the gap between a beat starting and being legible - the ball
   has to be falling, the square has to have laid down some line - so the word lands on the
   beat rather than announcing it. */
.fc-lockup--intro .fc-tagword {
  animation: fc-intro-rise 0.55s cubic-bezier(.16,.84,.30,1) var(--fc-d-word-beat) both;
}

.fc-lockup--intro .fc-tagword:nth-child(1) {
  --fc-d-word-beat: calc(var(--fc-d-icore) + 0.30s);    /* the ball rebounds */
}
.fc-lockup--intro .fc-tagword:nth-child(2) {
  --fc-d-word-beat: calc(var(--fc-d-iframe) + 0.10s);   /* the square draws */
}
.fc-lockup--intro .fc-tagword:nth-child(3) {
  --fc-d-word-beat: calc(var(--fc-d-itail) + 0.10s);    /* the stub retracts: the opening */
}

@keyframes fc-intro-rise {
  from { opacity: 0; transform: translateY(0.75rem); }
  to   { opacity: 1; transform: none; }
}
@keyframes fc-intro-rise-calm {
  from { opacity: 0; }
  to   { opacity: 1; }
}

/* Same rule as everywhere else: the mark's construction is content and stays; the words'
   travel is decoration and goes. */
@media (prefers-reduced-motion: reduce) {
  /* `animation-name` REPLACES the whole list. The name and the statement each carry two
     animations - the rise and the ink reveal - so naming only the calm rise here would drop
     the reveal and leave both stuck in waiting grey for good. The tagwords carry one, so
     they are listed separately. */
  .fc-lockup--intro .fc-tagword { animation-name: fc-intro-rise-calm; }
  .fc-lockup--intro .fc-lockup__name { animation-name: fc-intro-rise-calm, fc-ink-reveal; }
  .fc-intro__line { animation-name: fc-intro-rise-calm, fc-ink-reveal-body; }

  /* The intro ball damps exactly as the scrolled one does - the drop is the same gesture on
     a different clock, so it cannot honour the setting in one place and ignore it in the
     other. The square and the opening are the mark's construction, not decoration, and stay
     in both paths. */
  .fc-lockup--intro #intro-core { animation-name: fc-ball-drop-calm; }
}

/* =========================================================================
   THE PINNED SEQUENCE
   ========================================================================= */

.fc-seq {
  position: relative;

  /* THE COLUMN'S OWN GUTTER, declared here rather than on the stage because the RAIL has to
     read it too and the rail is the stage's sibling, not its child. One value, two users,
     no chance of the line drifting away from the text it is reporting on. */
  --fc-stage-pad: clamp(1.25rem, 6vw, 4rem);
  /* the dot's hit area - the rail's offset is derived from it, so a bigger dot stays put */
  --fc-dot-size: 1.4rem;

  /* One place for every duration, so a beat can be retimed without hunting through
     keyframes. The delays below are expressed against these, never retyped. */
  --fc-t-ball: 1.15s;     /* fall, impact, rebound, settle */
  --fc-t-draw: 1.30s;     /* the square's long side-to-side draw */
  --fc-t-tail: 0.30s;     /* the short closing stub */
  --fc-t-open: 0.75s;     /* the tail retracting - the opening itself */
  --fc-t-tick: 0.24s;     /* one tick appearing */
  --fc-t-word: 0.325s;    /* a section title arriving */
  --fc-t-copy: 0.45s;     /* its section text, resolving as one block */
  /* How far back the section copy is held before it resolves. Opacity, not ink: the block is
     legible the whole time, it is simply not yet the thing being read. */
  --fc-o-copy-hold: 0.5;
  --fc-t-copylag: 0.40s;  /* the pause between the title landing and that text resolving */
  --fc-t-stagger: 0.19s;  /* step between the closing screen's items */
}

/* GUTTERS. The composition is a centred column with symmetric side gutters, and the gutters
   scale with the viewport rather than being a fixed inset:
     - below the measure, `padding-inline` is fluid (6vw), so narrow screens still breathe;
     - above it, `max-inline-size` caps the column and `margin-inline: auto` turns everything
       left over into gutter, which then grows with the window.
   Because the cap and the auto margins sit on the grid itself, every layer inside - intro,
   mark, copy - is centred against the same column, so nothing drifts as the window resizes. */
.fc-stage {
  block-size: 100svh;
  /* containing block for the edge arrows, which hold the same two positions on every screen
     regardless of how tall that screen's content is */
  position: relative;
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(0, 2fr);
  align-items: center;
  max-inline-size: var(--fc-wrap);
  margin-inline: auto;
  padding-inline: var(--fc-stage-pad);
}

/* the layers share cells so they can cross-fade in place */
.fc-intro { grid-area: 1 / 1 / 2 / 3; justify-self: center; }
.fc-build { grid-area: 1 / 1 / 2 / 2; justify-self: center; inline-size: min(100%, 17rem); }
.fc-beats { grid-area: 1 / 2 / 2 / 3; display: grid; }
.fc-outro { grid-area: 1 / 1 / 2 / 3; justify-self: center; }

/* and the three beats share one cell with each other */
.fc-beat { grid-area: 1 / 1 / 2 / 2; }

.fc-intro {
  display: grid;
  justify-items: center;
  gap: clamp(1.25rem, 4vh, 3rem);
}

/* THE MARK. Four layers, one grid cell, one shared viewBox, so a shape drawn in one layer
   registers exactly with the shapes in the others. The aspect-ratio holds the box open even
   when every layer is still gated shut, so nothing reflows as the first one arrives. */
.fc-build {
  display: grid;
  aspect-ratio: 1;
  color: var(--fc-white);
}

.fc-layer { grid-area: 1 / 1 / 2 / 2; }
.fc-layer svg { inline-size: 100%; block-size: 100%; display: block; }

/* the circle squashes on contact, so it is anchored at its own bottom edge */
#core { transform-box: fill-box; transform-origin: center bottom; }

.fc-beat__title {
  margin: 0;
  /* One continuous curve rather than a size per breakpoint. A plain `4vw` floors at 20px on
     a phone - caption-sized - while a mobile-only override would step the type down at the
     breakpoint, which reads as a bug. Adding a rem to the vw term lifts the small end
     without touching the large one: 40px at 390, 48px from ~600 up, nothing in between
     jumping. */
  font-size: clamp(2.5rem, 4vw + 1.5rem, 3rem);
  font-weight: 800;
  line-height: 1;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  background-image: var(--fc-gold-pattern);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* The copy grew from one placeholder line to real paragraphs, a list and a reference line, so
   the column is now a flow rather than a single block.

   SPACING IS SPECTRUM'S, NOT THE COLUMN'S. This used to be one `gap` on the container,
   clamped against the viewport height so the tallest beat could close up on a short screen.
   Measured, that clamp floored at 9.6px around a 686px viewport and did nothing below it -
   i.e. it stopped compressing exactly where the fit is tightest. Paragraph spacing is now
   --spectrum-body-margin-multiplier, which is 0.75em, so each block's space is a ratio of
   ITS OWN type - and since the body size is already viewport-height-linked, the margin still
   shrinks with the screen, just through the type rather than around it. */
.fc-beat__body {
  margin-block-start: clamp(0.75rem, 2vh, 1.5rem);
  display: flex;
  flex-direction: column;
  max-inline-size: 54ch;
}

/* Bottom margin only, and only between blocks. Flex does not collapse margins, so a margin
   on both ends of adjacent children would add up - double between blocks, single at the
   edges - and a margin under the LAST block would push the column off the optical centre.

   `:not(:last-child)` is carrying the specificity as well as the rule. `*` contributes
   nothing, so `.fc-beat__body > *` would score (0,1,0) - a tie with the `margin: 0` each
   part declares for itself, decided by source order, which the parts win. The pseudo-class
   makes it (0,2,0) and the reset stays in force for every side except this one. */
.fc-beat__body > *:not(:last-child) {
  margin-block-end: calc(var(--spectrum-body-margin-multiplier) * 1em);
}

.fc-beat__text {
  margin: 0;
  /* Body copy tracks the viewport HEIGHT as well as its width here: the stage is pinned to
     100svh and cannot scroll, so on a short window the limit on how much copy fits is
     vertical. Taking the smaller of the two keeps the longest beat inside the screen. */
  font-size: clamp(0.9rem, min(1.15vw, 1.7vh) + 0.35rem, 1.15rem);
  line-height: var(--spectrum-body-line-height);
  color: var(--fc-ink-body);
  text-wrap: pretty;
}

/* THE THREE MARKED WORDS. `em` carries the emphasis semantically; the colour is the flag,
   struck in the same metal as the gold so the four patterns on the page read as one material.

   The flat colour is declared FIRST and unconditionally, and the pattern only replaces it
   inside @supports. Painting text with a gradient means `color: transparent`, so a browser
   that ignores background-clip would render three invisible words in the middle of the
   sentence - the fallback has to be the default, not the exception. */
.fc-em {
  font-style: normal;
  font-weight: 700;
  /* the rake is per-word, so each one catches the light across its own width rather than
     across the paragraph it happens to sit in */
  background-size: 100% 100%;
  background-repeat: no-repeat;
}

.fc-em--liberte { color: var(--fc-liberte); }
.fc-em--egalite { color: var(--fc-egalite); }
.fc-em--fraternite { color: var(--fc-fraternite); }

@supports (background-clip: text) or (-webkit-background-clip: text) {
  .fc-em {
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
  .fc-em--liberte { background-image: var(--fc-blue-pattern); }
  .fc-em--egalite { background-image: var(--fc-platine-pattern); }
  .fc-em--fraternite { background-image: var(--fc-red-pattern); }
}

/* The three failure modes. Marker is a gold rule rather than a bullet glyph - it reads as
   part of the mark's language, and it lines the quotes up on one edge. */
.fc-beat__list {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-direction: column;
  gap: clamp(0.3rem, 0.8vh, 0.55rem);
  font-size: clamp(0.85rem, min(1.05vw, 1.55vh) + 0.3rem, 1.05rem);
  line-height: var(--spectrum-body-line-height);
  color: var(--fc-ink-body);
}

.fc-beat__list li {
  position: relative;
  padding-inline-start: 1.5em;
}

/* The same gold point that separates the Tech tags, used here as the marker. One mark doing
   both jobs is why the two sections read as one system: between words there, in front of a
   line here. Absolutely positioned so a wrapped second line stays flush with the first. */
.fc-beat__list li::before {
  content: "";
  position: absolute;
  inset-inline-start: 0;
  inset-block-start: 0.52em;
  inline-size: 0.3rem;
  block-size: 0.3rem;
  rotate: 45deg;
  background-image: var(--fc-gold-pattern);
}

.fc-beat__list q {
  quotes: "\00ab\00a0" "\00a0\00bb";
  color: var(--fc-ink-muted);
  font-style: italic;
}

/* THE TECH TAGS. Six words with no sentence around them, so they are set as a row rather
   than a list: no bullets, no boxes. A gold lozenge sits BETWEEN items - `::before` on every
   item but the first - which reads as a separator rather than as a marker owned by the word
   after it, and it lets the row wrap at any point without a stray mark starting a line. */
.fc-beat__tags {
  margin: 0;
  padding: 0;
  list-style: none;
  display: flex;
  flex-wrap: wrap;
  align-items: baseline;
  gap: 0.15rem 0.7rem;
  font-size: clamp(0.72rem, 0.85vw + 0.2rem, 0.88rem);
  font-weight: 600;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fc-ink-strong);
}

.fc-beat__tags li {
  display: flex;
  align-items: baseline;
  gap: 0.7rem;
}

.fc-beat__tags li + li::before {
  content: "";
  inline-size: 0.3rem;
  block-size: 0.3rem;
  flex: none;
  rotate: 45deg;
  translate: 0 -0.15em;
  background-image: var(--fc-gold-pattern);
}

/* The client list is evidence, not body copy, so it is set as a caption: smaller, tracked
   out, and led by a gold label that names what follows. */
.fc-beat__refs {
  margin: 0;
  font-size: clamp(0.72rem, 0.85vw + 0.2rem, 0.85rem);
  line-height: var(--spectrum-detail-line-height);
  letter-spacing: 0.04em;
  color: var(--fc-ink-muted);
}

.fc-beat__refs-label {
  display: block;
  margin-block-end: 0.25em;
  font-weight: 700;
  letter-spacing: 0.14em;
  text-transform: uppercase;
  background-image: var(--fc-gold-pattern);
  -webkit-background-clip: text;
  background-clip: text;
  color: transparent;
}

/* A quiet marker, not a debug chip: the copy is the client's to write. */
.fc-await {
  font-family: var(--fc-font-mono);
  font-size: 0.85em;
  color: #8a8a8a;
}

/* ---- the closing screen -------------------------------------------------
   The opening screen said the name; this one says it again and then asks for a reply.
   TODO - ENDPOINT NOT DECIDED. `mailto:` is the only action that works with zero JavaScript
   and no third-party origin. It is a placeholder, not a decision. */

.fc-outro {
  display: grid;
  justify-items: center;
  text-align: center;
  gap: clamp(1rem, 3vh, 2rem);
  inline-size: min(100%, 34rem);
}

.fc-outro__line {
  margin: 0;
  max-width: 40ch;
  font-size: clamp(0.95rem, 1.25vw, 1.15rem);
  line-height: var(--spectrum-body-line-height);
  color: #cfcfcf;
}

.fc-form {
  inline-size: 100%;
  display: grid;
  gap: clamp(0.6rem, 1.6vh, 0.9rem);
  margin: 0;
}

/* THE TOGGLE. One question, two answers, and exactly one of them is true - so radios, not
   checkboxes: a control that can be both-on or both-off is not a toggle. The first is
   pre-checked in the markup so the form never submits without an answer.

   Zero JavaScript. The two inputs are visually hidden but still focusable and still the
   thing the label activates; the gold thumb is a separate element the TRACK slides, and the
   track knows which side to slide it to through :has(). */
.fc-toggle {
  margin: 0;
  padding: 0;
  border: 0;
  inline-size: 100%;
  text-align: start;
}

.fc-toggle__legend {
  padding: 0;
  margin-block-end: 0.45rem;
  font-family: var(--fc-font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fc-ink-muted);
}

.fc-toggle__track {
  position: relative;
  display: grid;
  grid-template-columns: 1fr 1fr;
  border: 1px solid var(--fc-border-hairline);
  background: var(--fc-surface-sunken);
  isolation: isolate;
}

/* Not display:none - that would take the radios out of the tab order and out of the
   accessibility tree, and the control would stop being operable by keyboard. */
.fc-toggle__input {
  position: absolute;
  inline-size: 1px;
  block-size: 1px;
  margin: -1px;
  padding: 0;
  border: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
}

.fc-toggle__option {
  position: relative;
  z-index: 1;
  padding: clamp(0.6rem, 1.6vh, 0.9rem) clamp(0.7rem, 2vw, 1rem);
  font-size: clamp(0.8rem, 1vw, 0.95rem);
  line-height: var(--spectrum-line-height-100);
  color: var(--fc-ink-muted);
  cursor: pointer;
  transition: color 0.18s ease;
}

/* The lit side goes to the page's strongest ink rather than to gold: the gold is under it,
   and gold text on a gold thumb would vanish. */
.fc-toggle__track:has(#fc-need-si:checked) [for="fc-need-si"],
.fc-toggle__track:has(#fc-need-produit:checked) [for="fc-need-produit"] {
  color: var(--fc-black);
  font-weight: 600;
}

.fc-toggle__thumb {
  position: absolute;
  z-index: 0;
  inset-block: 0;
  inset-inline-start: 0;
  inline-size: 50%;
  background-image: var(--fc-gold-pattern);
  transition: translate 0.24s cubic-bezier(.16,.84,.30,1);
}

.fc-toggle__track:has(#fc-need-produit:checked) .fc-toggle__thumb { translate: 100% 0; }

/* The focus ring belongs on the TRACK: the input it would normally sit on is 1px wide and
   clipped, so its own ring would be invisible. */
.fc-toggle__track:has(.fc-toggle__input:focus-visible) {
  outline: 2px solid var(--fc-white);
  outline-offset: 2px;
}

.fc-toggle__option:hover { color: var(--fc-ink-strong); }
.fc-toggle__track:has(#fc-need-si:checked) [for="fc-need-si"]:hover,
.fc-toggle__track:has(#fc-need-produit:checked) [for="fc-need-produit"]:hover {
  color: var(--fc-black);
}

@media (prefers-reduced-motion: reduce) {
  .fc-toggle__thumb, .fc-toggle__option { transition: none; }
}

/* A browser without :has() cannot move the thumb or light a side, and would leave a gold
   block stuck under the left option with no way to tell which is chosen. There, the thumb
   is dropped and the checked side is marked with a border instead - which :checked can do
   on its own, since the label follows the input in source order. */
@supports not selector(:has(*)) {
  .fc-toggle__thumb { display: none; }
  .fc-toggle__input:checked + .fc-toggle__option {
    color: var(--fc-ink-strong);
    font-weight: 600;
    box-shadow: inset 0 -3px 0 0 var(--fc-gold-300);
  }
}

.fc-field {
  margin: 0;
  display: grid;
  gap: 0.35rem;
  text-align: start;
}

.fc-field__hint { text-transform: none; letter-spacing: 0.04em; opacity: 0.75; }

/* THE TWO ROUTES. Left is the direct flow and holds everything it needs - address, message,
   send. Right is the same message carried by hand. The OU between them is the whole point of
   the layout: it says these are alternatives, not steps, which is what a send button sitting
   under the LinkedIn button failed to say.

   The right column takes its own width and the left takes the rest, so the textarea keeps
   every pixel the button does not need. */
.fc-routes {
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto auto;
  gap: clamp(0.75rem, 2.5vw, 1.75rem);
  align-items: stretch;
}

.fc-route {
  display: grid;
  gap: clamp(0.6rem, 1.6vh, 0.9rem);
  align-content: start;
}

/* the direct column's own rows: address, message (taking the slack), the send button */
.fc-route--direct { grid-template-rows: auto 1fr auto; }

.fc-field--message { min-block-size: 0; display: grid; grid-template-rows: auto 1fr; }
.fc-field--message textarea { block-size: 100%; min-block-size: 4.5rem; resize: vertical; }

/* A rule with the word sitting in it, rather than a word floating in a gap - the line is what
   makes the two columns read as one choice instead of two unrelated blocks. */
.fc-routes__or {
  margin: 0;
  display: grid;
  grid-template-rows: 1fr auto 1fr;
  justify-items: center;
  gap: 0.5rem;
  font-family: var(--fc-font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.12em;
  text-transform: uppercase;
  color: var(--fc-ink-muted);
}

.fc-routes__or::before,
.fc-routes__or::after {
  content: "";
  inline-size: 1px;
  background: var(--fc-border-hairline);
}

.fc-route--via { align-content: center; justify-items: start; }

.fc-route__label {
  margin: 0;
  font-family: var(--fc-font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fc-ink-muted);
}

/* Deliberately not a second .fc-button. The gold button is the page's one primary action and
   there must not be two: this is an outlined sibling that carries the mark's metal only on
   its edge and its glyph. */
.fc-linkedin {
  display: grid;
  grid-template-columns: auto auto;
  gap: 0.6rem;
  align-items: center;
  padding: clamp(0.6rem, 1.6vh, 0.85rem) clamp(0.75rem, 2vw, 1.1rem);
  border: 1px solid var(--fc-border-hairline);
  background: var(--fc-surface-raised);
  color: var(--fc-ink-body);
  text-decoration: none;
  transition: border-color 0.18s ease, color 0.18s ease;
}

.fc-linkedin__glyph { inline-size: 1.35rem; block-size: 1.35rem; color: var(--fc-gold-300); }

.fc-linkedin__text { display: grid; text-align: start; }

.fc-linkedin__lead {
  font-size: clamp(0.78rem, 0.95vw, 0.9rem);
  font-weight: 700;
  letter-spacing: 0.02em;
  color: var(--fc-ink-strong);
}

.fc-linkedin__sub {
  font-family: var(--fc-font-mono);
  font-size: 0.65rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fc-ink-muted);
}

.fc-linkedin:hover, .fc-linkedin:focus-visible { border-color: var(--fc-gold-300); }
.fc-linkedin:focus-visible { outline: 2px solid var(--fc-white); outline-offset: 2px; }

@media (prefers-reduced-motion: reduce) {
  .fc-linkedin { transition: none; }
}

.fc-field label {
  font-family: var(--fc-font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: #8a8a8a;
}

.fc-field input,
.fc-field textarea {
  inline-size: 100%;
  padding: 0.7em 0.85em;
  font: inherit;
  font-size: 0.95rem;
  color: var(--fc-white);
  background: #141414;
  border: 1px solid #2b2b2b;
  border-radius: 2px;
  resize: vertical;
}

.fc-field input:focus-visible,
.fc-field textarea:focus-visible {
  outline: 2px solid var(--fc-gold-400);
  outline-offset: 1px;
  border-color: transparent;
}

.fc-form__actions { margin: 0; justify-self: end; }

.fc-button {
  padding: 0.75em 1.9em;
  font: inherit;
  font-weight: 700;
  font-size: 0.9rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: var(--fc-black);
  background-image: var(--fc-gold-pattern);
  border: 0;
  border-radius: 2px;
  cursor: pointer;
}

.fc-button:focus-visible { outline: 2px solid var(--fc-white); outline-offset: 2px; }

.fc-link {
  color: var(--fc-ink-body);
  text-decoration-color: var(--fc-gold-200);
  text-underline-offset: 0.25em;
}

.fc-link:hover, .fc-link:focus-visible { color: var(--fc-gold-400); }


/* ---- the dot rail -------------------------------------------------------
   Five stops standing in for the hidden scrollbar. Each dot is a link to a zero-size mark
   placed at that section's scroll depth, so the rail navigates as well as reports - still
   with no script. */

/* ON THE LEFT, TWICE AS WIDE, AND BESIDE THE TEXT. The stage is pinned, so a finger drags
   and the screen does not move - the only thing that reports the scroll is this rail, and at
   1px on the trailing edge it was too quiet to do that job. Left, because that is where a
   reader in a left-to-right language starts the line and where the eye returns between them;
   a progress line there is read on the way back rather than hunted for.

   IT FOLLOWS THE COLUMN, NOT THE WINDOW. Pinned to the viewport edge it was fine on a narrow
   screen and absurd on a wide one: the stage is capped at --fc-wrap and centred, so at 1440
   the copy started 176px in while the rail sat at 28px - 148px of empty black between the
   reader and their own position. The offset below is the same arithmetic the stage does to
   centre itself, so the two cannot disagree: half the leftover width, plus the gutter, less
   the dot's own box. What is left is half a dot of air between the line and the text. */
.fc-dots {
  position: fixed;
  inset-inline-start: calc(
      max(0px, (100vw - var(--fc-wrap)) / 2) + var(--fc-stage-pad) - var(--fc-dot-size));
  inset-block-start: 50%;
  translate: 0 -50%;
  z-index: 40;
  display: grid;

  /* An explicit length with the dots spread across it, rather than a gap between them. That
     makes the spacing a consequence of the rail rather than the other way round, so the five
     dot centres land on exact quarters however tall it gets - which is what lets the fill
     below be aimed at them by number instead of by eye. */
  block-size: min(30vh, 15.5rem);
  align-content: space-between;
  gap: 0;
}

/* The dots say WHICH section; the line through them says exactly HOW FAR IN. Five discrete
   states cannot answer "how much is left" the way the scrollbar thumb did, so the rail
   carries both: a dim track, and a gold fill scaled by scroll progress. */
.fc-dots::before,
.fc-dots::after {
  content: "";
  position: absolute;
  inset-inline-start: 50%;
  /* exactly half a dot in at each end, so the track runs centre-to-centre: the fill's 0.25,
     0.5 and 0.75 then ARE dots two, three and four, with no fudge factor */
  inset-block: 0.7rem;
  inline-size: 2px;
  translate: -50% 0;
  border-radius: 2px;
}

.fc-dots::before { background: #303030; }

.fc-dots::after {
  background-image: var(--fc-gold-pattern);
  transform-origin: center top;
  scale: 1 0;
}

.fc-dot {
  display: grid;
  place-items: center;
  inline-size: var(--fc-dot-size);
  block-size: var(--fc-dot-size);   /* a finger-sized hit area around a very small dot */
  text-decoration: none;
  position: relative;
}

.fc-dot__hit {
  inline-size: 0.45rem;
  aspect-ratio: 1;
  border-radius: 50%;
  background-image: var(--fc-gold-pattern);
  opacity: 0.24;
}

/* the name only when it is wanted - hovered, or reached by keyboard */
/* The label hangs on the OPEN side of the rail - away from the screen edge, so it has room
   to run - which is why it moved across with it. */
.fc-dot__label {
  position: absolute;
  inset-inline-start: 100%;
  margin-inline-start: 0.5rem;
  white-space: nowrap;
  font-family: var(--fc-font-mono);
  font-size: 0.65rem;
  letter-spacing: 0.1em;
  text-transform: uppercase;
  color: var(--fc-gold-400);
  opacity: 0;
  translate: -0.35rem 0;
  transition: opacity 160ms ease, translate 160ms ease;
  pointer-events: none;
}

.fc-dot:hover .fc-dot__label,
.fc-dot:focus-visible .fc-dot__label { opacity: 1; translate: 0 0; }

.fc-dot:focus-visible { outline: 2px solid var(--fc-gold-400); outline-offset: 0; border-radius: 50%; }

/* the anchors the dots point at: no size, no ink, just a scroll depth */
/* These already sit at the exact scroll offset where each gate opens - that is what the dot
   rail links to - so they double as the sequence's snap points. Nothing else needs to know:
   the stops move, the snapping follows. */
.fc-stop {
  position: absolute;
  inset-inline-start: 0;
  inline-size: 1px;
  block-size: 1px;
  pointer-events: none;
  scroll-snap-align: start;
}

/* ---- MOBILE -------------------------------------------------------------
   The mark goes on TOP and the copy sits under it, instead of the 1/3 - 2/3 pair. A third of
   390px is 115px of mark beside 229px of copy: both too small, and the copy ran under the
   dot rail (measured, copy right edge 367 against a rail starting at 356). So the rail moves
   to the foot of the screen and lies down, and the stage reserves its height.

   Placed BEFORE the @supports blocks on purpose: the no-scroll-timeline fallback below has
   to keep winning on a phone too, and it lays the same markup out as plain stacked flow. */

@media (max-width: 48rem) {
  /* The 1/3 - 2/3 split survives the turn: it just stops being columns and becomes rows.
     The mark holds the top third, the copy the bottom two thirds. Proportional tracks, not
     `auto`, so the ratio is the layout rather than a by-product of content height - and the
     vertical padding stays small, because on a 844px screen every rem of gutter comes
     straight out of the room the text has to say anything in. */
  .fc-stage {
    grid-template-columns: minmax(0, 1fr);
    grid-template-rows: minmax(0, 1fr) minmax(0, 2fr);
    gap: 0;
    padding-block: clamp(0.5rem, 1.5vh, 1rem);
  }

  /* wide enough on BOTH sides to clear the vertical rail, so the mark and the copy stay
     centred on the screen rather than on the space left over beside it. Set as the token, so
     the rail moves in with the text instead of being told separately where to stand. */
  .fc-seq { --fc-stage-pad: clamp(3rem, 12vw, 3.75rem); }

  /* The intro spans both rows here, and its own 100svh floor was sizing them - it pushed the
     mark and its copy 320px apart with nothing in between. The stage is already a full
     viewport tall, so the floor is redundant while the sequence runs; the fallback further
     down puts it back for the case where the stage is not. */
  /* The intro spans both rows here, and its own 100svh floor was sizing them - it pushed the
     mark and its copy 320px apart with nothing in between. The tracks are proportional now,
     so the floor is not only redundant but actively wrong; the fallback further down puts it
     back for the case where the stage is not a full viewport. */
  /* THE OPENING SCREEN USES THE SAME TWO BANDS AS EVERY OTHER ONE. It spans both stage rows,
     so it used to centre its own four-row grid across the whole height and put the mark
     wherever that landed - measured at 390x844, 197px down, against 61px for the mark on the
     three sections. 136px of drift between the first screen and the rest of them, and the
     opening mark read as sunk.

     Stretching it and repeating the stage's own 1fr / 2fr split puts the mark in the top
     band and the words in the bottom one, which is exactly what the sections do. The ratio
     is not restated as a number: it is the same declaration, so the two cannot drift. */
  .fc-intro {
    grid-area: 1 / 1 / 3 / 2;
    min-block-size: 0;
    align-self: stretch;
    grid-template-rows: minmax(0, 1fr) minmax(0, 2fr);
  }
  .fc-lockup--intro { grid-row: 1; align-self: center; }
  .fc-intro__copy { grid-row: 2; align-self: start; }

  .fc-outro { grid-area: 1 / 1 / 3 / 2; align-self: center; }

  .fc-build {
    grid-area: 1 / 1 / 2 / 2;
    align-self: center;
    inline-size: min(48vw, 11rem);
    max-block-size: 100%;
  }

  /* the copy starts at the third-line rather than floating in the middle of its band, so the
     ratio is legible and long copy grows downward into room it already has */
  .fc-beats { grid-area: 2 / 1 / 3 / 2; align-self: start; }

  /* the copy centres under the mark rather than hanging off its right */
  .fc-beat { text-align: center; }
  .fc-beat__body { margin-inline: auto; max-inline-size: 34ch; }

  /* Stacked, the copy has a third of the screen rather than two thirds of the width, so this
     used to tighten paragraph spacing here to buy back the fold. It no longer needs to: the
     margin is a ratio of the type, and the type is already smaller at this width, so the
     spacing comes down on its own. Measured at 390x844 the longest beat (Design - two
     paragraphs, three items, a reference line) ends 142px clear of the fold on Spectrum's
     own 0.75. Only the LIST keeps a hand-set step, because its items are a column rather
     than prose and Spectrum has no token for the space between them. */
  .fc-beat__list { gap: clamp(0.25rem, 0.6vh, 0.4rem); }

  /* The list keeps its left edge even while the prose around it centres. Three items each
     running two or three lines read as a column, not as prose, and centring them would
     leave every line starting at a different x with the gold points scattered down the
     middle. The block itself is still centred; only its contents are flush left. */
  .fc-beat__list {
    text-align: start;
    inline-size: fit-content;
    margin-inline: auto;
  }

  .fc-beat__tags { justify-content: center; }

  /* NO EDGE CUES ON A TOUCH SCREEN. They sit at the top and bottom of the stage, which is
     where a phone already puts its own chrome and where a thumb already swipes - so they
     cost vertical room the copy needs and duplicate a gesture the device does natively. The
     OPENING cue stays: that one is not navigation, it is the invitation to start, and it is
     the only screen where a reader has no reason yet to think there is anything below. */
  .fc-cue--edge { display: none; }

  /* Two columns here would leave the message box too narrow to type in, so the routes stack
     and the OU turns from a vertical rule between them into a horizontal one across them. */
  .fc-routes { grid-template-columns: minmax(0, 1fr); }

  .fc-routes__or {
    grid-template-rows: none;
    grid-template-columns: 1fr auto 1fr;
    align-items: center;
  }

  .fc-routes__or::before,
  .fc-routes__or::after { inline-size: auto; block-size: 1px; }

  .fc-route--via { justify-items: stretch; justify-self: stretch; }
  .fc-linkedin { justify-content: center; }

  /* The lockup stacks for the same reason the stage does. Stacked, "stretch" would run the
     mark the full width of the text, so here it goes back to an explicit size - the
     stretch-to-text-height trick only means anything while the two sit side by side. */
  .fc-lockup {
    flex-direction: column;
    align-items: center;
    gap: clamp(0.75rem, 2.5vh, 1.25rem);
  }
  /* stacked, the mark answers to nothing above it, so it takes an explicit square - both
     axes, because the ink-to-ink rule above sets a block-size that would otherwise survive */
  .fc-lockup__mark {
    align-self: auto;
    inline-size: min(34vw, 7rem);
    block-size: min(34vw, 7rem);
  }
  .fc-lockup__text { text-align: center; align-items: center; }
  .fc-lockup__tag { justify-content: center; }

  /* THE RAIL GOES BACK TO THE SCREEN EDGE HERE. Tracking the column is what makes it read
     on a wide window, where the copy sits far inside the frame and a rail at the edge is
     stranded. A phone has no such gap: the gutter is 48px, the whole measure is 294px, and a
     line standing half a dot off the text is not "beside the reading", it is IN it. So on a
     touch screen it returns to the outer edge - out of the column entirely, still in view.

     The copy is not moved out of ITS way; the symmetric inline padding above already keeps
     them apart. Symmetric, because padding on one side alone would shove every centred thing
     on the screen off centre to buy the same clearance. */
  .fc-dots { inset-inline-start: clamp(0.75rem, 2vw, 1.75rem); }
  .fc-dot__label { font-size: 0.6rem; }
}

/* ---- ON: scroll opens the gates ----------------------------------------

   Five screens now, so eight viewports of travel. Where the gates sit, as percentages of
   the whole sequence:

        0 -  6   the intro fades up and out   (scrubbed - it answers the reader's own scroll)
        9        DESIGN gate     -> ball drops, DESIGN, design text
       29 - 33   design copy fades out
       34        DESIGN gate closes
       36        TECH gate       -> square draws around the standing ball, TECH, tech text
       56 - 60   tech copy fades out
       61        TECH gate closes
       63        INNOVATION gate -> the square opens, ticks land, INNOVATION, innovation text
       83 - 87   innovation copy fades out
       88        INNOVATION gate closes; the finished mark holds alone for a beat
       89        the mark leaves, CONTACT arrives -> lockup, line, form, back-to-top

   Each gate is a pair of stops one percent apart. Chrome resolves a discrete `display` step
   by holding the VISIBLE value across the whole interval rather than flipping at its
   midpoint, so a gate written `36% none -> 37% block` is already open at 36%, and one
   written `62% block -> 63% none` stays open until 63. The tail's two layers are written to
   exploit exactly that, so the closed one leaves on the same frame the open one arrives. */

@supports (animation-timeline: view()) {
  .fc-seq {
    /* THE WHOLE SCROLL BUDGET, and the only place it is stated. Nine viewports was a desk
       measurement - a wheel notch and a trackpad flick cover ground a thumb does not, and on
       a phone the same sequence read as work. Six point three is the same journey at 70% of
       the travel.

       Nothing else moves with it. Every gate below is written as a PERCENTAGE of the
       timeline, and the stops derive their offsets from this box, so the five screens keep
       their proportions and only the distance between them changes. */
    block-size: 630svh;
    view-timeline-name: --seq;
    view-timeline-axis: block;
  }

  .fc-stage {
    position: sticky;
    inset-block-start: 0;
  }

  /* each stop sits at its fraction of the travel, which is the sequence minus one viewport */
  .fc-stop { inset-block-start: calc(var(--at) * (100% - 100svh)); }

  /* --- what scroll drives: the intro, the gates, and the rail ------------ */

  .fc-intro,
  .fc-beat,
  .fc-outro,
  .fc-layer,
  .fc-dot__hit {
    animation-timeline: --seq;
    animation-range: contain 0% contain 100%;
    animation-fill-mode: both;
    animation-timing-function: linear;
    animation-duration: 1ms;   /* the timeline drives it; the duration only has to exist */
  }

  /* THIS ONE GATES OFF LIKE EVERY OTHER SCREEN. It used to fade to opacity 0 and stop
     there - no `display` step - so the opening screen stayed RENDERED for the whole
     sequence: a full-stage, invisible box lying over all four screens that follow, with a
     link inside it. Invisible is not gone. It still takes taps, and on a touch screen that
     is the difference between an arrow that works and one that does nothing.

     Off at 8%, one step before the first beat opens at 9%, so the two never overlap. Held
     visible through 7% rather than cut at 6% because a discrete `display` step holds the
     VISIBLE value across its interval - the same behaviour the tail's two layers rely on. */
  .fc-intro { animation-name: fc-intro-out; }
  @keyframes fc-intro-out {
    0%,  2%    { display: grid; opacity: 1; transform: none;
                 animation-timing-function: cubic-bezier(.55,0,.85,.35); }
    6%,  7%    { display: grid; opacity: 0; transform: translateY(-12vh); }
    8%, 100%   { display: none;  opacity: 0; transform: translateY(-12vh); }
  }

  .fc-layer--ball { animation-name: fc-gate-ball; }
  @keyframes fc-gate-ball {
    0%, 8%    { display: none; }
    9%, 88%   { display: block; }
    89%, 100% { display: none; }
  }

  .fc-layer--frame { animation-name: fc-gate-frame; }
  @keyframes fc-gate-frame {
    0%, 35%   { display: none; }
    36%, 88%  { display: block; }
    89%, 100% { display: none; }
  }

  /* this layer's job is to be taken away: it leaves at 63%, exactly where the open layer
     arrives holding the identical geometry fully drawn */
  .fc-layer--tail { animation-name: fc-gate-tail; }
  @keyframes fc-gate-tail {
    0%, 35%   { display: none; }
    36%, 62%  { display: block; }
    63%, 100% { display: none; }
  }

  .fc-layer--open { animation-name: fc-gate-open; }
  @keyframes fc-gate-open {
    0%, 62%   { display: none; }
    63%, 88%  { display: block; }
    89%, 100% { display: none; }
  }

  /* Two animations ride each copy wrapper: the gate (display) and the fade-out (opacity).
     Different properties, so they compose rather than fight - unlike two animations touching
     one property, where the last-declared simply wins and the other never renders. */
  .fc-beat--design { animation-name: fc-gate-design, fc-copy-out-design; }
  @keyframes fc-gate-design {
    0%, 8%    { display: none; }
    9%, 33%   { display: block; }
    34%, 100% { display: none; }
  }
  @keyframes fc-copy-out-design {
    0%,  29%  { opacity: 1; animation-timing-function: cubic-bezier(.55,0,.85,.35); }
    33%, 100% { opacity: 0; }
  }

  .fc-beat--tech { animation-name: fc-gate-tech, fc-copy-out-tech; }
  @keyframes fc-gate-tech {
    0%, 35%   { display: none; }
    36%, 60%  { display: block; }
    61%, 100% { display: none; }
  }
  @keyframes fc-copy-out-tech {
    0%,  56%  { opacity: 1; animation-timing-function: cubic-bezier(.55,0,.85,.35); }
    60%, 100% { opacity: 0; }
  }

  .fc-beat--innovation { animation-name: fc-gate-innovation, fc-copy-out-innovation; }
  @keyframes fc-gate-innovation {
    0%, 62%   { display: none; }
    63%, 87%  { display: block; }
    88%, 100% { display: none; }
  }
  @keyframes fc-copy-out-innovation {
    0%,  83%  { opacity: 1; animation-timing-function: cubic-bezier(.55,0,.85,.35); }
    87%, 100% { opacity: 0; }
  }

  .fc-outro { animation-name: fc-gate-contact; }
  @keyframes fc-gate-contact {
    0%, 88%   { display: none; }
    89%, 100% { display: grid; }
  }

  /* --- the rail: a continuous fill, and one dot lit at a time ------------ */

  /* The fill is the only thing on the page still SCRUBBED rather than played - which is
     right: it reports the reader's own position, so it must track the scroll exactly and
     never run on a clock of its own. */
  .fc-dots::after {
    animation: fc-rail-fill 1ms linear both;
    animation-timeline: --seq;
    animation-range: contain 0% contain 100%;
  }
  /* NOT a straight 0 -> 1. The sections are not evenly spaced on the scroll (9, 36, 63, 89)
     but the dots ARE evenly spaced on the rail, so a linear fill arrives at each dot late -
     it sat at 9% of the rail while the second section was already on screen. Pinning a stop
     per gate makes the fill land exactly on a dot at the instant that section opens, and
     travel at whatever rate the gap between them requires. */
  @keyframes fc-rail-fill {
    0%        { scale: 1 0; }      /* dot 1 - the screen at rest */
    9%        { scale: 1 0.25; }   /* dot 2 - Design */
    36%       { scale: 1 0.5; }    /* dot 3 - Tech */
    63%       { scale: 1 0.75; }   /* dot 4 - Innovation */
    89%, 100% { scale: 1 1; }      /* dot 5 - Contact */
  }


  /* Each dot crossfades over a 6% band CENTRED on the gate it marks - 9, 36, 63, 89 - so the
     outgoing and incoming dots pass through half strength together at the exact moment the
     section changes. The earlier 2% band read as a pop: two frames of scroll is not a fade,
     it is a cut with extra steps. Nothing snaps now, and the handover still reads as one
     event rather than two. */
  .fc-dot--1 .fc-dot__hit { animation-name: fc-dot-1; }
  @keyframes fc-dot-1 {
    0%,   6%  { opacity: 1;    scale: 1.45;
                animation-timing-function: cubic-bezier(.40,0,.60,1); }
    12%, 100% { opacity: 0.24; scale: 1; }
  }

  .fc-dot--2 .fc-dot__hit { animation-name: fc-dot-2; }
  @keyframes fc-dot-2 {
    0%,   6%  { opacity: 0.24; scale: 1;
                animation-timing-function: cubic-bezier(.40,0,.60,1); }
    12%, 33%  { opacity: 1;    scale: 1.45;
                animation-timing-function: cubic-bezier(.40,0,.60,1); }
    39%, 100% { opacity: 0.24; scale: 1; }
  }

  .fc-dot--3 .fc-dot__hit { animation-name: fc-dot-3; }
  @keyframes fc-dot-3 {
    0%,  33%  { opacity: 0.24; scale: 1;
                animation-timing-function: cubic-bezier(.40,0,.60,1); }
    39%, 60%  { opacity: 1;    scale: 1.45;
                animation-timing-function: cubic-bezier(.40,0,.60,1); }
    66%, 100% { opacity: 0.24; scale: 1; }
  }

  .fc-dot--4 .fc-dot__hit { animation-name: fc-dot-4; }
  @keyframes fc-dot-4 {
    0%,  60%  { opacity: 0.24; scale: 1;
                animation-timing-function: cubic-bezier(.40,0,.60,1); }
    66%, 86%  { opacity: 1;    scale: 1.45;
                animation-timing-function: cubic-bezier(.40,0,.60,1); }
    92%, 100% { opacity: 0.24; scale: 1; }
  }

  .fc-dot--5 .fc-dot__hit { animation-name: fc-dot-5; }
  @keyframes fc-dot-5 {
    0%,  86%  { opacity: 0.24; scale: 1;
                animation-timing-function: cubic-bezier(.40,0,.60,1); }
    92%, 100% { opacity: 1;    scale: 1.45; }
  }

  /* --- and what time plays, once its gate has opened --------------------- */

  #core {
    animation: fc-ball-drop var(--fc-t-ball) both;
  }

  /* The square is laid down by TWO elements in series - #frame-main then #tail-closed - and
     the pen has to cross the join without noticing it. It used to stop twice: the draw ran
     up to 3.0x its own mean speed and then braked to a DEAD stop at bottom-centre (ending on
     y2 = 1 means zero exit speed), and the tail then started again from zero and peaked at
     2.0x. That double stop-start, two thirds of the way along the bottom edge, is what read
     as a slow down at the end.

     A curve's exit speed is (1 - y2)/(1 - x2) and its entry speed y1/x1, both relative to
     its own mean. So the draw now ends on y2 = x2 - leaving at exactly its mean - and the
     tail starts on y1 = x1, entering at its own. The two paths were already cut to the same
     pen speed (221.1914/1.30s = 170 vs 51.8555/0.30s = 173), so the join is seamless and the
     square reads as one stroke: accelerate, cruise, land. Only the tail eases out, because
     only the tail actually stops; peaks are now 1.4x and 1.2x instead of 3.0x and 2.0x. */
  #frame-main {
    animation: fc-draw-main var(--fc-t-draw) cubic-bezier(.42,.06,.75,.75) both;
  }

  /* it closes the square the draw has just laid down, so it waits for it - and starts at the
     speed the draw left off (y1 = x1), landing soft at the corner it finishes on */
  #tail-closed {
    animation: fc-draw-tail var(--fc-t-tail) cubic-bezier(.30,.30,.80,1) var(--fc-t-draw) both;
  }
  @keyframes fc-draw-tail {
    from { stroke-dashoffset: 51.8555; }
    to   { stroke-dashoffset: 0; }
  }

  /* the opening: the tail retracts from its far end, back toward bottom-centre. The delay is
     a beat to register the closed square before it gives way. */
  #tail-open {
    animation: fc-open-tail var(--fc-t-open) cubic-bezier(.55,.06,.30,1) 0.2s both;
  }
  @keyframes fc-open-tail {
    from { stroke-dashoffset: 0; }
    to   { stroke-dashoffset: 51.8555; }
  }

  /* the ticks land once the gap has cleared them, so the break reads as deliberate */
  #tick2 {
    animation: fc-tick var(--fc-t-tick) cubic-bezier(.25,.85,.35,1)
               calc(0.2s + var(--fc-t-open) * 0.72) both;
  }
  #tick1 {
    animation: fc-tick var(--fc-t-tick) cubic-bezier(.25,.85,.35,1)
               calc(0.2s + var(--fc-t-open) * 0.92) both;
  }

  /* The copy rides with the mark, not behind it: the title starts on the same frame the gate
     opens, and the text follows while the title is still arriving, so the three read as one
     move rather than three waits. The text's only delay is a fraction of the title's own
     duration, so accelerating the title pulls the text forward with it. */
  .fc-beat__title {
    animation: fc-word-in var(--fc-t-word) cubic-bezier(.16,.84,.30,1)
               var(--fc-d-word, 0s) both;
  }
  @keyframes fc-word-in {
    from { opacity: 0; transform: translateY(1.5rem); }
    to   { opacity: 1; transform: none; }
  }
  @keyframes fc-word-in-calm {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  /* THE SAME TREATMENT THE OPENING SCREEN USES. Each part is PRESENT from the frame the gate
     opens, held back, and comes up as one block. It does not fade in from nothing: the copy
     is readable throughout and the sequence is it arriving at the front, not arriving at all.

     OPACITY ON THE BLOCK, NOT INK ON THE TEXT. This used to animate `color`, which reaches
     only what inherits it - so the gold list markers, the tricolore words and the reference
     label, all of which set their own colour, stayed at full strength while the prose around
     them was still held back, and the paragraph resolved in pieces. Opacity applies to the
     element, so every part of it moves together and the rule stops caring what anything
     inside is painted with. Held at --fc-o-copy-hold rather than at 0 - the point is copy
     that is present but not yet foreground, never copy that is missing.

     No reduced-motion variant to keep in step: nothing moves, so the same rule serves both
     paths.

     THE LEAD PARAGRAPH IS EXEMPT. It is the section's answer to its own title, so it carries
     the reading and has to be there the moment the title is. Holding it back bought nothing:
     the eye lands on it first and then waits for permission to read. Only what follows it -
     the list, the quotes, the reference line - resolves on the delay. */
  .fc-beat__body > *:not(:first-child) {
    /* The copy used to start at 0.45 of the title's own duration - 146ms - so it was already
       resolving while the title was still arriving and the two read as one event. It now
       waits for the title to FINISH and then holds a beat, so the section reads title first,
       then text. Expressed as the title's end plus a lag rather than as a multiplier of it,
       because the two are different decisions and only one of them is being tuned here. */
    --fc-d-copy: calc(var(--fc-d-word, 0s) + var(--fc-t-word) + var(--fc-t-copylag));
    animation: fc-copy-resolve var(--fc-t-copy) ease var(--fc-d-copy) both;
  }
  @keyframes fc-copy-resolve {
    from { opacity: var(--fc-o-copy-hold); }
    to   { opacity: 1; }
  }
  /* ONE FADE FOR THE WHOLE BLOCK. The parts were stepped 420ms apart to pace the reading, and
     it did the opposite: copy resolving under the eye while you are already reading it pulls
     attention back to the top instead of carrying it down. Every part shares --fc-d-copy, so
     the section resolves as one thing and then stays still.

     The animation stays on each child rather than moving to the block because the lead
     paragraph is exempt - the parent would take it with it. */

  /* the closing screen assembles top to bottom, one step per item */
  .fc-outro > * {
    animation: fc-rise-in var(--fc-t-copy) cubic-bezier(.16,.84,.30,1)
               calc(var(--fc-i-item, 0) * var(--fc-t-stagger)) both;
  }
  .fc-outro > :nth-child(1) { --fc-i-item: 0; }
  .fc-outro > :nth-child(2) { --fc-i-item: 1; }
  .fc-outro > :nth-child(3) { --fc-i-item: 2; }
  .fc-outro > :nth-child(4) { --fc-i-item: 3; }
  @keyframes fc-rise-in {
    from { opacity: 0; transform: translateY(1rem); }
    to   { opacity: 1; transform: none; }
  }
  @keyframes fc-rise-in-calm {
    from { opacity: 0; }
    to   { opacity: 1; }
  }

  /* REDUCED MOTION - damp it, do not delete it.
     An earlier version collapsed the whole sequence to a static stack here, which threw away
     the site's entire structure for anyone with the OS setting on - and that is a lot of
     people, including this project's own machine.

     The GATES are untouched: they are structure, not motion, and without them the sections
     would stack on top of each other. The dot rail is untouched too - it reports position,
     which is information. What comes out is unrequested TRAVEL: the ball's fall and the
     copy's slide become plain fades. The mark's CONSTRUCTION stays: the square drawing
     itself is the content, not an embellishment on it. */
  @media (prefers-reduced-motion: reduce) {
    .fc-intro      { animation-name: fc-intro-out-calm; }
    @keyframes fc-intro-out-calm {
      0%,  2%   { opacity: 1; animation-timing-function: cubic-bezier(.55,0,.85,.35); }
      6%, 100%  { opacity: 0; }
    }
    #core           { animation-name: fc-ball-drop-calm; }
    .fc-beat__title { animation-name: fc-word-in-calm; }
    /* the copy is not listed: it resolves from waiting grey to ink, which is a colour change
       and not motion, so it plays the same in both paths */
    .fc-outro > *   { animation-name: fc-rise-in-calm; }
  }
}

/* ---- OFF: no scroll-timeline support -----------------------------------
   Same markup, ordinary stacked blocks, everything visible and nothing pinned. The mark's
   markup base state is the UNBUILT one, because that is what the sequence needs at scroll 0,
   so here it is reset to the finished logo: the main run drawn, the closing tail dropped
   (that gap IS the logo), the ticks in place. Otherwise a browser without scroll timelines
   would show a bare circle as the company's mark.

   The rail cannot report a position without a scroll timeline, and a rail that never moves
   is worse than none, so it is dropped here rather than shown inert. */

@supports not (animation-timeline: view()) {
  .fc-stage {
    display: block;
    block-size: auto;
    padding-block: 10vh;
  }
  .fc-intro { margin-block-end: 14vh; min-block-size: 100svh; }
  .fc-build { inline-size: min(100%, 14rem); margin-inline: auto; }
  .fc-beats { display: block; }
  .fc-beat { margin-block-start: 6vh; text-align: center; }
  .fc-beat__text { margin-inline: auto; }
  .fc-outro { margin-block-start: 14vh; margin-inline: auto; }

  .fc-dots { display: none; }

  #frame-main { stroke-dashoffset: 0; }
  .fc-layer--tail, #tail-open { display: none; }
  #tick1, #tick2 { stroke-dashoffset: 0; }
}

/* ---- motion override ----------------------------------------------------
   Opt back IN to full motion when the OS says "reduce". Honouring the setting is the right
   default, but for a site whose structure IS motion it must not be a dead end: without this
   the author and a large share of visitors can never see what was designed.

   Zero JS. The checkbox precedes .fc-seq, so `:checked ~` reaches everything, and the
   selectors below outrank the plain rules inside the reduced-motion block. */

.fc-motion__input {
  position: fixed;
  inset-block-start: 0.85rem;
  inset-inline-end: 0.85rem;
  z-index: 50;
  accent-color: var(--fc-gold-300);
}

.fc-motion__label {
  position: fixed;
  inset-block-start: 0.8rem;
  inset-inline-end: 2.3rem;
  z-index: 50;
  font-family: var(--fc-font-mono);
  font-size: 0.7rem;
  letter-spacing: 0.08em;
  text-transform: uppercase;
  color: #7d7d7d;
  cursor: pointer;
  user-select: none;
}

.fc-motion__label:hover,
.fc-motion__input:focus-visible + .fc-motion__label { color: var(--fc-gold-400); }

#fc-motion:checked ~ .fc-seq .fc-intro       { animation-name: fc-intro-out; }
#fc-motion:checked ~ .fc-seq #core           { animation-name: fc-ball-drop; }
#fc-motion:checked ~ .fc-seq .fc-beat__title { animation-name: fc-word-in; }
#fc-motion:checked ~ .fc-seq .fc-outro > *   { animation-name: fc-rise-in; }

#fc-motion:checked ~ .fc-seq .fc-lockup--intro .fc-lockup__name,
/* Same trap as the reduce block above: these lists must carry the ink reveal too, or ticking
   Full motion would strand the name and the statement in waiting grey. */
#fc-motion:checked ~ .fc-seq .fc-lockup--intro .fc-tagword { animation-name: fc-intro-rise; }
#fc-motion:checked ~ .fc-seq .fc-lockup--intro .fc-lockup__name {
  animation-name: fc-intro-rise, fc-ink-reveal;
}
#fc-motion:checked ~ .fc-seq .fc-intro__line {
  animation-name: fc-intro-rise, fc-ink-reveal-body;
}
#fc-motion:checked ~ .fc-seq .fc-lockup--intro #intro-core { animation-name: fc-ball-drop; }

/* the bob is switched off entirely under reduce, so it needs restoring alongside the
   arrival - both, because naming one animation here would drop the other */
/* Scoped to the opening arrow, like the no-preference rule it mirrors: opting back into full
   motion must not start the three section arrows bobbing, which were never meant to. */
#fc-motion:checked ~ .fc-seq .fc-cue--intro {
  animation: fc-cue-in 0.5s cubic-bezier(.16,.84,.30,1) var(--fc-d-cue) both,
             fc-cue-hint 1.9s cubic-bezier(.45,0,.55,1) var(--fc-d-cue) infinite;
}
