/* =========================================================================
   Shoti — home page.
   Loaded after styles.css, home page only. Replaces the 728px reading shell
   that the essays / help / troubleshooting pages keep.

   Structure replicated from fabraix.agencidev.com, part for part:
     - one full-viewport screen; the ASCII field is the ground (ascii.js)
     - a floating nav pill, centred at the top, that opens downward into a
       menu drawer of large nav rows plus an action row
     - a right rail: a bright action card, and under it a dark panel that
       expands to a preview frame
     - the hero anchored bottom-left: a mono bracket tag, then one headline
       whose first sentence is bright and whose continuation is muted

   Their geometry, kept: 50px control height, 440px pill, 4px / 6px radii,
   the cubic-bezier(.22,1,.36,1) ease, the 8-step grey scale.
   Of their two frosted surfaces, the nav chrome's frost IS kept — an explicit
   product call (2026-08-26), Tier-B allowlisted where it lives in chrome.css;
   DESIGN-MARKETING.md §0 records the ruling. Everything else on the page
   fakes glass with solid plates, the rail included.

   The nav pill, the fx token set and the site footer moved to chrome.css —
   the essays and the individual essay pages carry the same nav now, and one
   copy of it is the only way that stays true. Load chrome.css before this.
   ========================================================================= */

/* ---------------------------------------------------------- the one screen */

/* The hero is still one full screen — it is just no longer the whole page.
   The essays strip sits under it, so the page scrolls; the ASCII field, the
   pill and the rail are all fixed, so they hold their positions while it does. */
body.home {
  background: var(--fx-black);
  color: var(--fx-off-white);
  min-height: 100dvh;
  overflow-x: hidden;
}

/* The reading shell is an essay frame — unwind it here. Padding and height are
   left to the hero: this selector outranks `.fx-hero`, so anything set here
   wins, and `min-height: 0` is what flattened the hero against the top of the
   page once the essays strip made the document longer than one screen. The
   hero's own screen-height rule is the one that has to survive. */
body.home main {
  background: transparent;
  border: 0;
  max-width: none;
  margin: 0;
}

/* Absolutely positioned inside the hero rather than fixed to the viewport.
   Fixed, the field stayed on screen after the hero had scrolled away and sat
   under the essays; clipped to the hero, it ends where the hero does. Out of
   flow, so it creates no grid track in the hero's two-row layout. */
#ascii {
  position: absolute;
  inset: 0;
  /* Explicit, and load-bearing: a canvas is a REPLACED element with an
     intrinsic 300x150, so `inset: 0` alone does not stretch it — `width: auto`
     resolves to that intrinsic width and the offsets are simply dropped, which
     leaves the field drawn into a 300x150 corner of the hero. The old fixed
     version got away without this because the script wrote style.width and
     style.height on every resize. */
  width: 100%;
  height: 100%;
  z-index: 0;
  display: block;
  background: var(--fx-black);
  pointer-events: none;
}

/* ---------------------------------------------------------- 2. the right rail */

/* Fixed, not absolute: the session keeps running while you read, so the panel
   stays on screen the way the pill does. It earns that by minimising — past
   the hero it collapses to its own title bar (the same grid-row collapse the
   toggle drives), which is 50px of furniture in the corner rather than a
   200px preview parked over the essays strip. Scroll back up to the hero and
   it opens again. The crossing is wired in index.html, on the observer that
   already watches the hero for the pill's calls.

   It holds the pill's line: same --fx-edge inset, same --fx-nav-h title row,
   so minimised it reads as the right-hand end of the header rather than as a
   loose card. */
.fx-rail {
  position: fixed;
  top: var(--fx-edge);
  right: var(--fx-edge);
  z-index: 30;
  width: min(180px, calc(50vw - 2rem));
  display: grid;
  gap: var(--s-2);
}

.fx-rail-card,
.fx-rail-item {
  position: relative;
  width: 100%;
  border-radius: var(--fx-radius);
  white-space: nowrap;
  overflow: hidden;
}
/* here */
/* Their bright card is an off-white plate at 60%, filling to 100% on hover.
   Two stacked opaque plates give the same read without a translucent stack. */
.fx-rail-card {
  display: flex;
  align-items: center;
  column-gap: var(--s-5);
  height: var(--fx-nav-h);
  padding-left: 11px;
  padding-right: var(--s-8);
  background: #9c9a96; /* off-white at 60% over black, flattened */
  color: var(--fx-black);
  transition: background 0.34s var(--fx-ease);
}
.fx-rail-card:hover {
  background: var(--fx-off-white);
  color: var(--fx-black);
}

.fx-rail-card--grey {
  background: var(--fx-dark);
  color: var(--fx-off-white);
  box-shadow: inset 0 0 0 1px var(--fx-border-soft); /* the navbar's edge, same value */
  justify-content: center;
  padding-inline: var(--s-3);
}
.fx-rail-card--grey:hover {
  background: #1b1b1d;
  color: var(--fx-off-white);
  box-shadow: inset 0 0 0 1px rgba(244, 242, 238, 0.22);
}
/* The download card is the brightest thing on the screen, which is the point.
   No mark on it, so the label centres and the padding evens out. */
.fx-rail-card--white {
  background: var(--fx-off-white);
  justify-content: center;
  padding-inline: var(--s-3);
}
.fx-rail-card--white:hover {
  background: #ffffff;
}

.fx-rail-mark {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 28px;
  height: 28px;
  flex: 0 0 auto;
  border-radius: 2px;
  background: var(--fx-black);
}
.fx-rail-mark img {
  height: 15px;
  width: auto;
}

.fx-rail-item {
  padding-inline: var(--s-3);
  background: var(--fx-raised);
  box-shadow: inset 0 0 0 1px var(--fx-border-soft);
  transition: box-shadow 200ms var(--fx-ease);
}
/* The panel is a door into the session, and until you hover it there is
   nothing saying so — the ring is the only affordance it has, so on hover it
   takes the one colour on this page that means live. */
.fx-rail-item:hover {
  box-shadow: inset 0 0 0 1px var(--fx-orange);
}
/* Minimised, the bar is not what you came to this part of the page for — it
   sits back a step and comes forward when you reach for it, so it stays
   available without competing with the copy it is now floating over.

   THE FADE IS ON THE ROW, NOT ON THE PLATE, and that is the correction. Fading
   .fx-rail-item made the whole element translucent — including its --fx-raised
   fill — so the page ran THROUGH the bar. Over the black bands nobody could
   tell; over the problem section's orange comb the stripes came straight up
   the middle of it and the bar read as broken glass. The plate stays opaque and
   the contents recede, which is what "sits back" was asking for anyway. */
.fx-rail[data-min="true"] .fx-rail-title {
  opacity: 0.72;
}
.fx-rail[data-min="true"] .fx-rail-item:hover .fx-rail-title,
.fx-rail[data-min="true"] .fx-rail-item:focus-within .fx-rail-title {
  opacity: 1;
}

.fx-rail-title {
  position: relative;
  z-index: 2;
  transition: opacity 200ms var(--fx-ease);
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: var(--s-3);
  width: 100%;
  height: var(--fx-nav-h);
  padding: var(--s-2) 0;
}
.fx-rail-title-label {
  flex: 1 1 auto;
  min-width: 0;
  font-family: var(--font-mono);
  font-size: 10px;
  font-weight: 500;
  letter-spacing: -0.02em;
  text-transform: uppercase;
  background: none;
  border: 0;
  padding: 0;
  color: var(--fx-off-white);
  text-align: left;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}

/* The sound control sits where the collapse glyph used to. The panel holds a
   running session with narration, so mute is the useful thing to reach for. */
.fx-rail-sound {
  flex-shrink: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 26px;
  height: 26px;
  padding: 0;
  background: none;
  border: 0;
  color: var(--fx-ash);
  transition: color 160ms var(--fx-ease);
}
.fx-rail-sound:hover {
  color: var(--fx-off-white);
}
.fx-rail-sound svg {
  display: block;
}
/* THE ARCS ARE ORANGE, THE CONE IS NOT. The glyph is one currentColor drawing
   that goes ash, then off-white on hover; the sound coming out of it is the
   part that means something is happening, so it takes the page's one accent —
   the same orange the panel's ring goes to mean live and the same one the
   founders' current role is set in. The cone keeps currentColor: it is the
   control, and it should still light with the button it sits in.

   The slash keeps currentColor too. It is the muted state — the opposite of
   live — so a slash in the live colour would be saying both at once. */
.fx-rail-sound .fx-wave {
  stroke: var(--fx-orange);
  transition: opacity 160ms var(--fx-ease);
}
.fx-rail-sound[aria-pressed="true"] .fx-wave {
  opacity: 0;
}

/* THE ICON TALKS WHILE THE SOUND IS ON. The panel holds a narrated session, and
   a static speaker says only that audio is permitted — this says someone is
   speaking, which is the thing worth pressing for.

   THE TWO ARCS RUN ON THE SAME CLOCK AND DIFFERENT SCORES, which is the whole
   figure. The near arc is lit for most of the loop and dips; the far one comes
   and goes. That is what a voice looks like on a meter — the quiet part of a
   sentence still moves the needle a little, and only the loud syllables throw
   it to the top. Both arcs pulsing together would be a metronome, and a
   metronome reads as a progress indicator rather than as speech.

   THE STOPS ARE DELIBERATELY UNEVEN. Speech is not periodic; a keyframe on
   regular stops gives a bleep-bleep-bleep the eye locks onto and then finds
   annoying two floors down the page. The gaps here are syllable-length and
   different every time round, so it reads as talking and stays ignorable.

   IT IS GATED ON aria-pressed, so mute stops the mouth as well as the sound —
   the [aria-pressed="true"] rule above already takes both arcs to zero, and
   these animations never run against it because the selector requires "false".

   NOT TIED TO THE SESSION'S ACTUAL AUDIO, and that is a deliberate limit rather
   than an oversight. The show owns its narration and the button asks it to mute
   (see applyMute in index.html); there is no "is a voice sounding right now"
   signal to read, and inventing one — polling the frame for a playing Audio —
   would be a second source of truth for a decoration. The claim it makes is the
   one the button already makes: sound is on. */
.fx-rail-sound[aria-pressed="false"] .fx-wave--near {
  animation: fx-talk-near 2.4s var(--fx-ease-inout) infinite;
}
.fx-rail-sound[aria-pressed="false"] .fx-wave--far {
  animation: fx-talk-far 2.4s var(--fx-ease-inout) infinite;
}

@keyframes fx-talk-near {
  0%, 6% { opacity: 0.42; }
  12%, 27% { opacity: 1; }
  33% { opacity: 0.5; }
  39%, 51% { opacity: 1; }
  58% { opacity: 0.38; }
  66%, 79% { opacity: 1; }
  86% { opacity: 0.55; }
  92%, 100% { opacity: 0.42; }
}

@keyframes fx-talk-far {
  0%, 10% { opacity: 0.12; }
  17%, 26% { opacity: 1; }
  34% { opacity: 0.14; }
  43%, 49% { opacity: 0.9; }
  57% { opacity: 0.1; }
  70%, 76% { opacity: 1; }
  84% { opacity: 0.18; }
  90%, 100% { opacity: 0.12; }
}
.fx-rail-sound .fx-slash {
  opacity: 0;
  transition: opacity 160ms var(--fx-ease);
}
.fx-rail-sound[aria-pressed="true"] .fx-slash {
  opacity: 1;
}

.fx-rail-children {
  display: grid;
  grid-template-rows: 0fr;
  transition: grid-template-rows var(--fx-dur) var(--fx-ease);
}

.fx-rail-item[data-open="true"] .fx-rail-children {
  grid-template-rows: 1fr;
}
.fx-rail-children > div {
  overflow: hidden;
  min-height: 0;
}

.fx-rail-preview {
  display: block;
  width: 100%;
  padding-block: var(--s-3);
}
.fx-rail-preview-frame {
  position: relative;
  display: block;
  height: 200px;
  overflow: hidden;
  background: var(--fx-black);
  box-shadow: inset 0 0 0 1px var(--fx-border-soft);
  transition: opacity 0.3s var(--fx-ease);
}
.fx-rail-preview:hover .fx-rail-preview-frame {
  opacity: 0.82;
}

/* The live session runs at a real desktop size and is scaled into the panel —
   rendering it at panel size instead would lay the demo out for a 156px
   viewport, which is not the same picture at all. transform-origin plus the
   offset put the city, not the sidebar, in the frame. It never takes the
   pointer: the anchor over it owns the click, which opens the full session. */
.fx-rail-live {
  position: absolute;
  top: 0;
  left: 0;
  width: 1280px;
  height: 800px;
  border: 0;
  transform-origin: 0 0;
  transform: scale(0.42) translate(-354px, -170px);
  pointer-events: none;
  opacity: 0;
  transition: opacity 600ms var(--fx-ease);
  z-index: 1;
}
.fx-rail-live.is-live {
  opacity: 1;
}

/* The preview holds the city — the company drawn as a map. It is 16:9 in a
   portrait frame, so `cover` shows a vertical slice of it; the drift walks
   that slice across the illustration, which is where the movement comes from.
   (The session clip that was here before is the two-pane letters UI, not the
   illustration, and its centre crop landed on the empty gutter between its
   two columns — which is why it read as blank.) */
.fx-rail-preview-frame img,
.fx-rail-preview-frame video {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  object-fit: cover;
  object-position: center;
  transform: scale(1.08);
  animation: fx-drift 34s var(--fx-ease-inout) infinite alternate;
}

@keyframes fx-drift {
  from {
    transform: scale(1.08) translate3d(-9%, 1%, 0);
  }
  to {
    transform: scale(1.14) translate3d(9%, -1%, 0);
  }
}

/* ---------------------------------------------------------- 3. the hero */

/* In flow rather than pinned to the viewport: it holds the first screen and
   the essays strip follows it. A fixed hero would have ridden down over the
   strip on scroll.

   Two rows. The first takes whatever is left and centres the headline in it,
   which lands the type on the figure's face — the darkest part of the field,
   and the reason the headline needs no plate behind it. The second is the
   calls, sitting on the foot of the screen where the ground is densest. */
.fx-hero {
  position: relative;
  z-index: 10;
  min-height: 100dvh;
  display: grid;
  grid-template-rows: 1fr auto;
  justify-items: center;
  align-items: center;
  text-align: center;
  padding: calc(var(--fx-edge) * 2 + var(--fx-nav-h)) var(--fx-edge)
           max(28px, calc(44 / 1440 * 100vw));
  overflow: hidden;              /* the clip the field is drawn into */
  pointer-events: none;
}
.fx-hero a {
  pointer-events: auto;
}

/* Everything except the canvas sits on top of it. */
.fx-hero__row,
.fx-hero__calls {
  position: relative;
  z-index: 1;
}

.fx-hero__tag {
  margin: 0 0 var(--s-4);
  color: var(--fx-smoke);
}

.fx-hero__row {
  display: flex;
  flex-direction: column;
  align-items: center;
  /* Pulled up off dead centre: the optical middle of the first screen sits
     above the geometric one, and the figure's face is up there too. */
  margin-block: 0 6vh;
}

/* Smaller than it was, and centred. It reads as a statement standing in the
   middle of the field rather than a band running along its foot, and the
   figure behind it is what carries the width now. */
.fx-hero__line {
  margin: 0;
  max-width: 22ch;
  font-size: clamp(21px, 2.35vw, 34px);
  line-height: 1.3;
  font-weight: 400;
  letter-spacing: -0.02em;
  color: var(--fx-off-white);
  text-wrap: balance;
}

.fx-hero__sub {
  margin: var(--s-4) 0 0;
  max-width: 44ch;
  font-size: 14px;
  line-height: 1.5;
  letter-spacing: -0.006em;
  color: var(--fx-smoke);
  text-wrap: pretty;
}

/* The calls, centred on the foot of the screen. Equal-width tracks rather than
   buttons sized by their own labels: side by side, two buttons of different
   widths read as a primary and an afterthought, and these are one choice with
   two answers. Grid rather than flex because only grid gives both tracks the
   same width without hard-coding one — and the hidden platform variant creates
   no track at all, so the pair stays even whichever Download is showing. */
.fx-hero__calls {
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: 1fr;
  gap: var(--s-2);
  width: min(100%, 25rem);
}

/* "Book a call" sits over the field, and a bare outline let the dither read
   straight through the label. So it takes a material instead: a dark
   translucent panel with a hairline ring and a lit top edge — the specular
   line is the detail that makes a flat translucent panel read as glass rather
   than as a grey box.

   The frosting itself is deliberately absent. A true frosted material is what
   Apple's is, and it is exactly the tell DESIGN.md §8 bans and that the Tier B
   CSS rules in scripts/verify-language.sh catch by property name — the same
   rule that took fabraix's two frosted surfaces out of the pill and the rail.
   (Naming those two properties here would trip the verifier on this comment,
   which is why it does not.) This gets the read without the tell.

   Scoped to the hero on purpose: the drawer's copy of this button sits on a
   solid raised surface where an outline is correct and a fill is noise. */
.fx-hero__calls .fx-btn-outline {
  background: rgba(18, 18, 19, 0.62);
  color: var(--fx-off-white);
  box-shadow:
    inset 0 0 0 1px rgba(244, 242, 238, 0.16),
    inset 0 1px 0 0 rgba(244, 242, 238, 0.22);
}
.fx-hero__calls .fx-btn-outline:hover {
  background: rgba(30, 30, 32, 0.78);
  box-shadow:
    inset 0 0 0 1px rgba(244, 242, 238, 0.3),
    inset 0 1px 0 0 rgba(244, 242, 238, 0.34);
}

/* Too narrow to sit side by side: one per row, still equal. */
@media (max-width: 560px) {
  .fx-hero__calls {
    grid-auto-flow: row;
    grid-auto-columns: auto;
    width: min(100%, 22rem);
  }
}

/* ---------------------------------------------------------- 4. the problem */

/* The statement and the reading of it sit on one line, then the figure runs
   the full width underneath. Two columns rather than one because the two are
   different registers — the claim, and the detail that earns it — and stacked
   they read as one long paragraph in two type sizes. */
.home-problem {
  position: relative;
  z-index: 10;
  background: var(--fx-black);
  /* The foot is short on purpose. Two full-bleed sections in a row each pay
     their own vertical padding, and the two stack — the boundary below the
     comb was measuring more than 400px because both sides were being generous
     about the same gap. The section below owns that gap; this one just stops. */
  padding: max(104px, calc(168 / 1440 * 100vw)) var(--fx-edge)
           max(32px, calc(48 / 1440 * 100vw));
}

.home-problem .page-eyebrow {
  margin: 0 0 var(--fx-space-lg);
}

.home-problem__row {
  display: grid;
  grid-template-columns: minmax(0, 1.55fr) minmax(0, 1fr);
  align-items: start;
  gap: var(--fx-space-lg);
  margin: 0 0 max(56px, calc(84 / 1440 * 100vw));
}

/* Light and small: this is a statement, not a hero line, and at 400 it was
   competing with the H1 one screen up. 300 is a real weight here — it was
   added to the page's font request rather than left to the browser to fake,
   which produces a smeared 400 rather than a light. */
.home-problem__lead {
  margin: 0;
  max-width: 34ch;
  font-size: clamp(19px, 1.85vw, 26px);
  line-height: 1.38;
  font-weight: 300;
  letter-spacing: -0.014em;
  color: var(--fx-off-white);
  text-wrap: pretty;
}

/* Sits on the same top line as the statement, at reading size — the register
   change is what tells you which of the two is the claim. */
.home-problem__note {
  margin: 0;
  max-width: 46ch;
  justify-self: end;
  font-size: 14px;
  line-height: 1.6;
  color: var(--fx-smoke);
  text-wrap: pretty;
}

/* One column below 900: the note is a second voice on the statement, not a
   column beside it, and at phone width two columns leave both of them too
   narrow to read. Restored here after it was deleted along with the section
   that used to sit between the comb and the founders. */
@media (max-width: 900px) {
  .home-problem__row {
    grid-template-columns: minmax(0, 1fr);
    gap: var(--fx-space-md);
  }
  .home-problem__note {
    justify-self: start;
  }
}

/* The comb is a flat SVG of rects, so it scales without a script and without a
   raster to ship. width/height on the element give it an aspect ratio before
   it loads, which is what stops the essays below it jumping up the page on a
   slow connection; height: auto is what keeps that ratio while it fills the
   column. */
.problem-comb {
  display: block;
  width: 100%;
  height: auto;
}

/* ----------------------------------------------------- 5. the core features */

/* Five plates, three across, the wide one spanning two:

     [ 01 ][      02      ]
     [ 03 ][  04  ][  05  ]

   Each plate is drawn as a technical plate: a hairline frame, the figure over
   a measuring grid, and the copy under it. No header bar — the figure is the
   first thing on the plate, and the title under it names the part. */
/* THE ROOM ABOVE THE PLATES IS THIS SECTION'S, NOT THE FOUNDERS'. The row of
   portraits closes with its own bottom padding; on top of that the plates
   take a band of their own, so the eyebrow reads as the start of a new part
   of the page rather than as the caption under the row that came before it.
   Split across the two sections rather than piled onto one, because the
   founders row is not always what sits above this. */
.home-features {
  position: relative;
  z-index: 10;
  background: var(--fx-black);
  padding: max(40px, calc(64 / 1440 * 100vw)) var(--fx-edge)
           max(48px, calc(72 / 1440 * 100vw));
}

.home-features__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--fx-space-md);
  margin: 0 0 var(--fx-space-lg);
}
.home-features__head .page-eyebrow {
  margin: 0;
}
.home-features__count {
  flex: 0 0 auto;
  font-family: var(--font-mono);
  font-size: 10px;
  letter-spacing: -0.02em;
  color: var(--fx-ash);
}

/* minmax(0,1fr) rather than 1fr so a long title cannot push a column wider
   than its share. */
.plans {
  display: grid;
  grid-template-columns: repeat(3, minmax(0, 1fr));
  gap: var(--fx-space-sm);
}
.plan--wide {
  grid-column: span 2;
}

.plan {
  display: flex;
  flex-direction: column;
  border: 1px solid var(--fx-border-soft);
  border-radius: var(--fx-radius);
  background: var(--fx-raised);
  overflow: hidden;
}

/* THE FIGURE IS ONE HEIGHT ON EVERY PLATE, whatever its width, so the titles
   under them sit on one line across a row. The drawing fits inside it
   (`meet`) and the measuring grid runs under the whole box, so a wide plate
   shows more grid rather than a stretched drawing.

   The grid is an SVG tile, not a gradient: a 26x15 rhombus is two hairlines
   at 30 degrees, the same angle every drawing is projected at. */
.plan__figure {
  position: relative;
  height: clamp(300px, 26.5vw, 400px);
  overflow: hidden;
  background-color: var(--fx-raised);
  background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='26' height='15'%3E%3Cpath d='M0 7.5L13 0l13 7.5M0 7.5L13 15l13-7.5' fill='none' stroke='%23f4f2ee' stroke-opacity='.07'/%3E%3C/svg%3E");
  background-size: 26px 15px;
  background-position: center;
}
/* THE TOP ROW IS TALLER. The labyrinth is the one drawing with a real
   footprint — 35 cells across and 35 deep — and at the standard height it
   had to be cropped to fill the plate, which cut the signs at the top edge.
   The row takes the height the maze needs and the maze fits it whole
   (`meet`, not `slice`); the map beside it simply gets more room. */
.plan--map .plan__figure,
.plan--wide .plan__figure {
  height: clamp(320px, 27vw, 420px);
}
.plan__draw {
  position: absolute;
  inset: 0;
  width: 100%;
  height: 100%;
  display: block;
  /* the drawing's own type: the pill labels and the tile names */
  font-family: var(--font-sans, "DM Sans", sans-serif);
}

/* THE BODY IS GLASS OVER THE FIGURE. It rides up over the bottom of the
   drawing rather than sitting under it, and what shows through is the grid
   and the figure's feet, frosted — the copy reads on a pane, not on a black
   block, and the plate no longer breaks in two at the figure's edge.

   A gradient and a blur, on purpose and on record: both are Tier-B tells the
   verifier flags, allowlisted here on the CPO's call (2026-08-27) for this
   one surface. Solid where the copy sits, clear where the figure shows. */
.plan__body {
  position: relative;
  z-index: 1;
  margin-top: -64px;
  padding: calc(var(--fx-space-sm) + 12px) 14px var(--fx-space-md);
  border-top: 1px solid rgba(244, 242, 238, 0.09);
  /* language-flip-ok — the plate body's frosted pane, CPO call 2026-08-27 */
  background: linear-gradient(180deg, rgba(9, 9, 10, 0.34) 0%, rgba(9, 9, 10, 0.86) 46%, var(--fx-raised) 100%);
  /* language-flip-ok — the plate body's frosted pane, CPO call 2026-08-27 */
  -webkit-backdrop-filter: blur(14px) saturate(1.1);
  /* language-flip-ok — the plate body's frosted pane, CPO call 2026-08-27 */
  backdrop-filter: blur(14px) saturate(1.1);
}
/* THE BODY IS ONE HEIGHT ON EVERY PLATE. The figure above it is already a
   fixed height, so pinning the title to one line and the copy to two makes
   every plate exactly as tall as every other — including down a column, which
   the grid's own stretching cannot do on its own. */
.plan__title {
  margin: 0;
  font-size: 17px;
  line-height: 1.3;
  min-height: 22px;
  font-weight: 500;
  letter-spacing: -0.016em;
  color: var(--fx-off-white);
  white-space: nowrap;
  overflow: hidden;
  text-overflow: ellipsis;
}
/* BALANCE, NOT PRETTY. `pretty` only protects the last line from being a
   runt; it leaves the lines above it full, so a two-line paragraph reads as
   one long line and a short one. `balance` levels every line in the block,
   which is what makes the five bodies sit as one block of text across the
   row rather than as five ragged shapes. It applies up to a browser-set line
   count (well above the two here) and falls back to normal wrapping where it
   is unsupported, so nothing is riding on it.

   NO LINE CLAMP. Two lines was the right height and the wrong mechanism: the
   plates in a row are grid children, so they are already equal height without
   help — the clamp only capped the row, and the price was copy cut mid-
   sentence into an ellipsis at every width where a line broke one word early
   (measured: "on its next move...." at 1440). min-height holds the two-line
   floor so a short body cannot shrink the row; the copy is written to fit it. */
.plan__copy {
  margin: 6px 0 0;
  max-width: 64ch;
  min-height: 42px;
  font-size: 14px;
  line-height: 1.5;
  color: var(--fx-smoke);
  text-wrap: balance;
}


/* Registered so they interpolate: an unregistered custom property flips at
   the keyframe rather than easing to it, and a pill that snaps orange is a
   blink where a pill that warms is a signal arriving. */
@property --pill {
  syntax: "<color>";
  inherits: true;
  initial-value: #f4f2ee;
}
@property --lift {
  syntax: "<length>";
  inherits: true;
  initial-value: 0px;
}
/* TWO GATES, ONE OPACITY. A sign arrives on its own clock — when the line's
   tip reaches it — and leaves on the drawing's clock, with every other sign.
   One animation cannot do both: the arrival is staggered by animation-delay,
   which shifts every later keyframe by the same amount, so an exit written as
   a percentage leaves late by exactly as much as the sign arrived late. So the
   arrival and the departure are two animations, and because both would other-
   wise be writing `opacity`, each writes its own registered number and the
   opacity is their product. Interpolable because they are registered; an
   unregistered property would jump at the keyframe instead of fading to it. */
@property --in {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}
@property --out {
  syntax: "<number>";
  inherits: false;
  initial-value: 0;
}

/* ---- the drawing's own palette. Three faces by tone — the reference plates
   draw white blocks with black edges on paper; on black ground the tones flip,
   so the faces are three near-blacks and the edge is the off-white. */
.plan__draw .f-t { fill: #1c1c1f; }
.plan__draw .f-l { fill: #131315; }
.plan__draw .f-r { fill: #0d0d0f; }
.plan__draw .f-t,
.plan__draw .f-l,
.plan__draw .f-r {
  stroke: var(--fx-off-white);
  stroke-width: 1;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.plan__draw .ground {
  fill: none;
  stroke: rgba(244, 242, 238, 0.18);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
.plan__draw .wire {
  fill: none;
  stroke: rgba(244, 242, 238, 0.28);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}

/* Every animation waits for the section to be on screen — the observer in
   index.html flips data-on — so five loops are not running under a hero the
   reader has not scrolled past yet. */
.home-features[data-on="false"] .plan__draw *,
.home-features[data-on="false"] .plan--agents .plan__figure {
  animation-play-state: paused;
}

/* 01 — THE MAP. Buildings of different heights scattered over the ground
   plane — a city drawn on a map, not one block stacked. They rise from the
   back of the plate to the front, one after another, stand, and fall away to
   rise again. The lit one carries the page's one accent: the place on the map
   where something is live right now. */
.plan--map .cube {
  animation: fx-rise 8s var(--fx-ease) infinite;
  animation-delay: calc(var(--i) * 110ms);
  opacity: 0;
}
@keyframes fx-rise {
  0%       { opacity: 0; transform: translateY(14px); }
  7%, 84%  { opacity: 1; transform: translateY(0); }
  91%, 100% { opacity: 0; transform: translateY(14px); }
}
.plan--map .cube.lit .f-t,
.plan--map .cube.lit .f-l,
.plan--map .cube.lit .f-r {
  stroke: var(--fx-orange);
}
.plan--map .live {
  fill: var(--fx-orange);
  animation: fx-live 8s var(--fx-ease-inout) infinite;
  animation-delay: calc(var(--i) * 110ms);
  opacity: 0;
}
@keyframes fx-live {
  0%, 8%   { opacity: 0; }
  12%, 84% { opacity: 1; }
  30%, 60% { opacity: 0.35; }
  91%, 100% { opacity: 0; }
}

/* 02 — THE LABYRINTH. The company as a walled maze, drawn to the x402 "the
   problem" plate: rings of one-cube-high wall with a corridor spiralling in
   between them, a line that draws itself down the corridor, and each intent
   standing up on its wall the moment the line's tip reaches it.

   pathLength normalises the corridor to 100, so the line's tip position IS
   the fraction of the draw phase — which is what lets a pill's --at (its
   fraction of the way along) become its animation-delay rather than a number
   somebody tuned by eye until it looked about right.

   The maze is the ground here, not the subject, so its edges sit back a step
   from the off-white every other plate draws volumes in; the line and the
   signs are what should be read first. */
.plan--broadcast .cube .f-t,
.plan--broadcast .cube .f-l,
.plan--broadcast .cube .f-r {
  stroke: rgba(244, 242, 238, 0.3);
}
.plan--broadcast .route {
  fill: none;
  stroke: rgba(244, 242, 238, 0.12);
  stroke-width: 1;
  stroke-linecap: round;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.plan--broadcast .signal {
  fill: none;
  stroke: var(--fx-orange);
  stroke-width: 2;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-dasharray: 100 100;
  stroke-dashoffset: 100;
  animation: fx-draw 11s linear infinite;
}
/* Draws over the first 52% at one speed — linear, because a tip that eases
   would arrive at each sign at a time no delay could predict — then stands,
   fades, and resets while it cannot be seen.

   IT IS THE LAST THING TO GO. The signs leave together at 58–64%; the line
   holds past them to 70% and only then fades. What the reader is left with for
   that beat is the route itself, which is the point of the plate: the signs
   were posted along a path, and the path is what remains. */
@keyframes fx-draw {
  0%          { stroke-dashoffset: 100; opacity: 1; }
  52%, 70%    { stroke-dashoffset: 0; opacity: 1; }
  78%         { stroke-dashoffset: 0; opacity: 0; }
  78.01%, 100% { stroke-dashoffset: 100; opacity: 0; }
}
.plan--broadcast .pill-side {
  fill: #0c0c0e;
  stroke: var(--fx-off-white);
  stroke-width: 1;
}
.plan--broadcast .pill-face {
  fill: #191a1c;
  stroke: var(--fx-off-white);
  stroke-width: 1;
}
.plan--broadcast .pill-text {
  fill: var(--fx-off-white);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: -0.01em;
}
/* Placed here rather than by an attribute: --x/--y put the slab on its stretch
   of corridor, then the matrix lays its local x along the iso axis that rises
   to the right and leaves local y vertical, so the sign stands upright and
   reads uphill. --lift is the hop it makes as it arrives. */
.plan--broadcast .pill {
  transform: translate(var(--x), calc(var(--y) + var(--lift, 0px))) matrix(0.866, -0.5, 0, 1, 0, 0);
  opacity: calc(var(--in) * var(--out));
  animation:
    fx-post 11s var(--fx-ease) infinite,
    fx-unpost 11s var(--fx-ease) infinite;
  /* 5.72s is the 52% draw phase: a sign at --at along the corridor stands up
     exactly when the tip gets there. Only the arrival is delayed; the exit
     runs on the drawing's own clock, undelayed, so it lands at one moment for
     every sign. */
  animation-delay: calc(var(--at) * 5.72s), 0s;
}
/* The arrival, on the sign's own clock. It hops up as the tip reaches it and
   stands. The fall back to 0 at 50–56% is not the exit the reader sees — by
   then --out has already closed the sign — it is the reset that has to happen
   somewhere, parked inside the window where the drawing is dark. That window
   holds for any sign posted between a fifth and most of the way along the
   corridor, which is the whole span the maze's signs can occupy. */
@keyframes fx-post {
  0%           { --in: 0; --lift: 12px; }
  4%           { --in: 1; --lift: 0px; }
  50%          { --in: 1; --lift: 0px; }
  56%, 100%    { --in: 0; --lift: 12px; }
}
/* The departure, on the drawing's clock. Every sign shares it exactly, so the
   four leave as one row and not as four staggered blinks — and they leave
   before the line does. */
@keyframes fx-unpost {
  0%           { --out: 0; }
  6%           { --out: 1; }
  58%          { --out: 1; }
  64%, 100%    { --out: 0; }
}

/* 03 — THE REPLIES. The teammate's towers on one side, the agent's on the
   other, and the hall between them where the words change hands. The towers
   stand still, and so does the hall; the letter running the ground route
   from one side through the hall to the other is the only thing that moves. */
.plan--replies .plate {
  fill: none;
  stroke: rgba(244, 242, 238, 0.22);
  stroke-width: 1;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.plan--replies .route {
  fill: none;
  stroke: rgba(244, 242, 238, 0.22);
  stroke-width: 1;
  vector-effect: non-scaling-stroke;
}
/* ONE DASH, TWO LEGS. The loop is drawn as two polylines of equal length —
   out along the front, back along the rear — each normalised to 50 with a
   dash pattern that repeats every 100. So over one 100-unit cycle the dash is
   on the front leg for the first half and on the back leg for the second;
   the back leg runs the same animation half a cycle behind, and at the turn
   the tail leaves one leg as the head enters the other. Linear throughout,
   and no wrap inside a leg, which is what the flicker was: an open path's
   pattern re-entering at the start while it was still leaving at the end. */
.plan--replies .dash {
  fill: none;
  stroke: var(--fx-orange);
  stroke-width: 1.5;
  stroke-linecap: round;
  stroke-dasharray: 9 91;
  stroke-dashoffset: 0;
  animation: fx-run 4.8s linear infinite;
}
.plan--replies .dash--back {
  animation-delay: -2.4s;
}
@keyframes fx-run {
  to { stroke-dashoffset: -100; }
}
.plan--replies .roof {
  fill: #232326;
}
.plan--replies .gable {
  fill: #1c1c1f;
}
.plan--replies .cella .f-t,
.plan--replies .cella .f-l,
.plan--replies .cella .f-r {
  stroke: rgba(244, 242, 238, 0.4);
}
/* 04 — THE CATCH. A field of cursors — everyone's agents — all pointing at
   one spark, and a human hand reaching for the same spot. It stands still on
   purpose: the reach is the picture. Placement is by custom property so the
   markup carries no transform attribute; the hand is a cutout on the plate's
   own ground, flush to the top-right corner the way it was drawn. */
.plan__draw--fill .cur {
  fill: var(--fx-off-white);
  stroke: var(--fx-raised);
  stroke-width: 1.4;
  paint-order: stroke;
  transform: translate(var(--x), var(--y)) rotate(var(--a)) scale(var(--s, 1));
}
.plan__draw--fill .spark {
  fill: var(--fx-off-white);
  transform: translate(var(--x), var(--y));
}

/* 05 — THE RUNTIMES. One crate per runtime on the ground plane the rest of
   the set stands in, drawn the way the reference draws its crates: a heavy
   outline, a second box set down and to the right of the first, the face to
   the reader filled solid and carrying the runtime's own mark, a soft shadow
   thrown down and to the left, and a dash behind it.

   THE TONES ARE THE ONLY THING THAT MOVED. The geometry is traced point for
   point from the reference the CPO supplied; on paper it fills the boxes white
   and the mark face near-black, and on this ground that flips — the boxes take
   the set's two dark face tones and the mark face takes the light one, with
   the mark itself on the page's accent.

   LIGHT GREY, NOT THE OFF-WHITE, and the outline with it. Five faces at
   #f4f2ee are the brightest thing on the page by a distance, and a plane
   brighter than the headline reads as a sticker laid over the drawing rather
   than as a side of a box. --fx-chalk sits one step under, which is also the
   grey the reference draws its own outlines in.

   GREY, NOT THE OFF-WHITE. Five faces at #f4f2ee are the brightest thing on
   the page by a distance, and a plane brighter than the headline reads as a
   sticker laid over the drawing rather than as a side of the box. --fx-chalk
   sits one step under, so the face is lit but still part of the scene.

   A CRATE RATHER THAN A CHIP. A row of upright logo chips is a badge wall, and
   a badge wall reads as a claim about endorsement. A crate on the map reads as
   the claim actually being made: whatever you run, there is already a box on
   your company's map with your name on it. Monochrome for the same reason.

   The motion is the reference's: a crate arrives — it lifts, tips back as it
   comes, and settles, with the trail showing behind it on the way. Each keeps
   its own clock, so five runtimes are running at once rather than in turn. */
.plan--agents .c-t { fill: #1c1c1f; }
.plan--agents .c-l { fill: #131315; }
.plan--agents .c-r { fill: var(--fx-chalk); }
.plan--agents .c-t,
.plan--agents .c-l,
.plan--agents .c-r {
  stroke: var(--fx-chalk);
  stroke-width: 2.2;
  stroke-linejoin: round;
  vector-effect: non-scaling-stroke;
}
.plan--agents .c-mark path {
  fill: var(--fx-orange);
}
/* The mark lies IN the face: the matrix lays its x along that face's own axis
   and leaves its y vertical, the same trick the signs on plate 02 use. */
.plan--agents .c-mark {
  transform: translate(var(--x), var(--y)) matrix(0.866, -0.5, 0, 1, 0, 0);
}
.plan--agents .c-shadow {
  fill: rgba(244, 242, 238, 0.12);
  stroke: none;
}
.plan--agents .c-shadow--soft {
  fill: rgba(244, 242, 238, 0.05);
}
/* The reference's accent, and two more behind it. They stand — they are part
   of the drawing, not a trail that arrives — and float with the stack. */
.plan--agents .c-dash {
  fill: none;
  stroke: var(--fx-chalk);
  stroke-width: 2;
  stroke-linecap: round;
  opacity: 0.85;
}
.plan--agents .tile-label {
  fill: var(--fx-ash);
  font-family: var(--font-mono);
  font-size: 7px;
  letter-spacing: 0.02em;
  text-transform: uppercase;
}

/* THE MOTION IS A FLOAT, NOT AN ARRIVAL. The reference's pieces hang and
   drift rather than tumble in and land, and a tumble was reading as the box
   being dropped — a shape at rest that keeps flinching. So: one slow rise
   along the drawing's own axis and back, no rotation.

   --ph is where a stack sits in that shared 7s clock, written by the
   generator: odd ones start half a cycle in, so one is always rising while
   the one beside it falls. Alternation is the whole point of a row of five —
   in step they read as a mechanism, and at random they read as noise. */
.plan--agents .crate,
.plan--agents .c-dash {
  animation: fx-float 7s var(--fx-ease-inout) infinite;
  animation-delay: var(--ph);
}
@keyframes fx-float {
  0%, 100% { transform: translate(0.8px, 1.2px); }
  50%      { transform: translate(-0.8px, -4px); }
}
/* THE GROUND MOVES, SO THE STACKS DO. Five boxes bobbing in place read as
   five boxes bobbing in place; what says travelling is the ground going past
   them. The plate's own measuring grid drifts one tile toward the top left —
   in from the bottom right, the corner the stacks are facing away from — and
   because the travel is exactly one tile, 26 by 15, the loop has no seam.
   Linear, not eased: a drift that slows at each end is a rock, not a drift.
   30px of travel over 3.4s is about 9px a second — quick enough that the
   stacks read as travelling rather than as bobbing over a still floor, and
   still under the speed at which a moving grid becomes a scroll.

   It is the FIGURE that moves and not the drawing, so this sits outside
   .plan__draw — and so it has to be named again in both gates that switch the
   plate's motion off: the off-screen pause, and the reduced-motion still. */
.plan--agents .plan__figure {
  animation: fx-ground 3.4s linear infinite;
}
@keyframes fx-ground {
  from { background-position: 50% 50%; }
  to   { background-position: calc(50% - 26px) calc(50% - 15px); }
}
/* The shadow stays on the ground and opens as the stack rises off it — a
   shadow travelling with the box reads as a sticker. */
.plan--agents .c-shadow {
  transform-box: fill-box;
  transform-origin: center;
  animation: fx-cast 7s var(--fx-ease-inout) infinite;
  animation-delay: var(--ph);
}
@keyframes fx-cast {
  0%, 100% { transform: scale(1); opacity: 1; }
  50%      { transform: scale(1.1); opacity: 0.55; }
}

/* Two across, then one. The wide plate stays wide; the map goes wide with it
   so the two rows of the narrower layout both read as full lines. */
@media (max-width: 1000px) {
  .plans {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .plan--map,
  .plan--agents {
    grid-column: span 2;
  }
  .plan__figure {
    height: clamp(300px, 42vw, 400px);
  }
}
@media (max-width: 680px) {
  .plans {
    grid-template-columns: minmax(0, 1fr);
  }
  /* One column gives the copy less width, so it gets a third line — the same
     third line on every plate, so they still all stand the same height. */
  .plan__copy {
    -webkit-line-clamp: 3;
    min-height: 63px;
  }
  /* The broadcasts drawing is laid out wide. In one column that squeezes it
     to a texture, so it is drawn over-wide and the plate crops the empty
     margins rather than the drawing shrinking to fit them. */
  .plan--broadcast .plan__draw {
    width: 190%;
    left: -45%;
  }
  .plan--map,
  .plan--wide,
  .plan--agents {
    grid-column: auto;
  }
  .plan__figure {
    height: clamp(280px, 72vw, 380px);
  }
}

/* ------------------------------------------------------- 6. the essays strip */

/* The section under the hero. It paints its own solid black rather than
   letting the ASCII field show through from behind: the field is the hero's
   ground, and reading three excerpts over a moving one is work. Full-bleed to
   the same --fx-edge the hero uses, so the two share a left margin. */
.home-essays {
  position: relative;
  z-index: 10;
  background: var(--fx-black);
  padding: max(64px, calc(96 / 1440 * 100vw)) var(--fx-edge)
           max(64px, calc(112 / 1440 * 100vw));
}

/* The label at one end, the way through to the index at the other. */
.home-essays__head {
  display: flex;
  align-items: baseline;
  justify-content: space-between;
  gap: var(--fx-space-md);
  margin: 0 0 var(--fx-space-lg);
}
.home-essays__head .page-eyebrow {
  margin: 0;
}
.home-essays__all {
  flex: 0 0 auto;
  color: var(--fx-smoke);
}
.home-essays__all:hover {
  color: var(--fx-off-white);
}

/* ---------------------------------------------------------- narrow screens */

/* The pill is 440px and the rail 180px; below this the two collide, so the
   rail goes — the same call fabraix makes. Nothing is lost: the drawer holds
   Download and every nav row, and the hero foot keeps the demo link. */
@media (max-width: 900px) {
  .fx-rail {
    display: none;
  }
}

@media (max-width: 720px) {
  .fx-hero__line {
    font-size: clamp(20px, 5.6vw, 27px);
  }
}

/* The rail's preview is the first thing to go when the screen is too short to
   hold the pill, the rail and the hero at once. */
@media (max-height: 620px) {
  .fx-rail-item {
    display: none;
  }
}

/* ---------------------------------------------------------- reduced motion */

@media (prefers-reduced-motion: reduce) {
  .fx-drawer,
  .fx-rail-children,
  .fx-menu-row,
  .fx-bar,
  .fx-btn,
  .fx-rail-card,
  .fx-rail-item,
  .fx-rail-title,
  .fx-rail-preview-frame {
    transition: none;
  }
  .fx-nav[data-open="true"] .fx-menu-row {
    transition-delay: 0s;
  }
  /* The drift stops, but the zoom stays — it is composition, not motion. */
  .fx-rail-preview-frame img,
  .fx-rail-preview-frame video {
    animation: none;
    transform: scale(1.08);
  }
  /* The mouth stops and both arcs stand lit, which is the icon's own resting
     state — sound-on, said once instead of said continuously. */
  .fx-rail-sound[aria-pressed="false"] .fx-wave--near,
  .fx-rail-sound[aria-pressed="false"] .fx-wave--far {
    animation: none;
    opacity: 1;
  }
  /* The feature plates stand on their finished frame: the city built, the
     signal drawn the whole way down the maze with every intent standing, the
     runtimes all at rest. What goes is only the letter on the replies wire,
     which has no still that means anything. */
  .plan__draw * {
    animation: none !important;
  }
  /* The runtimes' ground stops with them. It is the one plate animation that
     lives on the figure rather than inside the drawing, so the rule above
     does not reach it. */
  .plan--agents .plan__figure {
    animation: none;
  }
  .plan--map .cube,
  .plan--map .live {
    opacity: 1;
  }
  .plan--broadcast .signal {
    stroke-dashoffset: 0;
  }
  /* The signs stand too. With the animations off they were left on the base
     style, and the base style is both gates shut — a drawn line and no
     intents, which is not the finished frame the comment above promises. */
  /* Not tokens: fx-post and fx-unpost animate these two, and they are pinned
     open because the animations that open them are switched off here. */
  /* site-token-ok: an animated property, pinned, not a design token. */
  .plan--broadcast .pill { --in: 1; --out: 1; }
  .plan--replies .dash {
    display: none;
  }
}

/* ---------------------------------------------------------- OS-adaptive CTA */

/* The <html class="os-windows"> flag set in <head> picks which download
   variant is shown. This block sits last on purpose: .fx-rail-card and
   .fx-btn both declare `display`, and at equal specificity the later rule
   wins — placed earlier, the swap loses and both variants render. */
.dl-win {
  display: none;
}
.os-windows .dl-mac {
  display: none;
}
.os-windows .dl-win {
  display: flex;
}

/* ---------------------------------------------------------- the cursor */

/* REVERTED to the system cursor. The drawn right-pointing arrow put its
   hotspot on the arrow's tip at the RIGHT edge of the image, so the visible
   body sat ~15px to the LEFT of the actual click point. Every pointing
   convention says the active point is the arrow's leading corner, so people
   aimed the body at a target and clicked 15px to its right — which, on a 28px
   control, misses. That is the "hard to click" burger: the hit area measured
   correct at every point, the pointer was simply not where it looked.

   If the drawn arrow comes back, it has to point up-LEFT with the hotspot on
   that tip, the way every system cursor does. */

/* ------------------------------------------------------------ 7. the footer */

/* chrome.css dresses the footer itself; what belongs to this page is only that
   it sits on the page's own ground with a little air above the last section,
   rather than hard against the essay cards. */
.home .site-foot {
  background: var(--fx-black);
  padding-top: max(48px, calc(72 / 1440 * 100vw));
}
